diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77d220e633..d7a76f2673 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -57,6 +57,12 @@ jobs: command: fmt args: --all -- --check + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets -- -D warnings + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -82,9 +88,3 @@ jobs: with: command: test args: --workspace -- --ignored - - - name: Clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index b9d06b3a8e..def19342de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,6 +466,7 @@ checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core 0.4.5", + "axum-macros", "bytes", "futures-util", "http 1.1.0", @@ -553,6 +554,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "axum-test" version = "16.2.0" @@ -4524,20 +4536,20 @@ dependencies = [ [[package]] name = "nym-api-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "sha2 0.10.8", @@ -4665,7 +4677,7 @@ dependencies = [ [[package]] name = "nym-bin-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "const-str", "log", @@ -4989,11 +5001,11 @@ dependencies = [ [[package]] name = "nym-coconut-bandwidth-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", ] [[package]] @@ -5012,15 +5024,22 @@ dependencies = [ [[package]] name = "nym-coconut-dkg-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", "cw2", "cw4", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", +] + +[[package]] +name = "nym-common-models" +version = "0.1.0" +dependencies = [ + "serde", ] [[package]] @@ -5050,7 +5069,7 @@ dependencies = [ [[package]] name = "nym-compact-ecash" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bincode", "bls12_381", @@ -5060,8 +5079,8 @@ dependencies = [ "ff", "group", "itertools 0.12.1", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "rand", "serde", "sha2 0.9.9", @@ -5085,12 +5104,12 @@ dependencies = [ [[package]] name = "nym-config" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "dirs", "handlebars", "log", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "toml 0.8.14", "url", @@ -5114,7 +5133,7 @@ dependencies = [ [[package]] name = "nym-contracts-common" version = "0.5.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", @@ -5249,12 +5268,12 @@ dependencies = [ [[package]] name = "nym-credentials-interface" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bls12_381", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "rand", "serde", "strum 0.26.3", @@ -5293,12 +5312,12 @@ dependencies = [ [[package]] name = "nym-crypto" version = "0.4.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "ed25519-dalek", - "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "serde_bytes", "subtle-encoding", @@ -5372,14 +5391,14 @@ dependencies = [ [[package]] name = "nym-ecash-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", "cw-utils", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "thiserror", ] @@ -5403,7 +5422,7 @@ dependencies = [ [[package]] name = "nym-ecash-time" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "time", ] @@ -5431,7 +5450,7 @@ dependencies = [ [[package]] name = "nym-exit-policy" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "serde", "serde_json", @@ -5663,7 +5682,7 @@ dependencies = [ [[package]] name = "nym-group-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cw-controllers", @@ -5691,11 +5710,11 @@ dependencies = [ [[package]] name = "nym-http-api-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "http 1.1.0", - "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "reqwest 0.12.4", "serde", "serde_json", @@ -5874,7 +5893,7 @@ dependencies = [ [[package]] name = "nym-mixnet-contract-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", @@ -5882,7 +5901,7 @@ dependencies = [ "cw-controllers", "humantime-serde", "log", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "serde-json-wasm", @@ -5984,7 +6003,7 @@ dependencies = [ [[package]] name = "nym-multisig-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -6012,7 +6031,7 @@ dependencies = [ [[package]] name = "nym-network-defaults" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "dotenvy", "log", @@ -6207,18 +6226,18 @@ dependencies = [ [[package]] name = "nym-node-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "base64 0.22.1", "celes", "humantime 2.1.0", "humantime-serde", - "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "serde_json", @@ -6227,6 +6246,22 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-status-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.20", + "nym-bin-common 0.6.0", + "nym-common-models", + "reqwest 0.12.4", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + [[package]] name = "nym-node-status-api" version = "0.1.0" @@ -6240,16 +6275,20 @@ dependencies = [ "futures-util", "moka", "nym-bin-common 0.6.0", + "nym-common-models", "nym-explorer-client", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "nym-task", - "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "regex", "reqwest 0.12.4", "serde", "serde_json", "serde_json_path", "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", "thiserror", "tokio", "tokio-util", @@ -6365,7 +6404,7 @@ dependencies = [ [[package]] name = "nym-pemstore" version = "0.3.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "pem", ] @@ -6438,7 +6477,7 @@ dependencies = [ [[package]] name = "nym-serde-helpers" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "base64 0.22.1", "bs58", @@ -6753,7 +6792,7 @@ dependencies = [ [[package]] name = "nym-sphinx-types" version = "0.2.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "sphinx-packet", "thiserror", @@ -6917,7 +6956,7 @@ dependencies = [ [[package]] name = "nym-validator-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "base64 0.22.1", @@ -6935,20 +6974,19 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", - "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "prost 0.12.6", "reqwest 0.12.4", "serde", @@ -6958,6 +6996,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "url", "wasmtimer", "zeroize", @@ -7017,12 +7056,12 @@ dependencies = [ [[package]] name = "nym-vesting-contract-common" version = "0.7.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "thiserror", ] @@ -7089,12 +7128,12 @@ dependencies = [ [[package]] name = "nym-wireguard-types" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "base64 0.22.1", "log", - "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "thiserror", "x25519-dalek", @@ -8001,9 +8040,9 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.1.0" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ "bitflags 2.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index d431d4567b..d99e3d7532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "common/ip-packet-requests", "common/ledger", "common/mixnode-common", + "common/models", "common/network-defaults", "common/node-tester-utils", "common/nonexhaustive-delayqueue", @@ -121,6 +122,7 @@ members = [ "nym-node/nym-node-http-api", "nym-node/nym-node-requests", "nym-node-status-api", + "nym-node-status-agent", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -148,12 +150,14 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "common/models", "explorer-api", "gateway", "mixnode", "nym-api", "nym-data-observatory", "nym-node", + "nym-node-status-api", "nym-validator-rewarder", "nym-node-status-api", "service-providers/authenticator", @@ -314,6 +318,7 @@ si-scale = "0.2.3" sphinx-packet = "0.1.1" sqlx = "0.7.4" strum = "0.26" +strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b63631ddaa..11a2c76f2b 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -45,3 +45,4 @@ tracing = [ "opentelemetry", ] clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +models = [] diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index c90d731adb..f35c662e88 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -531,6 +531,18 @@ where } if res.status().is_success() { + #[cfg(debug_assertions)] + { + let text = res.text().await.inspect_err(|err| { + tracing::error!("Couldn't even get response text: {err}"); + })?; + tracing::trace!("Result:\n{:#?}", text); + + serde_json::from_str(&text) + .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + } + + #[cfg(not(debug_assertions))] Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) diff --git a/common/models/Cargo.toml b/common/models/Cargo.toml new file mode 100644 index 0000000000..acb6e35682 --- /dev/null +++ b/common/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-common-models" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/common/models/src/lib.rs b/common/models/src/lib.rs new file mode 100644 index 0000000000..3d85e66947 --- /dev/null +++ b/common/models/src/lib.rs @@ -0,0 +1 @@ +pub mod ns_api; diff --git a/common/models/src/ns_api.rs b/common/models/src/ns_api.rs new file mode 100644 index 0000000000..9c3373802a --- /dev/null +++ b/common/models/src/ns_api.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestrunAssignment { + /// has nothing to do with GW identity key. This is PK from `gateways` table + pub testrun_id: i64, + pub gateway_pk_id: i64, +} diff --git a/envs/canary.env b/envs/canary.env index c4aac15082..db70a19fd0 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 EXPLORER_API=https://canary-explorer.performance.nymte.ch/api -NYXD="https://canary-validator.performance.nymte.ch" -NYM_API="https://canary-api.performance.nymte.ch/api" +NYXD=https://canary-validator.performance.nymte.ch +NYM_API=https://canary-api.performance.nymte.ch/api diff --git a/envs/qa.env b/envs/qa.env index e88f845416..34306a62ea 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8 REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api -NYXD="https://qa-validator.qa.nymte.ch" -NYM_API="https://qa-nym-api.qa.nymte.ch/api" +NYXD=https://qa-validator.qa.nymte.ch +NYM_API=https://qa-nym-api.qa.nymte.ch/api diff --git a/envs/sandbox.env b/envs/sandbox.env index 4763269a6f..6310b8fa89 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api -NYXD="https://rpc.sandbox.nymtech.net" -NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" -NYM_API="https://sandbox-nym-api1.nymtech.net/api" +NYXD=https://rpc.sandbox.nymtech.net +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket +NYM_API=https://sandbox-nym-api1.nymtech.net/api diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 2415c90c69..50c5431c25 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -83,7 +83,9 @@ impl ExplorerClient { } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) } else { - Err(ExplorerApiError::RequestFailure(response.text().await?)) + let status = response.status(); + let err_msg = format!("{}: {}", response.text().await?, status); + Err(ExplorerApiError::RequestFailure(err_msg)) } } diff --git a/nym-data-observatory/README_SQLX.md b/nym-data-observatory/README_SQLX.md index 3e1021c8eb..72cca4294f 100644 --- a/nym-data-observatory/README_SQLX.md +++ b/nym-data-observatory/README_SQLX.md @@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled ### Possible solutions - does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`? + + `cargo install -f sqlx-cli --version ` ``` cargo install sqlx-cli --version --force ``` diff --git a/nym-node-status-agent/.gitignore b/nym-node-status-agent/.gitignore new file mode 100644 index 0000000000..bf462f3e05 --- /dev/null +++ b/nym-node-status-agent/.gitignore @@ -0,0 +1 @@ +nym-gateway-probe diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml new file mode 100644 index 0000000000..a2ce8cdf97 --- /dev/null +++ b/nym-node-status-agent/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + + +[package] +name = "nym-node-status-agent" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true} +clap = { workspace = true, features = ["derive", "env"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +reqwest = { workspace = true, features = ["json"] } +serde_json = { workspace = true } diff --git a/nym-node-status-agent/run.sh b/nym-node-status-agent/run.sh new file mode 100755 index 0000000000..5054a90825 --- /dev/null +++ b/nym-node-status-agent/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -eu + +export RUST_LOG=${RUST_LOG:-debug} + +crate_root=$(dirname $(realpath "$0")) +gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core +echo "gateway_probe_src=$gateway_probe_src" +echo "crate_root=$crate_root" + +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" + +# build & copy over GW probe +function copy_gw_probe() { + pushd $gateway_probe_src + cargo build --release --package nym-gateway-probe + cp target/release/nym-gateway-probe "$crate_root" + $crate_root/nym-gateway-probe --version + popd +} + +function build_agent() { + cargo build --package nym-node-status-agent --release +} + +function swarm() { + local workers=$1 + echo "Running $workers in parallel" + + build_agent + + for ((i=1; i<=$workers; i++)); do + ../target/release/nym-node-status-agent run-probe & + done + + wait + + echo "All agents completed" +} + +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" + +copy_gw_probe + +swarm 30 + +# cargo run -- run-probe diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs new file mode 100644 index 0000000000..c4465797d0 --- /dev/null +++ b/nym-node-status-agent/src/cli.rs @@ -0,0 +1,109 @@ +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_common_models::ns_api::TestrunAssignment; +use std::sync::OnceLock; +use tracing::instrument; + +use crate::probe::GwProbe; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Args { + #[command(subcommand)] + pub(crate) command: Command, + #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] + pub(crate) server_address: String, + + #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] + pub(crate) server_port: u16, + // TODO dz accept keypair for identification / auth +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Command { + RunProbe { + /// path of binary to run + #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] + probe_path: String, + #[arg(short, long, env = "NODE_STATUS_AGENT_GATEWAY_ID")] + gateway_id: Option, + }, +} + +impl Args { + pub(crate) async fn execute(&self) -> anyhow::Result<()> { + match &self.command { + Command::RunProbe { + probe_path, + gateway_id, + } => self.run_probe(probe_path, gateway_id).await?, + } + + Ok(()) + } + + async fn run_probe(&self, probe_path: &str, gateway_id: &Option) -> anyhow::Result<()> { + let server_address = format!("{}:{}", &self.server_address, self.server_port); + + let probe = GwProbe::new(probe_path.to_string()); + + let version = probe.version().await; + tracing::info!("Probe version:\n{}", version); + + let testrun = request_testrun(&server_address).await?; + + let log = probe.run_and_get_log(gateway_id); + + submit_results(&server_address, testrun.testrun_id, log).await?; + + Ok(()) + } +} + +const URL_BASE: &str = "internal/testruns"; + +#[instrument(level = "debug", skip_all)] +async fn request_testrun(server_addr: &str) -> anyhow::Result { + let target_url = format!("{}/{}", server_addr, URL_BASE); + let client = reqwest::Client::new(); + let res = client + .get(target_url) + .send() + .await + .and_then(|response| response.error_for_status())?; + res.json() + .await + .map(|testrun| { + tracing::info!("Received testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("err"); + err.into() + }) +} + +#[instrument(level = "debug", skip(probe_outcome))] +async fn submit_results( + server_addr: &str, + testrun_id: i64, + probe_outcome: String, +) -> anyhow::Result<()> { + let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id); + let client = reqwest::Client::new(); + let res = client + .post(target_url) + .body(probe_outcome) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results: {})", res.status()); + Ok(()) +} diff --git a/nym-node-status-agent/src/main.rs b/nym-node-status-agent/src/main.rs new file mode 100644 index 0000000000..133828b7fa --- /dev/null +++ b/nym-node-status-agent/src/main.rs @@ -0,0 +1,78 @@ +use crate::cli::Args; +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +mod cli; +mod probe; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing(); + let args = Args::parse(); + + let server_addr = format!("{}:{}", args.server_address, args.server_port); + test_ns_api_conn(&server_addr).await?; + + args.execute().await?; + + Ok(()) +} + +async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> { + reqwest::get(server_addr) + .await + .map(|res| { + tracing::info!( + "Testing connection to NS API at {server_addr}: {}", + res.status() + ); + }) + .map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err)) +} + +pub(crate) fn setup_tracing() { + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-agent/src/probe.rs b/nym-node-status-agent/src/probe.rs new file mode 100644 index 0000000000..c75900e936 --- /dev/null +++ b/nym-node-status-agent/src/probe.rs @@ -0,0 +1,54 @@ +use tracing::error; + +pub(crate) struct GwProbe { + path: String, +} + +impl GwProbe { + pub(crate) fn new(probe_path: String) -> Self { + Self { path: probe_path } + } + + pub(crate) async fn version(&self) -> String { + let mut command = tokio::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + command.arg("--version"); + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output().await { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get probe version".to_string() + } + Err(e) => { + error!("Failed to get probe version: {}", e); + "Failed to get probe version".to_string() + } + } + } + + pub(crate) fn run_and_get_log(&self, gateway_key: &Option) -> String { + let mut command = std::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + + if let Some(gateway_id) = gateway_key { + command.arg("--gateway").arg(gateway_id); + } + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output() { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get log from test run".to_string() + } + Err(e) => { + error!("Failed to spawn test: {}", e); + "Failed to spawn test run task".to_string() + } + } + } +} diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore index b2a9b208f0..91459afabe 100644 --- a/nym-node-status-api/.gitignore +++ b/nym-node-status-api/.gitignore @@ -1,2 +1,6 @@ data/ enter_db.sh +nym-gateway-probe +nym-node-status-api +*.sqlite +*.sqlite-journal diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 056271e41d..4cc9ff5d28 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -14,27 +14,31 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } -axum = { workspace = true, features = ["tokio"] } +axum = { workspace = true, features = ["tokio", "macros"] } chrono = { workspace = true } clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } cosmwasm-std = { workspace = true } envy = { workspace = true } futures-util = { workspace = true } moka = { workspace = true, features = ["future"] } -nym-bin-common = { path = "../common/bin-common" } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } nym-explorer-client = { path = "../explorer-api/explorer-client" } -# TODO dz: ref before Nym API client changes. Update to latest develop once new Nym API is live -nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } -nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +# TODO dz: before Nym API client breaking changes. Update to latest develop once new Nym API is live +nym-network-defaults = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } +nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } # nym-network-defaults = { path = "../common/network-defaults" } # nym-validator-client = { path = "../common/client-libs/validator-client" } nym-task = { path = "../common/task" } -nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +nym-node-requests = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } # nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_json_path = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/Dockerfile.dev new file mode 100644 index 0000000000..2967a6e605 --- /dev/null +++ b/nym-node-status-api/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY nym-node-status-api/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs index 394083f8be..7bc42cb03f 100644 --- a/nym-node-status-api/build.rs +++ b/nym-node-status-api/build.rs @@ -1,5 +1,7 @@ use anyhow::{anyhow, Result}; use sqlx::{Connection, SqliteConnection}; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; use tokio::{fs::File, io::AsyncWriteExt}; const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; @@ -41,6 +43,9 @@ async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Resu let mut file = File::create("enter_db.sh").await?; let _ = file.write(b"#!/bin/bash\n").await?; file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await?; + + file.set_permissions(Permissions::from_mode(0o755)) .await .map_err(From::from) } diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index 1db6bcc1b2..f9ebd364a9 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -6,13 +6,34 @@ export RUST_LOG=${RUST_LOG:-debug} export NYM_API_CLIENT_TIMEOUT=60 export EXPLORER_CLIENT_TIMEOUT=60 -#export NYXD=https://rpc.nymtech.net -#export NYM_API=https://validator.nymtech.net/api/ -#export EXPLORER_API=https://explorer.nymtech.net/api/ -#export NETWORK_NAME=mainnet -#cargo run --package nym-node-status-api --release -- --connection-url "sqlite://node-status-api.sqlite?mode=rwc" +export ENVIRONMENT="mainnet.env" -cd .. -docker build -t node-status-api -f nym-node-status-api/Dockerfile . -docker run --env-file envs/mainnet.env -e NYM_NODE_STATUS_API_CONNECTION_URL="sqlite://node-status-api.sqlite?mode=rwc" node-status-api +function run_bare() { + # export necessary env vars + set -a + source ../envs/$ENVIRONMENT + set +a + export RUST_LOG=debug + + # --conection-url is provided in build.rs + cargo run --package nym-node-status-api +} + +function run_docker() { + cargo build --package nym-node-status-api --release + cp ../target/release/nym-node-status-api . + + cd .. + docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev . + docker run --env-file envs/${ENVIRONMENT} \ + -e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \ + -e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \ + -e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \ + -e RUST_LOG=${RUST_LOG} node-status-api + +} + +run_bare + +# run_docker diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql index 35aaa40654..1e5683e2c9 100644 --- a/nym-node-status-api/migrations/000_init.sql +++ b/nym-node-status-api/migrations/000_init.sql @@ -98,3 +98,15 @@ CREATE TABLE FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index ); + + +CREATE TABLE testruns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id INTEGER, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs index ad214b6a23..84ee86577f 100644 --- a/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/src/cli/mod.rs @@ -41,26 +41,34 @@ pub(crate) struct Cli { pub(crate) nyxd_addr: Url, /// Nym api client timeout. - #[clap( - long, - default_value = "15", - env = "NYM_NODE_STATUS_API_NYM_API_CLIENT_TIMEOUT" - )] + #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] #[arg(value_parser = parse_duration)] pub(crate) nym_api_client_timeout: Duration, /// Explorer api client timeout. - #[clap( - long, - default_value = "15", - env = "NYM_NODE_STATUS_API_EXPLORER_CLIENT_TIMEOUT" - )] + #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] #[arg(value_parser = parse_duration)] pub(crate) explorer_client_timeout: Duration, /// Connection url for the database. - #[clap(long, env = "NYM_NODE_STATUS_API_CONNECTION_URL")] - pub(crate) connection_url: String, + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) monitor_refresh_interval: Duration, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) testruns_refresh_interval: Duration, } fn parse_duration(arg: &str) -> Result { diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index 8e840252f6..86e25e9227 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -1,7 +1,6 @@ -use std::str::FromStr; - use anyhow::{anyhow, Result}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +use std::str::FromStr; pub(crate) mod models; pub(crate) mod queries; @@ -30,7 +29,7 @@ impl Storage { } /// Cloning pool is cheap, it's the same underlying set of connections - pub async fn pool_owned(&self) -> DbPool { + pub fn pool_owned(&self) -> DbPool { self.pool.clone() } } diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs index 54164b34e3..f704c1ed8a 100644 --- a/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/src/db/models.rs @@ -4,6 +4,7 @@ use crate::{ }; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, FromRepr}; use utoipa::ToSchema; pub(crate) struct GatewayRecord { @@ -298,3 +299,36 @@ pub(crate) mod gateway { pub(crate) last_updated_utc: String, } } + +#[derive(Debug, Clone)] +pub struct TestRunDto { + pub id: i64, + pub gateway_id: i64, + pub status: i64, + pub timestamp_utc: i64, + pub ip_address: String, + pub log: String, +} + +#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)] +#[repr(u8)] +pub(crate) enum TestRunStatus { + Complete = 2, + InProgress = 1, + Pending = 0, +} + +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayInfoDto { + pub id: i64, + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs index 279d31dc34..349e62e7c7 100644 --- a/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -2,6 +2,7 @@ mod gateways; mod misc; mod mixnodes; mod summary; +pub(crate) mod testruns; pub(crate) use gateways::{ ensure_gateways_still_bonded, get_all_gateways, insert_gateways, diff --git a/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/src/db/queries/testruns.rs new file mode 100644 index 0000000000..cf5b8a3bbf --- /dev/null +++ b/nym-node-status-api/src/db/queries/testruns.rs @@ -0,0 +1,126 @@ +use crate::http::models::TestrunAssignment; +use crate::{ + db::models::{TestRunDto, TestRunStatus}, + testruns::now_utc, +}; +use anyhow::Context; +use sqlx::{pool::PoolConnection, Sqlite}; + +pub(crate) async fn get_testrun_by_id( + conn: &mut PoolConnection, + testrun_id: i64, +) -> anyhow::Result { + sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE id = ? + ORDER BY timestamp_utc"#, + testrun_id + ) + .fetch_one(conn.as_mut()) + .await + .context(format!("Couldn't retrieve testrun {testrun_id}")) +} + +pub(crate) async fn get_oldest_testrun_and_make_it_pending( + // TODO dz accept mut reference, repeat in all similar functions + conn: PoolConnection, +) -> anyhow::Result> { + let mut conn = conn; + let assignment = sqlx::query_as!( + TestrunAssignment, + r#"UPDATE testruns + SET status = ? + WHERE rowid = + ( + SELECT rowid + FROM testruns + WHERE status = ? + ORDER BY timestamp_utc asc + LIMIT 1 + ) + RETURNING + id as "testrun_id!", + gateway_id as "gateway_pk_id!" + "#, + TestRunStatus::InProgress as i64, + TestRunStatus::Pending as i64, + ) + .fetch_optional(&mut *conn) + .await?; + + Ok(assignment) +} + +pub(crate) async fn update_testrun_status( + conn: &mut PoolConnection, + testrun_id: i64, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as u32; + sqlx::query!( + "UPDATE testruns SET status = ? WHERE id = ?", + status, + testrun_id + ) + .execute(conn.as_mut()) + .await?; + + Ok(()) +} + +pub(crate) async fn update_gateway_last_probe_log( + conn: &mut PoolConnection, + gateway_pk: i64, + log: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + log, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_last_probe_result( + conn: &mut PoolConnection, + gateway_pk: i64, + result: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + result, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_score( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result<()> { + let now = now_utc().timestamp(); + sqlx::query!( + "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + now, + now, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs index 9dec3134e4..c1f4073767 100644 --- a/nym-node-status-api/src/http/api/gateways.rs +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -16,7 +16,7 @@ pub(crate) fn routes() -> Router { Router::new() .route("/", axum::routing::get(gateways)) .route("/skinny", axum::routing::get(gateways_skinny)) - .route("/skinny/:identity_key", axum::routing::get(get_gateway)) + .route("/:identity_key", axum::routing::get(get_gateway)) } #[utoipa::path( diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs index 4d385904b5..ed24fa80f5 100644 --- a/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/src/http/api/mod.rs @@ -35,7 +35,10 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()), - // .nest("/testruns", testruns::_routes()), + ) + .nest( + "/internal", + Router::new().nest("/testruns", testruns::routes()), ); Self { diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs index 875012b0fb..4bbc4d6295 100644 --- a/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -1,7 +1,116 @@ -use axum::Router; +use axum::Json; +use axum::{ + extract::{Path, State}, + Router, +}; +use reqwest::StatusCode; -use crate::http::state::AppState; +use crate::db::models::TestRunStatus; +use crate::db::queries; +use crate::{ + db, + http::{ + error::{HttpError, HttpResult}, + models::TestrunAssignment, + state::AppState, + }, +}; -pub(crate) fn _routes() -> Router { - unimplemented!() +// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id +// like in H< src/http/testruns.rs + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(request_testrun)) + .route("/:testrun_id", axum::routing::post(submit_testrun)) +} + +#[tracing::instrument(level = "debug", skip_all)] +async fn request_testrun(State(state): State) -> HttpResult> { + // TODO dz log agent's key + // TODO dz log agent's network probe version + tracing::debug!("Agent X requested testrun"); + + let db = state.db_pool(); + let conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(conn).await { + Ok(res) => { + if let Some(testrun) = res { + // TODO dz consider adding a column to testruns table with agent's public key + tracing::debug!( + "🏃‍ Assigned testrun row_id {} to agent X", + &testrun.testrun_id + ); + Ok(Json(testrun)) + } else { + Err(HttpError::not_found("No testruns available")) + } + } + Err(err) => Err(HttpError::internal_with_logging(err)), + }; +} + +// TODO dz accept testrun_id as query parameter +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_testrun( + Path(testrun_id): Path, + State(state): State, + body: String, +) -> HttpResult { + tracing::debug!( + "Agent submitted testrun {}. Total length: {}", + testrun_id, + body.len(), + ); + // TODO dz store testrun results + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let testrun = queries::testruns::get_testrun_by_id(&mut conn, testrun_id) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::not_found(testrun_id) + })?; + // TODO dz this should be part of a single transaction: commit after everything is done + queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body) + .await + .map_err(HttpError::internal_with_logging)?; + let result = get_result_from_log(&body); + queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + // TODO dz log gw identity key + + tracing::info!( + "✅ Testrun row_id {} for gateway {} complete", + testrun.id, + testrun.gateway_id + ); + + Ok(StatusCode::CREATED) +} + +fn get_result_from_log(log: &str) -> String { + let re = regex::Regex::new(r"\n\{\s").unwrap(); + let result: Vec<_> = re.splitn(log, 2).collect(); + if result.len() == 2 { + let res = format!("{} {}", "{", result[1]).to_string(); + return res; + } + "".to_string() } diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs index 172aa899a3..9ac5238fc4 100644 --- a/nym-node-status-api/src/http/api_docs.rs +++ b/nym-node-status-api/src/http/api_docs.rs @@ -8,7 +8,7 @@ use utoipauto::utoipauto; #[utoipauto(paths = "./nym-node-status-api/src")] #[derive(OpenApi)] #[openapi( - info(title = "Nym API"), + info(title = "Node Status API"), tags(), components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) )] diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs index 8bbd59e095..81a61e2d3e 100644 --- a/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/src/http/error.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + pub(crate) type HttpResult = Result; pub(crate) struct HttpError { @@ -13,12 +15,24 @@ impl HttpError { } } + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + pub(crate) fn internal() -> Self { Self { message: serde_json::json!({"message": "Internal server error"}).to_string(), status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, } } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::NOT_FOUND, + } + } } impl axum::response::IntoResponse for HttpError { diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs index 3a4c348b25..315bf085dc 100644 --- a/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/src/http/models.rs @@ -1,7 +1,10 @@ +use crate::db::models::TestRunDto; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +pub(crate) use nym_common_models::ns_api::TestrunAssignment; + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct Gateway { pub gateway_identity_key: String, @@ -72,3 +75,12 @@ pub(crate) struct SummaryHistory { pub value_json: serde_json::Value, pub timestamp_utc: String, } + +impl From for TestrunAssignment { + fn from(value: TestRunDto) -> Self { + Self { + gateway_pk_id: value.gateway_id, + testrun_id: value.id, + } + } +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs index 694f5fa79a..17d5d64ab0 100644 --- a/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/src/http/server.rs @@ -22,6 +22,7 @@ pub(crate) async fn start_http_api( // TODO dz do we need this to be configurable? let bind_addr = format!("0.0.0.0:{}", http_port); + tracing::info!("Binding server to {bind_addr}"); let server = router.build_server(bind_addr).await?; Ok(start_server(server)) diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs index 01dd31562e..6bdad3bae5 100644 --- a/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/src/logging.rs @@ -1,12 +1,10 @@ use tracing::level_filters::LevelFilter; use tracing_subscriber::{filter::Directive, EnvFilter}; -pub(crate) fn setup_tracing_logger() { - fn directive_checked(directive: impl Into) -> Directive { - directive - .into() - .parse() - .expect("Failed to parse log directive") +// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) } let log_builder = tracing_subscriber::fmt() @@ -22,10 +20,11 @@ pub(crate) fn setup_tracing_logger() { let mut filter = EnvFilter::builder() // if RUST_LOG isn't set, set default level - .with_default_directive(LevelFilter::INFO.into()) + .with_default_directive(LevelFilter::DEBUG.into()) .from_env_lossy(); + // these crates are more granularly filtered - let filter_crates = [ + let warn_crates = [ "reqwest", "rustls", "hyper", @@ -35,14 +34,36 @@ pub(crate) fn setup_tracing_logger() { "tower_http", "axum", ]; - for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); } - filter = filter.add_directive(directive_checked("nym_bin_common=debug")); - filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); - filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); - filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + let log_level_hint = filter.max_level_hint(); + + // debug or higher granularity (e.g. trace) + let debug_or_higher = std::cmp::max( + log_level_hint.unwrap_or(LevelFilter::DEBUG), + LevelFilter::DEBUG, + ); + filter = filter.add_directive(directive_checked(format!( + "nym_bin_common={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_explorer_client={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_network_defaults={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_validator_client={}", + debug_or_higher + ))?); log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) } diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index a15cfe68a4..b2c68b391b 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -6,26 +6,35 @@ mod db; mod http; mod logging; mod monitor; +mod testruns; #[tokio::main] async fn main() -> anyhow::Result<()> { - logging::setup_tracing_logger(); + logging::setup_tracing_logger()?; let args = cli::Cli::parse(); - let connection_url = args.connection_url.clone(); + let connection_url = args.database_url.clone(); tracing::debug!("Using config:\n{:#?}", args); let storage = db::Storage::init(connection_url).await?; - let db_pool = storage.pool_owned().await; + let db_pool = storage.pool_owned(); let args_clone = args.clone(); tokio::spawn(async move { - monitor::spawn_in_background(db_pool, args_clone).await; + monitor::spawn_in_background( + db_pool, + args_clone.explorer_client_timeout, + args_clone.nym_api_client_timeout, + &args_clone.nyxd_addr, + args_clone.monitor_refresh_interval, + ) + .await; + tracing::info!("Started monitor task"); }); - tracing::info!("Started monitor task"); + testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; let shutdown_handles = http::server::start_http_api( - storage.pool_owned().await, + storage.pool_owned(), args.http_port, args.nym_http_cache_ttl, ) diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 3c5172c8a9..680a9ce312 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,4 +1,3 @@ -use crate::cli::Cli; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, @@ -19,34 +18,50 @@ use nym_validator_client::NymApiClient; use reqwest::Url; use std::collections::HashSet; use std::str::FromStr; -use tokio::task::JoinHandle; use tokio::time::Duration; +use tracing::instrument; -const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5); -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(15); +// TODO dz should be configurable +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHandle<()> { +#[instrument(level = "debug", name = "data_monitor", skip_all)] +pub(crate) async fn spawn_in_background( + db_pool: DbPool, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, + refresh_interval: Duration, +) { let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); loop { tracing::info!("Refreshing node info..."); - if let Err(e) = run(&db_pool, &network_defaults, &config).await { + if let Err(e) = run( + &db_pool, + &network_defaults, + explorer_client_timeout, + nym_api_client_timeout, + nyxd_addr, + ) + .await + { tracing::error!( "Monitor run failed: {e}, retrying in {}s...", FAILURE_RETRY_DELAY.as_secs() ); + // TODO dz implement some sort of backoff tokio::time::sleep(FAILURE_RETRY_DELAY).await; } else { tracing::info!( "Info successfully collected, sleeping for {}s...", - REFRESH_DELAY.as_secs() + refresh_interval.as_secs() ); - tokio::time::sleep(REFRESH_DELAY).await; + tokio::time::sleep(refresh_interval).await; } } } @@ -54,7 +69,9 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHan async fn run( pool: &DbPool, network_details: &NymNetworkDetails, - config: &Cli, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, ) -> anyhow::Result<()> { let default_api_url = network_details .endpoints @@ -68,19 +85,17 @@ async fn run( .expect("rust sdk mainnet default explorer url not parseable") }); + // TODO dz replace explorer api with ipinfo.io let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); let explorer_client = - ExplorerClient::new_with_timeout(default_explorer_url, config.explorer_client_timeout)?; + ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; let explorer_gateways = explorer_client .get_gateways() .await .log_error("get_gateways")?; - tracing::debug!("6"); - let api_client = - // TODO dz introduce timeout ? - NymApiClient::new(default_api_url); + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); let gateways = api_client .get_cached_described_gateways() .await @@ -97,14 +112,16 @@ async fn run( .log_error("get_cached_mixnodes")?; tracing::debug!("Fetched {} mixnodes", mixnodes.len()); - // TODO dz can we calculate blacklisted GWs from their performance? - // where do we get their performance? - let gateways_blacklisted = api_client - .nym_api - .get_gateways_blacklisted() - .await - .map(|vec| vec.into_iter().collect::>()) - .log_error("get_gateways_blacklisted")?; + let gateways_blacklisted = skimmed_gateways + .iter() + .filter_map(|gw| { + if gw.performance.round_to_integer() <= 50 { + Some(gw.ed25519_identity_pubkey.to_owned()) + } else { + None + } + }) + .collect::>(); // Cached mixnodes don't include blacklisted nodes // We need that to calculate the total locked tokens later @@ -124,7 +141,7 @@ async fn run( .await .log_error("get_active_mixnodes")?; let delegation_program_members = - get_delegation_program_details(network_details, &config.nyxd_addr).await?; + get_delegation_program_details(network_details, nyxd_addr).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len(); diff --git a/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/src/testruns/mod.rs new file mode 100644 index 0000000000..86b36cf48e --- /dev/null +++ b/nym-node-status-api/src/testruns/mod.rs @@ -0,0 +1,76 @@ +use crate::db::models::GatewayIdentityDto; +use crate::db::DbPool; +use futures_util::TryStreamExt; +use std::time::Duration; +use tracing::instrument; + +pub(crate) mod models; +mod queue; +pub(crate) use queue::now_utc; + +pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { + tokio::spawn(async move { + loop { + tracing::info!("Spawning testruns..."); + + if let Err(e) = run(&pool).await { + tracing::error!("Cron job failed: {}", e); + } + tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs()); + tokio::time::sleep(refresh_interval).await; + } + }); +} + +// TODO dz make number of max agents configurable + +// TODO dz periodically clean up stale pending testruns +#[instrument(level = "debug", name = "testrun_queue", skip_all)] +async fn run(pool: &DbPool) -> anyhow::Result<()> { + if pool.is_closed() { + tracing::debug!("DB pool closed, returning early"); + return Ok(()); + } + + let mut conn = pool.acquire().await?; + + let gateways = sqlx::query_as!( + GatewayIdentityDto, + r#"SELECT + gateway_identity_key as "gateway_identity_key!", + bonded as "bonded: bool" + FROM gateways + ORDER BY last_testrun_utc"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + // TODO dz this filtering could be done in SQL + let gateways: Vec = gateways.into_iter().filter(|g| g.bonded).collect(); + + tracing::debug!("Trying to queue {} testruns", gateways.len()); + let mut testruns_created = 0; + for gateway in gateways { + if let Err(e) = queue::try_queue_testrun( + &mut conn, + gateway.gateway_identity_key.clone(), + // TODO dz read from config + "127.0.0.1".to_string(), + ) + .await + // TODO dz measure how many were actually inserted and how many were skipped + { + tracing::debug!( + "Skipping test for identity {} with error {}", + &gateway.gateway_identity_key, + e + ); + } else { + testruns_created += 1; + } + } + tracing::debug!("{} testruns queued in total", testruns_created); + + Ok(()) +} diff --git a/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/src/testruns/models.rs new file mode 100644 index 0000000000..fe4b33384c --- /dev/null +++ b/nym-node-status-api/src/testruns/models.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestRun { + pub id: u32, + pub identity_key: String, + pub status: String, + pub log: String, +} diff --git a/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/src/testruns/queue.rs new file mode 100644 index 0000000000..aa0ce32fa2 --- /dev/null +++ b/nym-node-status-api/src/testruns/queue.rs @@ -0,0 +1,118 @@ +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::testruns::models::TestRun; +use anyhow::anyhow; +use chrono::DateTime; +use futures_util::TryStreamExt; +use sqlx::pool::PoolConnection; +use sqlx::Sqlite; +use std::time::SystemTime; + +pub(crate) async fn try_queue_testrun( + conn: &mut PoolConnection, + identity_key: String, + ip_address: String, +) -> anyhow::Result { + let timestamp = now_utc().timestamp(); + let timestamp_pretty = now_utc_as_rfc3339(); + + let items = sqlx::query_as!( + GatewayInfoDto, + r#"SELECT + id as "id!", + gateway_identity_key as "gateway_identity_key!", + self_described as "self_described?", + explorer_pretty_bond as "explorer_pretty_bond?" + FROM gateways + WHERE gateway_identity_key = ? + ORDER BY gateway_identity_key + LIMIT 1"#, + identity_key, + ) + // TODO dz shoudl call .fetch_one + // TODO dz replace this in other queries as well + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + let gateway = items + .iter() + .find(|g| g.gateway_identity_key == identity_key); + + // TODO dz if let Some() = gateway.first() ... + if gateway.is_none() { + return Err(anyhow!("Unknown gateway {identity_key}")); + } + let gateway_id = gateway.unwrap().id; + + // + // check if there is already a test run for this gateway + // + let items = sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE gateway_id = ? AND status != 2 + ORDER BY id DESC + LIMIT 1"#, + gateway_id, + ) + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + if !items.is_empty() { + let testrun = items.first().unwrap(); + return Ok(TestRun { + id: testrun.id as u32, + identity_key, + status: format!( + "{}", + TestRunStatus::from_repr(testrun.status as u8).unwrap() + ), + log: testrun.log.clone(), + }); + } + + // + // save test run + // + let status = TestRunStatus::Pending as u32; + let log = format!( + "Test for {identity_key} requested at {} UTC\n\n", + timestamp_pretty + ); + + let id = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + gateway_id, + status, + ip_address, + timestamp, + log, + ) + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + Ok(TestRun { + id: id as u32, + identity_key, + status: format!("{}", TestRunStatus::Pending), + log, + }) +} + +// TODO dz do we need these? +pub fn now_utc() -> DateTime { + SystemTime::now().into() +} + +pub fn now_utc_as_rfc3339() -> String { + now_utc().to_rfc3339() +}