From a0fea6edb42eaa8b24ac55f0cb2e39415d478e97 Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:31:01 +0200 Subject: [PATCH] Add `axum` server to `nym-api` (#4803) * Migrate nym-api HTTP server from rocket to axum (#4698) Migrate endpoints to Axum * Squashed after PR review Initial WIP - bootstrap axum server with same data as rocket - start axum server alongside rocket - add routes for circulating-supply, contract-cache, network - write simple bash validation that migrated APIs return 200 - mark rocket parts of code as deprecated - start more complicated routes: WIP Init storage always Add coconut routes Add api-status routes Expand tests WIP Migrate unstable APIs with query params Update bash tests Add node-status routes Redirect / to /swagger Update API tests Implement graceful shutdown rustfmt Fix clippy * Add ecash routes after rebase * PR feedback - add CORS layer - move logger to common crate - remove global log filters for nym-api and axum * Serve OpenAPI for all endpoints (#4761) * Playing around with swagger * Generate OpenAPI for /status routes * Phase out static_routes as strings - also nest routers in a clearer way * Generate OpenAPI for /network routes * Generate OpenAPI for /api-status routes * Generate OpenAPI for "nym nodes" routes * Fix some network-monitor routes * Generate OpenAPI for /ecash routes * Add utoipa feature to /common mods * Add OpenAPI for unstable routes * Fix MixNodeDetails field in models * Introduce axum feature flag (#4775) * Add Axum bind_address to config * Introduce axum feature flag * Add comment to template.rs * Add Github action to build wtih `axum` feature * Refactor server start & shutdown (#4777) * Clippy: don't forget axum feature * Refactor router so it's safer * Implement graceful shutdown * Nicer pattern matching * Better Result syntax --- .github/workflows/ci-build.yml | 10 +- Cargo.lock | 316 +++++++++----- Cargo.toml | 6 +- common/config/Cargo.toml | 3 +- .../mixnet-contract/Cargo.toml | 2 + .../mixnet-contract/src/gateway.rs | 4 + .../mixnet-contract/src/interval.rs | 4 + common/http-api-common/Cargo.toml | 7 +- common/http-api-common/src/lib.rs | 12 +- common/http-api-common/src/logging.rs | 63 +++ common/network-defaults/Cargo.toml | 4 +- common/network-defaults/src/network.rs | 5 + nym-api/Cargo.toml | 27 +- nym-api/nym-api-requests/Cargo.toml | 3 +- nym-api/nym-api-requests/src/ecash/models.rs | 31 +- nym-api/nym-api-requests/src/models.rs | 124 ++++-- nym-api/nym-api-requests/src/nym_nodes.rs | 16 +- nym-api/nym-api-requests/src/pagination.rs | 5 +- .../src/circulating_supply_api/cache/mod.rs | 3 +- .../src/circulating_supply_api/handlers.rs | 102 +++++ nym-api/src/circulating_supply_api/mod.rs | 2 + nym-api/src/circulating_supply_api/routes.rs | 14 +- .../src/ecash/api_routes/aggregation_axum.rs | 143 +++++++ nym-api/src/ecash/api_routes/handlers.rs | 19 + nym-api/src/ecash/api_routes/helpers.rs | 7 + nym-api/src/ecash/api_routes/issued_axum.rs | 147 +++++++ nym-api/src/ecash/api_routes/mod.rs | 10 + .../ecash/api_routes/partial_signing_axum.rs | 178 ++++++++ nym-api/src/ecash/api_routes/spending_axum.rs | 245 +++++++++++ nym-api/src/epoch_operations/mod.rs | 9 +- nym-api/src/main.rs | 24 +- nym-api/src/network/handlers.rs | 100 +++++ nym-api/src/network/mod.rs | 2 + nym-api/src/network/models.rs | 2 + nym-api/src/node_status_api/cache/mod.rs | 3 +- .../src/node_status_api/cache/node_sets.rs | 42 +- .../src/node_status_api/cache/refresher.rs | 4 +- nym-api/src/node_status_api/handlers/mod.rs | 32 ++ .../handlers/network_monitor.rs | 339 +++++++++++++++ .../src/node_status_api/handlers/unstable.rs | 279 ++++++++++++ .../handlers/without_monitor.rs | 176 ++++++++ nym-api/src/node_status_api/helpers.rs | 104 ++--- .../src/node_status_api/helpers_deprecated.rs | 405 ++++++++++++++++++ nym-api/src/node_status_api/mod.rs | 72 ++-- nym-api/src/node_status_api/models.rs | 110 ++++- .../{routes.rs => routes_deprecated.rs} | 62 +-- nym-api/src/node_status_api/uptime_updater.rs | 4 +- nym-api/src/nym_contract_cache/cache/mod.rs | 3 +- nym-api/src/nym_contract_cache/handlers.rs | 270 ++++++++++++ nym-api/src/nym_contract_cache/mod.rs | 2 + nym-api/src/nym_contract_cache/routes.rs | 4 +- nym-api/src/nym_nodes/handlers.rs | 95 ++++ nym-api/src/nym_nodes/handlers_unstable.rs | 377 ++++++++++++++++ nym-api/src/nym_nodes/mod.rs | 7 +- nym-api/src/nym_nodes/unstable_routes.rs | 59 ++- nym-api/src/status/handlers.rs | 92 ++++ nym-api/src/status/mod.rs | 2 + nym-api/src/status/routes.rs | 6 +- nym-api/src/support/cli/run.rs | 44 +- nym-api/src/support/config/mod.rs | 18 + nym-api/src/support/config/template.rs | 6 +- nym-api/src/support/http/mod.rs | 39 +- nym-api/src/support/storage/mod.rs | 1 + nym-api/src/v2/api_docs.rs | 85 ++++ nym-api/src/v2/mod.rs | 313 ++++++++++++++ nym-api/src/v2/router.rs | 136 ++++++ nym-node/tests/definitions.sh | 217 ++++++++++ nym-node/tests/test_apis.sh | 24 ++ 68 files changed, 4653 insertions(+), 428 deletions(-) create mode 100644 common/http-api-common/src/logging.rs create mode 100644 nym-api/src/circulating_supply_api/handlers.rs create mode 100644 nym-api/src/ecash/api_routes/aggregation_axum.rs create mode 100644 nym-api/src/ecash/api_routes/handlers.rs create mode 100644 nym-api/src/ecash/api_routes/issued_axum.rs create mode 100644 nym-api/src/ecash/api_routes/partial_signing_axum.rs create mode 100644 nym-api/src/ecash/api_routes/spending_axum.rs create mode 100644 nym-api/src/network/handlers.rs create mode 100644 nym-api/src/node_status_api/handlers/mod.rs create mode 100644 nym-api/src/node_status_api/handlers/network_monitor.rs create mode 100644 nym-api/src/node_status_api/handlers/unstable.rs create mode 100644 nym-api/src/node_status_api/handlers/without_monitor.rs create mode 100644 nym-api/src/node_status_api/helpers_deprecated.rs rename nym-api/src/node_status_api/{routes.rs => routes_deprecated.rs} (91%) create mode 100644 nym-api/src/nym_contract_cache/handlers.rs create mode 100644 nym-api/src/nym_nodes/handlers.rs create mode 100644 nym-api/src/nym_nodes/handlers_unstable.rs create mode 100644 nym-api/src/status/handlers.rs create mode 100644 nym-api/src/v2/api_docs.rs create mode 100644 nym-api/src/v2/mod.rs create mode 100644 nym-api/src/v2/router.rs create mode 100755 nym-node/tests/definitions.sh create mode 100755 nym-node/tests/test_apis.sh diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 710b1e0d95..2c950f902a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -76,6 +76,14 @@ jobs: # Enable wireguard by default on linux only args: --workspace --features wireguard + # while disabled by default, this build ensures nothing is broken within + # `axum` feature + - name: Build with `axum` feature + uses: actions-rs/cargo@v1 + with: + command: build + args: --features axum + - name: Build all examples if: matrix.os == 'custom-linux' uses: actions-rs/cargo@v1 @@ -109,4 +117,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --all-targets --features wireguard -- -D warnings + args: --workspace --all-targets --features wireguard,axum -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index 7f52622e81..35cc541a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -218,6 +218,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "argon2" version = "0.5.3" @@ -394,7 +403,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.4.0", "hyper-util", "itoa", "matchit", @@ -548,7 +557,7 @@ dependencies = [ "rand_core 0.6.4", "ripemd", "sha2 0.10.8", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -586,9 +595,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" @@ -687,7 +696,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serdect 0.3.0-pre.0", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -737,9 +746,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -882,7 +891,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1509,7 +1518,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossterm_winapi", "libc", "parking_lot 0.12.3", @@ -1539,7 +1548,7 @@ checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -1621,9 +1630,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.72+curl-8.6.0" +version = "0.4.73+curl-8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" +checksum = "450ab250ecf17227c39afb9a2dd9261dc0035cb80f2612472fc0c4aac2dcb84d" dependencies = [ "cc", "libc", @@ -1643,7 +1652,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -1660,7 +1669,7 @@ dependencies = [ "fiat-crypto", "rustc_version 0.4.0", "serde", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -1938,6 +1947,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "devise" version = "0.4.1" @@ -1964,7 +1984,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "proc-macro2", "proc-macro2-diagnostics", "quote", @@ -1998,7 +2018,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -2042,6 +2062,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "dkg-bypass-contract" version = "0.1.0" @@ -2122,7 +2153,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2 0.10.8", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -2163,7 +2194,7 @@ dependencies = [ "rand_core 0.6.4", "sec1", "serdect 0.2.0", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -2360,7 +2391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -2744,7 +2775,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -3071,9 +3102,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" dependencies = [ "bytes", "futures-channel", @@ -3111,7 +3142,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.4.0", "hyper-util", "rustls 0.22.4", "rustls-pki-types", @@ -3143,7 +3174,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.4.1", + "hyper 1.4.0", "pin-project-lite", "socket2", "tokio", @@ -3519,9 +3550,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "ledger-apdu" @@ -3578,7 +3609,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -3746,9 +3777,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -3767,9 +3798,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -3957,7 +3988,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "libc", "memoffset", @@ -3969,7 +4000,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -4065,6 +4096,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -4086,6 +4138,8 @@ version = "1.1.42" dependencies = [ "anyhow", "async-trait", + "axum 0.7.5", + "axum-extra", "bincode", "bip39", "bloomfilter", @@ -4122,6 +4176,7 @@ dependencies = [ "nym-ecash-double-spending", "nym-ecash-time", "nym-gateway-client", + "nym-http-api-common", "nym-inclusion-probability", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -4153,8 +4208,15 @@ dependencies = [ "time", "tokio", "tokio-stream", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", "ts-rs", "url", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", "zeroize", ] @@ -4183,6 +4245,7 @@ dependencies = [ "thiserror", "time", "ts-rs", + "utoipa", ] [[package]] @@ -4432,7 +4495,7 @@ dependencies = [ "gloo-timers", "http-body-util", "humantime-serde", - "hyper 1.4.1", + "hyper 1.4.0", "hyper-util", "log", "nym-bandwidth-controller", @@ -5034,10 +5097,12 @@ version = "0.1.0" dependencies = [ "axum 0.7.5", "bytes", + "colored", "mime", "serde", "serde_json", "serde_yaml", + "tracing", "utoipa", ] @@ -5187,6 +5252,7 @@ dependencies = [ "thiserror", "time", "ts-rs", + "utoipa", ] [[package]] @@ -5288,6 +5354,7 @@ dependencies = [ "schemars", "serde", "url", + "utoipa", ] [[package]] @@ -5427,7 +5494,7 @@ dependencies = [ "fastrand 2.1.1", "headers", "hmac", - "hyper 1.4.1", + "hyper 1.4.0", "ipnetwork 0.20.0", "nym-crypto", "nym-http-api-common", @@ -6258,9 +6325,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "opaque-debug" @@ -6467,9 +6534,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6480,7 +6547,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -6558,9 +6625,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", "thiserror", @@ -6569,9 +6636,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" dependencies = [ "pest", "pest_generator", @@ -6579,9 +6646,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" dependencies = [ "pest", "pest_meta", @@ -6592,9 +6659,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" dependencies = [ "once_cell", "pest", @@ -7175,11 +7242,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -7306,12 +7373,13 @@ checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.4.0", "hyper-rustls 0.26.0", "hyper-util", "ipnet", @@ -7349,7 +7417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -7582,7 +7650,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -7623,7 +7691,7 @@ dependencies = [ "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.4", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -7817,7 +7885,7 @@ dependencies = [ "generic-array 0.14.7", "pkcs8", "serdect 0.2.0", - "subtle 2.5.0", + "subtle 2.6.1", "zeroize", ] @@ -7827,7 +7895,7 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -8273,7 +8341,7 @@ dependencies = [ "rand 0.8.5", "rand_distr", "sha2 0.10.8", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -8574,9 +8642,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "subtle-encoding" @@ -8716,7 +8784,7 @@ dependencies = [ "serde_repr", "sha2 0.10.8", "signature", - "subtle 2.5.0", + "subtle 2.6.1", "subtle-encoding", "tendermint-proto 0.34.1", "time", @@ -8747,7 +8815,7 @@ dependencies = [ "serde_repr", "sha2 0.10.8", "signature", - "subtle 2.5.0", + "subtle 2.6.1", "subtle-encoding", "tendermint-proto 0.37.0", "time", @@ -8822,7 +8890,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "subtle 2.5.0", + "subtle 2.6.1", "subtle-encoding", "tendermint 0.37.0", "tendermint-config", @@ -8988,9 +9056,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" dependencies = [ "tinyvec_macros", ] @@ -9286,7 +9354,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-util", "http 1.1.0", @@ -9684,7 +9752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle 2.5.0", + "subtle 2.6.1", ] [[package]] @@ -9768,20 +9836,54 @@ dependencies = [ [[package]] name = "utoipa-swagger-ui" -version = "6.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da" +checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e" dependencies = [ "axum 0.7.5", "mime_guess", "regex", + "reqwest 0.12.4", "rust-embed", "serde", "serde_json", + "url", "utoipa", "zip", ] +[[package]] +name = "utoipauto" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608b8f2279483be386261655b562e40877ea434eb92093c894a644fda2021860" +dependencies = [ + "utoipauto-macro", +] + +[[package]] +name = "utoipauto-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e82ab96c5a55263b5bed151b8426410d93aa909a453acdbd4b6792b5af7d64" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "utoipauto-macro" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8338dc3c9526011ffaa2aa6bd60ddfda9d49d2123108690755c6e34844212" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "utoipauto-core", +] + [[package]] name = "uuid" version = "1.8.0" @@ -10140,7 +10242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10149,7 +10251,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10176,7 +10278,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -10211,18 +10313,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -10239,9 +10341,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -10257,9 +10359,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -10275,15 +10377,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -10299,9 +10401,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -10317,9 +10419,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -10335,9 +10437,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -10353,9 +10455,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -10458,18 +10560,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", @@ -10498,14 +10600,18 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.6" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "crossbeam-utils", + "displaydoc", "flate2", + "indexmap 2.2.6", + "num_enum", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4d5975a380..15352251d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -311,8 +311,10 @@ tracing-tree = "0.2.2" ts-rs = "7.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" -utoipa = "4.2.0" -utoipa-swagger-ui = "6.0.0" +utoipa = "4.2" +utoipa-rapidoc = "4.0" +utoipa-swagger-ui = "7.1" +utoipauto = "0.1" uuid = "*" vergen = { version = "=8.3.1", default-features = false } walkdir = "2" diff --git a/common/config/Cargo.toml b/common/config/Cargo.toml index 5ea13a45ce..6ff3efc432 100644 --- a/common/config/Cargo.toml +++ b/common/config/Cargo.toml @@ -15,7 +15,8 @@ serde = { workspace = true, features = ["derive"] } toml = "0.7.4" url = { workspace = true } -nym-network-defaults = { path = "../network-defaults" } +nym-network-defaults = { path = "../network-defaults", features = ["utoipa"] } [features] default = ["dirs"] +utoipa = [ "nym-network-defaults/utoipa" ] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 31e422a49c..86afc9d0c8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -22,6 +22,7 @@ thiserror = { workspace = true } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } serde-json-wasm = { workspace = true } humantime-serde = { workspace = true } +utoipa = { workspace = true, optional = true } # TO CHECK WHETHER STILL NEEDED: log = { workspace = true } @@ -35,5 +36,6 @@ time = { workspace = true, features = ["serde", "macros"] } [features] default = [] contract-testing = [] +utoipa = [ "dep:utoipa" ] schema = ["cw2"] generate-ts = ['ts-rs'] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index ff991853fb..9d75b66c22 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -10,6 +10,7 @@ use std::fmt::Display; /// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. #[cw_serde] #[derive(PartialOrd)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct Gateway { /// Network address of this gateway, for example 1.1.1.1 or foo.gateway.com pub host: String, @@ -25,9 +26,11 @@ pub struct Gateway { pub location: String, /// Base58-encoded x25519 public key used for sphinx key derivation. + #[cfg_attr(feature = "utoipa", schema(value_type = String))] pub sphinx_key: SphinxKey, /// Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients + #[cfg_attr(feature = "utoipa", schema(value_type = String))] pub identity_key: IdentityKey, /// The self-reported semver version of this gateway. @@ -36,6 +39,7 @@ pub struct Gateway { /// Basic gateway information provided by the node operator. #[cw_serde] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct GatewayBond { /// Original amount pledged by the operator of this node. pub pledge_amount: Coin, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 9301d92d29..d5382a29e6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -209,8 +209,10 @@ impl Display for EpochState { ts(export_to = "ts-packages/types/src/types/rust/Interval.ts") )] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct Interval { /// Monotonously increasing id of this interval. + #[cfg_attr(feature = "utoipa", schema(value_type = u32))] id: IntervalId, /// Number of epochs in this interval. @@ -226,6 +228,7 @@ pub struct Interval { current_epoch_start: OffsetDateTime, /// Monotonously increasing id of the current epoch in this interval. + #[cfg_attr(feature = "utoipa", schema(value_type = u32))] current_epoch_id: EpochId, /// The duration of all epochs in this interval. @@ -233,6 +236,7 @@ pub struct Interval { epoch_length: Duration, /// The total amount of elapsed epochs since the first epoch of the first interval. + #[cfg_attr(feature = "utoipa", schema(value_type = u32))] total_elapsed_epochs: EpochId, } diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index 0bd7767a05..b9252c2b05 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -13,8 +13,13 @@ license.workspace = true [dependencies] axum.workspace = true bytes = { workspace = true } +colored.workspace = true mime = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml = { workspace = true } -utoipa = { workspace = true } +tracing.workspace = true +utoipa = { workspace = true, optional = true } + +[features] +utoipa = ["dep:utoipa"] diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index 83b9c685d8..f84e6e2ecc 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -6,9 +6,11 @@ use axum::response::{IntoResponse, Response}; use axum::Json; use bytes::{BufMut, BytesMut}; use serde::{Deserialize, Serialize}; -use utoipa::{IntoParams, ToSchema}; -#[derive(Debug, Clone, ToSchema)] +pub mod logging; + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub enum FormattedResponse { Json(Json), Yaml(Yaml), @@ -26,7 +28,8 @@ where } } -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, ToSchema)] +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "lowercase")] pub enum Output { #[default] @@ -34,7 +37,8 @@ pub enum Output { Yaml, } -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))] #[serde(default)] pub struct OutputParams { pub output: Option, diff --git a/common/http-api-common/src/logging.rs b/common/http-api-common/src/logging.rs new file mode 100644 index 0000000000..ca2f5c63ce --- /dev/null +++ b/common/http-api-common/src/logging.rs @@ -0,0 +1,63 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use axum::extract::{ConnectInfo, Request}; +use axum::http::header::{HOST, USER_AGENT}; +use axum::http::HeaderValue; +use axum::middleware::Next; +use axum::response::IntoResponse; +use colored::Colorize; +use std::net::SocketAddr; +use std::time::Instant; +use tracing::info; + +pub async fn logger( + ConnectInfo(addr): ConnectInfo, + request: Request, + next: Next, +) -> impl IntoResponse { + // TODO dz use `OriginalUri` extractor to get full URI even for nested + // routers if routes aren't logged correctly in handlers + fn header_map(header: Option<&HeaderValue>, msg: String) -> String { + header + .map(|x| x.to_str().unwrap_or(&msg).to_string()) + .unwrap_or(msg) + } + + let method = request.method().to_string().green(); + let uri = request.uri().to_string().blue(); + let agent = header_map( + request.headers().get(USER_AGENT), + "Unknown User Agent".to_string(), + ); + + let host = header_map(request.headers().get(HOST), "Unknown Host".to_string()); + + let start = Instant::now(); + // run request through all middleware, incl. extractors + let res = next.run(request).await; + let time_taken = start.elapsed(); + let status = res.status(); + let print_status = if status.is_client_error() || status.is_server_error() { + status.to_string().red() + } else if status.is_success() { + status.to_string().green() + } else { + status.to_string().yellow() + }; + + let taken = "time taken".bold(); + + let time_taken = match time_taken.as_millis() { + ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), + ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), + ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), + ms => format!("{taken}: {ms}ms"), + }; + + let agent_str = "agent".bold(); + + info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); + + res +} diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 03d09cf159..2cd2d4c443 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -13,6 +13,7 @@ log = { workspace = true, optional = true } schemars = { workspace = true, features = ["preserve_order"], optional = true } serde = { workspace = true, features = ["derive"], optional = true } url = { workspace = true, optional = true } +utoipa = { workspace = true, optional = true } # please be extremely careful when adding new dependencies because this crate is imported by the ecash contract, # so if anything new is added, consider feature-locking it and then just adding it to default feature @@ -20,4 +21,5 @@ url = { workspace = true, optional = true } [features] default = ["env", "network"] env = ["dotenvy", "log"] -network = ["schemars", "serde", "url"] \ No newline at end of file +network = ["schemars", "serde", "url"] +utoipa = [ "dep:utoipa" ] diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 9dbb5d9bc2..591f609ed9 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -8,6 +8,7 @@ use std::ops::Not; use url::Url; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ChainDetails { pub bech32_account_prefix: String, pub mix_denom: DenomDetailsOwned, @@ -15,6 +16,7 @@ pub struct ChainDetails { } #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, @@ -27,6 +29,7 @@ pub struct NymContracts { // I wanted to use the simpler `NetworkDetails` name, but there's a clash // with `NetworkDetails` defined in all.rs... #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct NymNetworkDetails { pub network_name: String, pub chain_details: ChainDetails, @@ -293,6 +296,7 @@ impl DenomDetails { } #[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct DenomDetailsOwned { pub base: String, pub display: String, @@ -321,6 +325,7 @@ impl DenomDetailsOwned { } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ValidatorDetails { // it is assumed those values are always valid since they're being provided in our defaults file pub nyxd_url: String, diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index d6906d0ce8..f776ca226f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -50,6 +50,7 @@ tokio = { workspace = true, features = [ "time", ] } tokio-stream = { workspace = true } +tokio-util = { workspace = true } url = { workspace = true } ts-rs = { workspace = true, optional = true } @@ -70,6 +71,16 @@ rocket_okapi = { workspace = true, features = ["swagger"] } schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } +# for axum server +axum = { workspace = true, features = ["tokio"], optional = true } +axum-extra = { workspace = true, features = ["typed-header"], optional = true } +tower-http = { workspace = true, features = ["cors", "trace"], optional = true } +utoipa = { workspace = true, features = ["axum_extras", "time"], optional = true } +utoipa-swagger-ui = { workspace = true, features = ["axum"], optional = true} +utoipauto = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } +tracing = { workspace = true, optional = true } + ## ephemera-specific #actix-web = "4" #array-bytes = "6.0.0" @@ -101,7 +112,7 @@ cw4 = { workspace = true } nym-dkg = { path = "../common/dkg", features = ["cw-types"] } nym-gateway-client = { path = "../common/client-libs/gateway-client" } nym-inclusion-probability = { path = "../common/inclusion-probability" } -nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"]} nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } @@ -112,14 +123,26 @@ nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } nym-validator-client = { path = "../common/client-libs/validator-client" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } +nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } [features] no-reward = [] generate-ts = ["ts-rs"] +axum = ["dep:axum", + "axum-extra", + "tower-http", + "utoipa", + "utoipauto", + "tracing-subscriber", + "tracing", + "utoipa-swagger-ui", + "nym-http-api-common/utoipa", + "nym-mixnet-contract-common/utoipa" +] [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index a4730db70e..ca647160e7 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -18,6 +18,7 @@ tendermint = { workspace = true } time = { workspace = true, features = ["serde", "parsing", "formatting"] } thiserror.workspace = true rocket = { workspace = true, optional = true } +utoipa.workspace = true sha2 = "0.10.8" @@ -31,7 +32,7 @@ nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"] nym-ecash-time = { path = "../../common/ecash-time" } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } -nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } +nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } [dev-dependencies] diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index bb3453463c..64c1a2dccb 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -20,6 +20,7 @@ use std::collections::BTreeMap; use std::ops::Deref; use thiserror::Error; use time::Date; +use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, JsonSchema)] pub struct VerifyEcashTicketBody { @@ -60,7 +61,7 @@ impl VerifyEcashCredentialBody { } } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct EcashTicketVerificationResponse { pub verified: Result<(), EcashTicketVerificationRejection>, } @@ -73,7 +74,7 @@ impl EcashTicketVerificationResponse { } } -#[derive(Debug, Error, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Error, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum EcashTicketVerificationRejection { #[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC")] InvalidSpentDate { @@ -155,7 +156,7 @@ impl BlindSignRequestBody { } } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct BlindedSignatureResponse { #[schemars(with = "PlaceholderJsonSchemaImpl")] pub blinded_signature: BlindedSignature, @@ -198,7 +199,7 @@ impl MasterVerificationKeyResponse { } } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct VerificationKeyResponse { #[schemars(with = "PlaceholderJsonSchemaImpl")] pub key: VerificationKeyAuth, @@ -222,25 +223,26 @@ impl CosmosAddressResponse { } } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct PartialExpirationDateSignatureResponse { pub epoch_id: u64, #[schemars(with = "String")] #[serde(with = "crate::helpers::date_serde")] + #[schema(value_type = String)] pub expiration_date: Date, #[schemars(with = "PlaceholderJsonSchemaImpl")] pub signatures: Vec, } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct PartialCoinIndicesSignatureResponse { pub epoch_id: u64, #[schemars(with = "PlaceholderJsonSchemaImpl")] pub signatures: Vec, } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct AggregatedExpirationDateSignatureResponse { pub epoch_id: u64, @@ -252,7 +254,7 @@ pub struct AggregatedExpirationDateSignatureResponse { pub signatures: Vec, } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct AggregatedCoinIndicesSignatureResponse { pub epoch_id: u64, #[schemars(with = "PlaceholderJsonSchemaImpl")] @@ -350,16 +352,17 @@ impl BatchRedeemTicketsBody { } } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct EcashBatchTicketRedemptionResponse { pub proposal_accepted: bool, } -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SpentCredentialsResponse { #[serde(with = "nym_serde_helpers::base64")] #[schemars(with = "String")] + #[schema(value_type = String)] pub bitmap: Vec, } @@ -369,7 +372,7 @@ impl SpentCredentialsResponse { } } -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct EpochCredentialsResponse { pub epoch_id: u64, @@ -377,20 +380,20 @@ pub struct EpochCredentialsResponse { pub total_issued: u32, } -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialsResponse { // note: BTreeMap returns ordered results so it's fine to use it with pagination pub credentials: BTreeMap, } -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialResponse { pub credential: Option, } -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedTicketbookBody { pub credential: IssuedTicketbook, diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 8245d26d70..5889a33b83 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1,10 +1,10 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::helpers::unix_epoch; use crate::nym_nodes::NodeRole; use crate::pagination::PaginatedResponse; -use cosmwasm_std::{Addr, Coin, Decimal}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; @@ -22,6 +22,7 @@ use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; use time::OffsetDateTime; +use utoipa::{IntoParams, ToResponse, ToSchema}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct RequestError { @@ -38,6 +39,12 @@ impl RequestError { pub fn message(&self) -> &str { &self.message } + + pub fn empty() -> Self { + Self { + message: String::new(), + } + } } impl Display for RequestError { @@ -77,7 +84,7 @@ impl MixnodeStatus { } } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -88,7 +95,7 @@ pub struct MixnodeCoreStatusResponse { pub count: i32, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -99,7 +106,7 @@ pub struct GatewayCoreStatusResponse { pub count: i32, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -109,19 +116,39 @@ pub struct MixnodeStatusResponse { pub status: MixnodeStatus, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct NodePerformance { + #[schema(value_type = String)] pub most_recent: Performance, + #[schema(value_type = String)] pub last_hour: Performance, + #[schema(value_type = String)] pub last_24h: Performance, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(ToSchema)] +#[schema(title = "MixNodeDetails")] +pub struct MixNodeDetailsSchema { + /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + pub bond_information: String, + + /// Details used for computation of rewarding related data. + pub rewarding_details: String, + + /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + pub pending_changes: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct MixNodeBondAnnotated { + #[schema(value_type = MixNodeDetailsSchema)] pub mixnode_details: MixNodeDetails, + #[schema(value_type = String)] pub stake_saturation: StakeSaturation, + #[schema(value_type = String)] pub uncapped_stake_saturation: StakeSaturation, // NOTE: the performance field is deprecated in favour of node_performance + #[schema(value_type = String)] pub performance: Performance, pub node_performance: NodePerformance, pub estimated_operator_apy: Decimal, @@ -152,7 +179,7 @@ impl MixNodeBondAnnotated { } } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct GatewayBondAnnotated { pub gateway_bond: GatewayBond, @@ -183,13 +210,16 @@ pub struct GatewayDescription { // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)] pub struct ComputeRewardEstParam { + #[schema(value_type = String)] pub performance: Option, pub active_in_rewarded_set: Option, pub pledge_amount: Option, pub total_delegation: Option, + #[schema(value_type = CoinSchema)] pub interval_operating_cost: Option, + #[schema(value_type = String)] pub profit_margin_percent: Option, } @@ -198,7 +228,7 @@ pub struct ComputeRewardEstParam { feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts") )] -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct RewardEstimationResponse { pub estimation: RewardEstimate, pub reward_params: RewardingParams, @@ -207,15 +237,16 @@ pub struct RewardEstimationResponse { pub as_at: i64, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct UptimeResponse { + #[schema(value_type = u32)] pub mix_id: MixId, // The same as node_performance.last_24h. Legacy pub avg_uptime: u8, pub node_performance: NodePerformance, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct GatewayUptimeResponse { pub identity: String, // The same as node_performance.last_24h. Legacy @@ -223,7 +254,7 @@ pub struct GatewayUptimeResponse { pub node_performance: NodePerformance, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -231,16 +262,18 @@ pub struct GatewayUptimeResponse { )] pub struct StakeSaturationResponse { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + #[schema(value_type = String)] pub saturation: StakeSaturation, #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + #[schema(value_type = String)] pub uncapped_saturation: StakeSaturation, pub as_at: i64, } pub type StakeSaturation = Decimal; -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -282,7 +315,7 @@ impl fmt::Display for SelectionChance { } } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -303,7 +336,7 @@ impl fmt::Display for InclusionProbabilityResponse { } } -#[derive(Clone, Serialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] pub struct AllInclusionProbabilitiesResponse { pub inclusion_probabilities: Vec, pub samples: u64, @@ -313,8 +346,9 @@ pub struct AllInclusionProbabilitiesResponse { pub as_at: i64, } -#[derive(Clone, Serialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] pub struct InclusionProbability { + #[schema(value_type = u32)] pub mix_id: MixId, pub in_active: f64, pub in_reserve: f64, @@ -322,7 +356,7 @@ pub struct InclusionProbability { type Uptime = u8; -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeStatusReportResponse { pub mix_id: MixId, pub identity: IdentityKey, @@ -332,22 +366,26 @@ pub struct MixnodeStatusReportResponse { pub last_day: Uptime, } -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayStatusReportResponse { pub identity: String, pub owner: String, + #[schema(value_type = u8)] pub most_recent: Uptime, + #[schema(value_type = u8)] pub last_hour: Uptime, + #[schema(value_type = u8)] pub last_day: Uptime, } -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HistoricalUptimeResponse { pub date: String, + #[schema(value_type = u8)] pub uptime: Uptime, } -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeUptimeHistoryResponse { pub mix_id: MixId, pub identity: String, @@ -355,22 +393,34 @@ pub struct MixnodeUptimeHistoryResponse { pub history: Vec, } -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, pub owner: String, pub history: Vec, } -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(ToSchema)] +#[schema(title = "Coin")] +pub struct CoinSchema { + pub denom: String, + #[schema(value_type = String)] + pub amount: Uint128, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)] pub struct CirculatingSupplyResponse { + #[schema(value_type = CoinSchema)] pub total_supply: Coin, + #[schema(value_type = CoinSchema)] pub mixmining_reserve: Coin, + #[schema(value_type = CoinSchema)] pub vesting_tokens: Coin, + #[schema(value_type = CoinSchema)] pub circulating_supply: Coin, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HostInformation { pub ip_address: Vec, pub hostname: Option, @@ -387,7 +437,7 @@ impl From for HostInf } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HostKeys { pub ed25519: String, pub x25519: String, @@ -402,7 +452,7 @@ impl From for HostKeys { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WebSockets { pub ws_port: u16, @@ -426,7 +476,7 @@ where } // for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)] pub struct OffsetDateTimeJsonSchemaWrapper( #[serde( default = "unix_epoch", @@ -494,7 +544,7 @@ impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { } // this struct is getting quite bloated... -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NymNodeDescription { #[serde(default)] pub last_polled: OffsetDateTimeJsonSchemaWrapper, @@ -528,7 +578,7 @@ fn default_node_role() -> NodeRole { NodeRole::Inactive } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct DescribedGateway { pub bond: GatewayBond, pub self_described: Option, @@ -543,7 +593,7 @@ impl From for DescribedGateway { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct DescribedMixNode { pub bond: MixNodeBond, pub self_described: Option, @@ -558,7 +608,7 @@ impl From for DescribedMixNode { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NetworkRequesterDetails { /// address of the embedded network requester pub address: String, @@ -567,25 +617,25 @@ pub struct NetworkRequesterDetails { pub uses_exit_policy: bool, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct IpPacketRouterDetails { /// address of the embedded ip packet router pub address: String, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct AuthenticatorDetails { /// address of the embedded authenticator pub address: String, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, pub uptime: u64, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ApiStatus { Up, @@ -606,7 +656,7 @@ impl ApiStatus { } } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SignerInformationResponse { pub cosmos_address: String, @@ -631,7 +681,7 @@ pub struct TestRoute { pub layer3: TestNode, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct PartialTestResult { pub monitor_run_id: i64, pub timestamp: i64, diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 50b527070a..3a4ea40ca9 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -8,6 +8,7 @@ use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::MixId; use serde::{Deserialize, Serialize}; use std::net::IpAddr; +use utoipa::ToSchema; #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] pub struct CachedNodesResponse { @@ -15,7 +16,7 @@ pub struct CachedNodesResponse { pub nodes: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { @@ -28,7 +29,7 @@ pub enum NodeRoleQueryParam { ExitGateway, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub enum NodeRole { // a properly active mixnode Mixnode { @@ -47,7 +48,7 @@ pub enum NodeRole { Inactive, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct BasicEntryInformation { pub hostname: Option, @@ -58,13 +59,15 @@ pub struct BasicEntryInformation { type NodeId = MixId; // the bare minimum information needed to construct sphinx packets -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SkimmedNode { // in directory v3 all nodes (mixnodes AND gateways) will have a unique id // but to keep structure consistent, introduce this field now + #[schema(value_type = u32)] pub node_id: NodeId, pub ed25519_identity_pubkey: String, + #[schema(value_type = Vec)] pub ip_addresses: Vec, // TODO: to be deprecated in favour of well-known hardcoded port for everyone @@ -74,6 +77,7 @@ pub struct SkimmedNode { pub entry: Option, /// Average node performance in last 24h period + #[schema(value_type = String)] pub performance: Performance, } @@ -143,14 +147,14 @@ impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { // an intermediate variant that exposes additional data such as noise keys but without // the full fat of the self-described data -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SemiSkimmedNode { pub basic: SkimmedNode, pub x25519_noise_pubkey: String, // pub location: } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct FullFatNode { pub expanded: SemiSkimmedNode, diff --git a/nym-api/nym-api-requests/src/pagination.rs b/nym-api/nym-api-requests/src/pagination.rs index 20ab30e7a6..c17d5a7bee 100644 --- a/nym-api/nym-api-requests/src/pagination.rs +++ b/nym-api/nym-api-requests/src/pagination.rs @@ -3,15 +3,16 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct Pagination { pub total: usize, pub page: u32, pub size: usize, } -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] pub struct PaginatedResponse { pub pagination: Pagination, pub data: Vec, diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs index af5568b158..7a2537ebee 100644 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ b/nym-api/src/circulating_supply_api/cache/mod.rs @@ -50,7 +50,7 @@ pub(crate) struct CirculatingSupplyCache { } impl CirculatingSupplyCache { - fn new(mix_denom: String) -> CirculatingSupplyCache { + pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache { CirculatingSupplyCache { initialised: Arc::new(AtomicBool::new(false)), data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))), @@ -67,6 +67,7 @@ impl CirculatingSupplyCache { } } + #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] pub(crate) fn stage(mix_denom: String) -> AdHoc { AdHoc::on_ignite("Circulating Supply Cache Stage", |rocket| async { rocket.manage(Self::new(mix_denom)) diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs new file mode 100644 index 0000000000..973d1fc64e --- /dev/null +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -0,0 +1,102 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + node_status_api::models::{AxumErrorResponse, AxumResult}, + v2::AxumAppState, +}; +use axum::{extract, Router}; +use nym_api_requests::models::CirculatingSupplyResponse; +use nym_validator_client::nyxd::Coin; + +pub(crate) fn circulating_supply_routes() -> Router { + Router::new() + .route("/", axum::routing::get(get_full_circulating_supply)) + .route( + "/total-supply-value", + axum::routing::get(get_circulating_supply), + ) + .route( + "/circulating-supply-value", + axum::routing::get(get_total_supply), + ) +} + +#[utoipa::path( + tag = "circulating-supply", + get, + path = "/v1/circulating-supply", + responses( + (status = 200, body = CirculatingSupplyResponse) + ) +)] +async fn get_full_circulating_supply( + extract::State(state): extract::State, +) -> AxumResult> { + match state + .circulating_supply_cache() + .get_circulating_supply() + .await + { + Some(value) => Ok(value.into()), + None => Err(AxumErrorResponse::internal_msg("unavailable")), + } +} + +#[utoipa::path( + tag = "circulating-supply", + get, + path = "/v1/circulating-supply/total-supply-value", + responses( + (status = 200, body = [f64]) + ) +)] +async fn get_total_supply( + extract::State(state): extract::State, +) -> AxumResult> { + let full_circulating_supply = match state + .circulating_supply_cache() + .get_circulating_supply() + .await + { + Some(res) => res, + None => return Err(AxumErrorResponse::internal_msg("unavailable")), + }; + + Ok(unym_coin_to_float_unym(full_circulating_supply.total_supply.into()).into()) +} + +#[utoipa::path( + tag = "circulating-supply", + get, + path = "/v1/circulating-supply/circulating-supply-value", + responses( + (status = 200, body = [f64]) + ) +)] +async fn get_circulating_supply( + extract::State(state): extract::State, +) -> AxumResult> { + let full_circulating_supply = match state + .circulating_supply_cache() + .get_circulating_supply() + .await + { + Some(res) => res, + None => return Err(AxumErrorResponse::internal_msg("unavailable")), + }; + + Ok(unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()).into()) +} + +// TODO: this is not the best place to put it, it should be more centralised, +// but for a quick fix, that's good enough for now... +// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent +// value from the mix denom here. +const UNYM_RATIO: f64 = 1000000.; + +fn unym_coin_to_float_unym(coin: Coin) -> f64 { + // our total supply can't exceed 1B so an overflow here is impossible + // (if it happened, then we SHOULD crash) + coin.amount as f64 / UNYM_RATIO +} diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 0449e9f09f..2bfefcc35f 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -11,6 +11,8 @@ use crate::support::{config, nyxd}; use self::cache::refresher::CirculatingSupplyCacheRefresher; pub(crate) mod cache; +#[cfg(feature = "axum")] +pub(crate) mod handlers; pub(crate) mod routes; /// Merges the routes with http information and returns it to Rocket for serving diff --git a/nym-api/src/circulating_supply_api/routes.rs b/nym-api/src/circulating_supply_api/routes.rs index 5bc76ab8bb..b8b00cb82e 100644 --- a/nym-api/src/circulating_supply_api/routes.rs +++ b/nym-api/src/circulating_supply_api/routes.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::node_status_api::models::ErrorResponse; +use crate::node_status_api::models::RocketErrorResponse; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::Coin; use rocket::http::Status; @@ -26,10 +26,10 @@ fn unym_coin_to_float_unym(coin: Coin) -> f64 { #[get("/circulating-supply")] pub(crate) async fn get_full_circulating_supply( cache: &State, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { match cache.get_circulating_supply().await { Some(value) => Ok(Json(value)), - None => Err(ErrorResponse::new( + None => Err(RocketErrorResponse::new( "unavailable", Status::InternalServerError, )), @@ -40,11 +40,11 @@ pub(crate) async fn get_full_circulating_supply( #[get("/circulating-supply/total-supply-value")] pub(crate) async fn get_total_supply( cache: &State, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { let full_circulating_supply = match cache.get_circulating_supply().await { Some(res) => res, None => { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "unavailable", Status::InternalServerError, )) @@ -60,11 +60,11 @@ pub(crate) async fn get_total_supply( #[get("/circulating-supply/circulating-supply-value")] pub(crate) async fn get_circulating_supply( cache: &State, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { let full_circulating_supply = match cache.get_circulating_supply().await { Some(res) => res, None => { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "unavailable", Status::InternalServerError, )) diff --git a/nym-api/src/ecash/api_routes/aggregation_axum.rs b/nym-api/src/ecash/api_routes/aggregation_axum.rs new file mode 100644 index 0000000000..4a03313580 --- /dev/null +++ b/nym-api/src/ecash/api_routes/aggregation_axum.rs @@ -0,0 +1,143 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::EpochIdParam; +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::extract::Path; +use axum::{Json, Router}; +use log::trace; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, +}; +use nym_api_requests::ecash::VerificationKeyResponse; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use serde::Deserialize; +use std::sync::Arc; +use time::Date; +use utoipa::IntoParams; + +/// routes with globally aggregated keys, signatures, etc. +pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/master-verification-key:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| master_verification_key(epoch_id, ecash_state) + }), + ) + .route( + "/aggregated-expiration-date-signatures:expiration_date", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/aggregated-coin-indices-signatures:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} + +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/master-verification-key/{epoch_id}", + responses( + (status = 200, body = VerificationKeyResponse) + ) +)] +async fn master_verification_key( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { + trace!("aggregated_verification_key request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let key = state.master_verification_key(epoch_id).await?; + + Ok(Json(VerificationKeyResponse::new(key.clone()))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct ExpirationDateParam { + expiration_date: Option, +} + +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}", + responses( + (status = 200, body = AggregatedExpirationDateSignatureResponse) + ) +)] +async fn expiration_date_signatures( + Path(ExpirationDateParam { expiration_date }): Path, + state: Arc, +) -> AxumResult> { + trace!("aggregated_expiration_date_signatures request"); + + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .master_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}", + responses( + (status = 200, body = AggregatedCoinIndicesSignatureResponse) + ) +)] +async fn coin_indices_signatures( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { + trace!("aggregated_coin_indices_signatures request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; + + Ok(Json(AggregatedCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs new file mode 100644 index 0000000000..b40ccf9773 --- /dev/null +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -0,0 +1,19 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::aggregation_axum::aggregation_routes; +use crate::ecash::api_routes::issued_axum::issued_routes; +use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes; +use crate::ecash::api_routes::spending_axum::spending_routes; +use crate::ecash::state::EcashState; +use crate::v2::AxumAppState; +use axum::Router; +use std::sync::Arc; + +pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { + Router::new() + .merge(aggregation_routes(Arc::clone(&ecash_state))) + .merge(issued_routes(Arc::clone(&ecash_state))) + .merge(partial_signing_routes(Arc::clone(&ecash_state))) + .merge(spending_routes(Arc::clone(&ecash_state))) +} diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 521f11067a..4469bf11d0 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -26,3 +26,10 @@ pub(crate) fn build_credentials_response( Ok(IssuedCredentialsResponse { credentials }) } + +#[cfg(feature = "axum")] +#[derive(serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Path)] +pub(super) struct EpochIdParam { + pub(super) epoch_id: Option, +} diff --git a/nym-api/src/ecash/api_routes/issued_axum.rs b/nym-api/src/ecash/api_routes/issued_axum.rs new file mode 100644 index 0000000000..72fa55e39f --- /dev/null +++ b/nym-api/src/ecash/api_routes/issued_axum.rs @@ -0,0 +1,147 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::build_credentials_response; +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::extract::Path; +use axum::{Json, Router}; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +}; +use nym_api_requests::ecash::CredentialsRequestBody; +use serde::Deserialize; +use std::sync::Arc; +use utoipa::IntoParams; + +pub(crate) fn issued_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/epoch-credentials/:epoch", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch| epoch_credentials(epoch, ecash_state) + }), + ) + .route( + "/issued-credential/:id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |id| issued_credential(id, ecash_state) + }), + ) + .route( + "/issued-credentials", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| issued_credentials(body, ecash_state) + }), + ) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct EpochParam { + epoch: u64, +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochParam + ), + path = "/v1/ecash/epoch-credentials/{epoch}", + responses( + (status = 200, body = EpochCredentialsResponse) + ) +)] +async fn epoch_credentials( + Path(EpochParam { epoch }): Path, + state: Arc, +) -> AxumResult> { + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; + + let response = if let Some(issued) = issued { + issued.into() + } else { + EpochCredentialsResponse { + epoch_id: epoch, + first_epoch_credential_id: None, + total_issued: 0, + } + }; + + Ok(Json(response)) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdParam { + id: i64, +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + IdParam + ), + path = "/v1/ecash/issued-credential/{id}", + responses( + (status = 200, body = IssuedCredentialResponse) + ) +)] +async fn issued_credential( + Path(IdParam { id }): Path, + state: Arc, +) -> AxumResult> { + let issued = state.aux.storage.get_issued_credential(id).await?; + + let credential = if let Some(issued) = issued { + Some(issued.try_into()?) + } else { + None + }; + + Ok(Json(IssuedCredentialResponse { credential })) +} + +#[utoipa::path( + tag = "Ecash", + post, + request_body = CredentialsRequestBody, + path = "/v1/ecash/issued-credentials", + responses( + (status = 200, body = IssuedCredentialsResponse) + ) +)] +async fn issued_credentials( + Json(params): Json, + state: Arc, +) -> AxumResult> { + if params.pagination.is_some() && !params.credential_ids.is_empty() { + return Err(EcashError::InvalidQueryArguments.into()); + } + + let credentials = if let Some(pagination) = params.pagination { + state + .aux + .storage + .get_issued_credentials_paged(pagination) + .await? + } else { + state + .aux + .storage + .get_issued_credentials(params.credential_ids) + .await? + }; + + build_credentials_response(credentials) + .map(Json) + .map_err(From::from) +} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 6037a8d18f..910e253288 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -6,3 +6,13 @@ mod helpers; pub(crate) mod issued; pub(crate) mod partial_signing; pub(crate) mod spending; + +cfg_if::cfg_if! { + if #[cfg(feature = "axum")] { + pub(crate) mod aggregation_axum; + pub(crate) mod handlers; + pub(crate) mod issued_axum; + pub(crate) mod partial_signing_axum; + pub(crate) mod spending_axum; + } +} diff --git a/nym-api/src/ecash/api_routes/partial_signing_axum.rs b/nym-api/src/ecash/api_routes/partial_signing_axum.rs new file mode 100644 index 0000000000..fe93ddc921 --- /dev/null +++ b/nym-api/src/ecash/api_routes/partial_signing_axum.rs @@ -0,0 +1,178 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::EpochIdParam; +use crate::ecash::error::EcashError; +use crate::ecash::helpers::blind_sign; +use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::extract::Path; +use axum::{Json, Router}; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, +}; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use serde::Deserialize; +use std::ops::Deref; +use std::sync::Arc; +use time::Date; +use utoipa::IntoParams; + +pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/blind-sign", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| post_blind_sign(body, ecash_state) + }), + ) + .route( + "/partial-expiration-date-signatures:expiration_date", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/partial-coin-indices-signatures:epoch_id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} + +#[utoipa::path( + tag = "Ecash", + post, + request_body = BlindSignRequestBody, + path = "/v1/ecash/blind-sign", + responses( + (status = 200, body = BlindedSignatureResponse) + ) +)] +async fn post_blind_sign( + Json(blind_sign_request_body): Json, + state: Arc, +) -> AxumResult> { + debug!("Received blind sign request"); + trace!("body: {:?}", blind_sign_request_body); + + // check if we have the signing key available + debug!("checking if we actually have ecash keys derived..."); + let signing_key = state.ecash_signing_key().await?; + + // basic check of expiration date validity + if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { + return Err(EcashError::ExpirationDateTooLate.into()); + } + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + // check if we already issued a credential for this deposit + let deposit_id = blind_sign_request_body.deposit_id; + debug!( + "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", + ); + if let Some(blinded_signature) = state.already_issued(deposit_id).await? { + return Ok(Json(BlindedSignatureResponse { blinded_signature })); + } + + //check if account was blacklisted + let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); + state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; + + // get the deposit details of the claimed id + debug!("getting deposit details from the chain"); + let deposit = state.get_deposit(deposit_id).await?; + + // check validity of the request + debug!("fully validating received request"); + state + .validate_request(&blind_sign_request_body, deposit) + .await?; + + // produce the partial signature + debug!("producing the partial credential"); + let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?; + + // store the information locally + debug!("storing the issued credential in the database"); + state + .store_issued_credential(blind_sign_request_body, &blinded_signature) + .await?; + + // finally return the credential to the client + Ok(Json(BlindedSignatureResponse { blinded_signature })) +} + +#[derive(Deserialize, IntoParams)] +struct ExpirationDateParam { + expiration_date: Option, +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse) + ) +)] +async fn partial_expiration_date_signatures( + Path(ExpirationDateParam { expiration_date }): Path, + state: Arc, +) -> AxumResult> { + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .partial_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(PartialExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse) + ) +)] +async fn partial_coin_indices_signatures( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; + + Ok(Json(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/ecash/api_routes/spending_axum.rs b/nym-api/src/ecash/api_routes/spending_axum.rs new file mode 100644 index 0000000000..aca64b7f0c --- /dev/null +++ b/nym-api/src/ecash/api_routes/spending_axum.rs @@ -0,0 +1,245 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::{Json, Router}; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{ + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, + EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_compact_ecash::identify::IdentifyResult; +use nym_ecash_time::EcashTime; +use std::collections::HashSet; +use std::ops::Deref; +use std::sync::Arc; +use time::macros::time; +use time::{OffsetDateTime, Time}; + +pub(crate) fn spending_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/verify-ecash-ticket", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| verify_ticket(body, ecash_state) + }), + ) + .route( + "/batch-redeem-ecash-tickets", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| batch_redeem_tickets(body, ecash_state) + }), + ) + .route( + "/double-spending-filter-v1", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + || double_spending_filter_v1(ecash_state) + }), + ) +} + +const ONE_AM: Time = time!(1:00); + +fn reject_ticket( + reason: EcashTicketVerificationRejection, +) -> AxumResult> { + Ok(Json(EcashTicketVerificationResponse::reject(reason))) +} + +// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` +// introduce bloomfilter checks without touching storage first, etc. +#[utoipa::path( + tag = "Ecash", + post, + request_body = VerifyEcashTicketBody, + path = "/v1/ecash/verify-ecash-ticket", + responses( + (status = 200, body = EcashTicketVerificationResponse) + ) +)] +async fn verify_ticket( + // TODO in the future: make it send binary data rather than json + Json(verify_ticket_body): Json, + state: Arc, +) -> AxumResult> { + let credential_data = &verify_ticket_body.credential; + let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; + + // easy check: is there only a single payment attached? + if credential_data.payment.spend_value != 1 { + return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); + } + + let sn = &credential_data.encoded_serial_number(); + let spend_date = credential_data.spend_date; + let epoch_id = credential_data.epoch_id; + + let now = OffsetDateTime::now_utc(); + let today_ecash = now.ecash_date(); + + #[allow(clippy::unwrap_used)] + let yesterday_ecash = today_ecash.previous_day().unwrap(); + + // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC + if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { + return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }); + } + + // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification + // TODO: when blacklisting is implemented, this should get removed + if state.check_bloomfilter(sn).await { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + // actual double spend detection with storage + if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { + match nym_compact_ecash::identify::identify( + &credential_data.payment, + &previous_payment.payment, + credential_data.pay_info, + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + } + } + } + + let verification_key = state.master_verification_key(Some(epoch_id)).await?; + + // perform actual crypto verification + if credential_data.verify(&verification_key).is_err() { + return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + } + + // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN + let was_present = state + .update_bloomfilter(sn, spend_date, today_ecash) + .await?; + if was_present { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + //store credential + state + .store_verified_ticket(credential_data, gateway_cosmos_addr) + .await?; + + Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) +} + +// // for particular SN returns what gateway has submitted it and whether it has been verified correctly +// async fn credential_status() -> ! { +// todo!() +// } + +#[utoipa::path( + tag = "Ecash", + post, + request_body = BatchRedeemTicketsBody, + path = "/v1/ecash/batch-redeem-ecash-tickets", + responses( + (status = 200, body = EcashBatchTicketRedemptionResponse) + ) +)] +async fn batch_redeem_tickets( + // TODO in the future: make it send binary data rather than json + Json(batch_redeem_credentials_body): Json, + state: Arc, +) -> AxumResult> { + // 1. see if that gateway has even submitted any tickets + let Some(provider_info) = state + .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) + .await? + else { + return Err(EcashError::NotTicketsProvided.into()); + }; + + // 2. check if the gateway is not trying to spam the redemption requests + // (we have to protect our poor chain) + if let Some(last_redemption) = provider_info.last_batch_verification { + let now = OffsetDateTime::now_utc(); + let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; + + if next_allowed > now { + return Err(EcashError::TooFrequentRedemption { + last_redemption, + next_allowed, + } + .into()); + } + } + + // 3. verify the request digest + if !batch_redeem_credentials_body.verify_digest() { + return Err(EcashError::MismatchedRequestDigest.into()); + } + + // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) + state + .validate_redemption_proposal(&batch_redeem_credentials_body) + .await?; + + let proposal_id = batch_redeem_credentials_body.proposal_id; + let received = batch_redeem_credentials_body.included_serial_numbers; + + // 5. check if **every** serial number included in the request has been verified by us + // if we have more than requested, tough luck, they're going to lose them + let verified = state.get_redeemable_tickets(provider_info).await?; + let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); + + for sn in &received { + if !verified_tickets.contains(sn.deref()) { + return Err(EcashError::TicketNotVerified { + serial_number_bs58: bs58::encode(sn).into_string(), + } + .into()); + } + } + + // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx + state.accept_proposal(proposal_id).await?; + Ok(Json(EcashBatchTicketRedemptionResponse { + proposal_accepted: true, + })) +} + +// explicitly mark it as v1 in the URL because the response type WILL change; +// it will probably be compressed bincode or something +#[utoipa::path( + tag = "Ecash", + get, + path = "/v1/ecash/double-spending-filter-v1", + responses( + (status = 200, body = SpentCredentialsResponse) + ) +)] +async fn double_spending_filter_v1( + state: Arc, +) -> AxumResult> { + let spent_credentials_export = state.get_bloomfilter_bytes().await; + Ok(Json(SpentCredentialsResponse::new( + spent_credentials_export, + ))) +} diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index d8e4365eee..952bb5ca1f 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -268,14 +268,11 @@ impl RewardedSetUpdater { pub(crate) fn start( nyxd_client: Client, nym_contract_cache: &NymContractCache, - storage: &NymApiStorage, + storage: NymApiStorage, shutdown: &TaskManager, ) { - let mut rewarded_set_updater = RewardedSetUpdater::new( - nyxd_client, - nym_contract_cache.to_owned(), - storage.to_owned(), - ); + let mut rewarded_set_updater = + RewardedSetUpdater::new(nyxd_client, nym_contract_cache.to_owned(), storage); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 0bebd493ed..3121ce18f2 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,6 +1,9 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +// TODO rocket remove +#![allow(deprecated)] + #[macro_use] extern crate rocket; @@ -41,11 +44,15 @@ pub(crate) mod nym_nodes; mod status; pub(crate) mod support; +#[cfg(feature = "axum")] +mod v2; + struct ShutdownHandles { task_manager_handle: TaskManager, rocket_handle: rocket::Shutdown, } +// TODO rocket: remove all such Todos once rocket is phased out completely #[tokio::main] async fn main() -> Result<(), anyhow::Error> { cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] { @@ -54,6 +61,7 @@ async fn main() -> Result<(), anyhow::Error> { }} setup_logging(); + // TODO rocket: replace with tracing logger once rocket is eliminated from code info!("Starting nym api..."); @@ -107,7 +115,11 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result let nym_contract_cache_state = rocket.state::().unwrap(); let node_status_cache_state = rocket.state::().unwrap(); let circulating_supply_cache_state = rocket.state::().unwrap(); - let maybe_storage = rocket.state::(); + let storage = if let Some(storage) = rocket.state::() { + storage.to_owned() + } else { + storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await? + }; let described_nodes_state = rocket.state::>().unwrap(); // start note describe cache refresher @@ -134,7 +146,7 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result &config.node_status_api, nym_contract_cache_state, node_status_cache_state, - maybe_storage, + storage.to_owned(), nym_contract_cache_listener, &shutdown, ); @@ -163,19 +175,16 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring if it's enabled if config.network_monitor.enabled { - // if network monitor is enabled, the storage MUST BE available - let storage = maybe_storage.unwrap(); - network_monitor::start::( &config.network_monitor, nym_contract_cache_state, - storage, + &storage, nyxd_client.clone(), &shutdown, ) .await; - HistoricalUptimeUpdater::start(storage, &shutdown); + HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown); // start 'rewarding' if its enabled if config.rewarding.enabled { @@ -183,7 +192,6 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown); } } - // Launch the rocket, serve http endpoints and finish the startup tokio::spawn(rocket.launch()); diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs new file mode 100644 index 0000000000..0cb27ca92d --- /dev/null +++ b/nym-api/src/network/handlers.rs @@ -0,0 +1,100 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::network::models::{ContractInformation, NetworkDetails}; +use crate::v2::AxumAppState; +use axum::{extract, Router}; +use nym_contracts_common::ContractBuildInformation; +use std::collections::HashMap; +use utoipa::ToSchema; + +pub(crate) fn nym_network_routes() -> Router { + Router::new() + .route("/details", axum::routing::get(network_details)) + .route("/nym-contracts", axum::routing::get(nym_contracts)) + .route( + "/nym-contracts-detailed", + axum::routing::get(nym_contracts_detailed), + ) +} + +#[utoipa::path( + tag = "network", + get, + path = "/v1/network/details", + responses( + (status = 200, body = NetworkDetails) + ) +)] +async fn network_details( + extract::State(state): extract::State, +) -> axum::Json { + state.network_details().to_owned().into() +} + +// it's used for schema generation so dead_code is fine +#[allow(dead_code)] +#[derive(ToSchema)] +#[schema(title = "ContractVersion")] +pub(crate) struct ContractVersionSchemaResponse { + /// contract is the crate name of the implementing contract, eg. `crate:cw20-base` + /// we will use other prefixes for other languages, and their standard global namespacing + pub contract: String, + /// version is any string that this implementation knows. It may be simple counter "1", "2". + /// or semantic version on release tags "v0.7.0", or some custom feature flag list. + /// the only code that needs to understand the version parsing is code that knows how to + /// migrate from the given contract (and is tied to it's implementation somehow) + pub version: String, +} + +#[utoipa::path( + tag = "network", + get, + path = "/v1/network/nym-contracts", + responses( + (status = 200, body = HashMap>) + ) +)] +async fn nym_contracts( + extract::State(state): extract::State, +) -> axum::Json>> { + let info = state.nym_contract_cache().contract_details().await; + info.iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.base.clone(), + }, + ) + }) + .collect::>() + .into() +} + +#[utoipa::path( + tag = "network", + get, + path = "/v1/network/nym-contracts-detailed", + responses( + (status = 200, body = HashMap>) + ) +)] +async fn nym_contracts_detailed( + extract::State(state): extract::State, +) -> axum::Json>> { + let info = state.nym_contract_cache().contract_details().await; + info.iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.detailed.clone(), + }, + ) + }) + .collect::>() + .into() +} diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index ed60de4a39..1f91dea3fd 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -6,6 +6,8 @@ use rocket::Route; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; +#[cfg(feature = "axum")] +pub(crate) mod handlers; pub(crate) mod models; mod routes; diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 5a86ea6401..7cdaa1d052 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -6,6 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] pub struct NetworkDetails { pub(crate) connected_nyxd: String, pub(crate) network: NymNetworkDetails, @@ -20,6 +21,7 @@ impl NetworkDetails { } } +#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] #[derive(Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct ContractInformation { diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 022a56f156..99d4358802 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -41,12 +41,13 @@ pub struct NodeStatusCache { impl NodeStatusCache { /// Creates a new cache with no data. - fn new() -> NodeStatusCache { + pub(crate) fn new() -> NodeStatusCache { NodeStatusCache { inner: Arc::new(RwLock::new(NodeStatusCacheData::new())), } } + #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] pub fn stage() -> AdHoc { AdHoc::on_ignite("Node Status Cache", |rocket| async { rocket.manage(Self::new()) diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index 05dfc8f777..d20f3329b4 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -52,12 +52,11 @@ pub(super) fn split_into_active_and_rewarded_set( } pub(super) async fn get_mixnode_performance_from_storage( - storage: &Option, + storage: &NymApiStorage, mix_id: MixId, epoch: Interval, ) -> Option { storage - .as_ref()? .get_average_mixnode_uptime_in_the_last_24hrs( mix_id, epoch.current_epoch_end_unix_timestamp(), @@ -68,12 +67,11 @@ pub(super) async fn get_mixnode_performance_from_storage( } pub(super) async fn get_gateway_performance_from_storage( - storage: &Option, + storage: &NymApiStorage, gateway_id: &str, epoch: Interval, ) -> Option { storage - .as_ref()? .get_average_gateway_uptime_in_the_last_24hrs( gateway_id, epoch.current_epoch_end_unix_timestamp(), @@ -84,7 +82,7 @@ pub(super) async fn get_gateway_performance_from_storage( } pub(super) async fn annotate_nodes_with_details( - storage: &Option, + storage: &NymApiStorage, mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, @@ -123,16 +121,12 @@ pub(super) async fn annotate_nodes_with_details( current_interval, ); - let node_performance = if let Some(storage) = storage { - storage - .construct_mixnode_report(mixnode.mix_id()) - .await - .map(NodePerformance::from) - .ok() - } else { - None - } - .unwrap_or_default(); + let node_performance = storage + .construct_mixnode_report(mixnode.mix_id()) + .await + .map(NodePerformance::from) + .ok() + .unwrap_or_default(); // safety: this conversion is infallible let ip_addresses = @@ -177,7 +171,7 @@ pub(super) async fn annotate_nodes_with_details( } pub(crate) async fn annotate_gateways_with_details( - storage: &Option, + storage: &NymApiStorage, gateway_bonds: Vec, current_interval: Interval, blacklist: &HashSet, @@ -192,16 +186,12 @@ pub(crate) async fn annotate_gateways_with_details( .await .unwrap_or_default(); - let node_performance = if let Some(storage) = storage { - storage - .construct_gateway_report(gateway_bond.identity()) - .await - .map(NodePerformance::from) - .ok() - } else { - None - } - .unwrap_or_default(); + let node_performance = storage + .construct_gateway_report(gateway_bond.identity()) + .await + .map(NodePerformance::from) + .ok() + .unwrap_or_default(); // safety: this conversion is infallible let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() { diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index e1dd3896f0..ad07919e62 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -29,7 +29,7 @@ pub struct NodeStatusCacheRefresher { // Sources for when refreshing data contract_cache: NymContractCache, contract_cache_listener: watch::Receiver, - storage: Option, + storage: NymApiStorage, } impl NodeStatusCacheRefresher { @@ -38,7 +38,7 @@ impl NodeStatusCacheRefresher { fallback_caching_interval: Duration, contract_cache: NymContractCache, contract_cache_listener: watch::Receiver, - storage: Option, + storage: NymApiStorage, ) -> Self { Self { cache, diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs new file mode 100644 index 0000000000..2a385b2812 --- /dev/null +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::v2::AxumAppState; +use axum::Router; +use nym_mixnet_contract_common::MixId; +use serde::Deserialize; +use utoipa::IntoParams; + +pub(crate) mod network_monitor; +pub(crate) mod unstable; +pub(crate) mod without_monitor; + +pub(crate) fn node_status_routes(network_monitor: bool) -> Router { + // in the minimal variant we would not have access to endpoints relying on existence + // of the network monitor and the associated storage + let without_network_monitor = without_monitor::mandatory_routes(); + + if network_monitor { + let with_network_monitor = network_monitor::network_monitor_routes(); + + with_network_monitor.merge(without_network_monitor) + } else { + without_network_monitor + } +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct MixIdParam { + mix_id: MixId, +} diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs new file mode 100644 index 0000000000..ea38422c17 --- /dev/null +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -0,0 +1,339 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::handlers::MixIdParam; +use crate::node_status_api::helpers::{ + _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed, + _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation, + _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, + _mixnode_uptime_history, +}; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::extract::{Path, Query, State}; +use axum::Json; +use axum::Router; +use nym_api_requests::models::{ + ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, + GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, + MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, + MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse, +}; +use serde::Deserialize; +use utoipa::IntoParams; + +use super::unstable; + +pub(super) fn network_monitor_routes() -> Router { + Router::new() + .nest( + "/gateway/:identity", + Router::new() + .route("/report", axum::routing::get(gateway_report)) + .route("/history", axum::routing::get(gateway_uptime_history)) + .route( + "/core-status-count", + axum::routing::get(gateway_core_status_count), + ) + .route("/avg_uptime", axum::routing::get(get_gateway_avg_uptime)), + ) + .nest( + "/mixnode/:mix_id", + Router::new() + .route("/report", axum::routing::get(mixnode_report)) + .route("/history", axum::routing::get(mixnode_uptime_history)) + .route( + "/core-status-count", + axum::routing::get(mixnode_core_status_count), + ) + .route( + "/reward-estimation", + axum::routing::get(get_mixnode_reward_estimation), + ) + .route( + "/compute-reward-estimation", + axum::routing::post(compute_mixnode_reward_estimation), + ) + .route("/avg_uptime", axum::routing::get(get_mixnode_avg_uptime)), + ) + .nest( + "/mixnodes", + Router::new() + .route( + "/detailed-unfiltered", + axum::routing::get(get_mixnodes_detailed_unfiltered), + ) + .route( + "/unstable/:mix_id/test-results", + axum::routing::get(unstable::mixnode_test_results), + ), + ) + .nest( + "/gateways", + Router::new() + .route("/detailed", axum::routing::get(get_gateways_detailed)) + .route( + "/detailed-unfiltered", + axum::routing::get(get_gateways_detailed_unfiltered), + ) + .route( + "/unstable/:gateway_identity/test-results", + axum::routing::get(unstable::gateway_test_results), + ), + ) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/gateway/{identity}/report", + responses( + (status = 200, body = GatewayStatusReportResponse) + ) +)] +async fn gateway_report( + Path(identity): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _gateway_report(state.node_status_cache(), &identity).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/gateway/{identity}/history", + responses( + (status = 200, body = GatewayUptimeHistoryResponse) + ) +)] +async fn gateway_uptime_history( + Path(identity): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _gateway_uptime_history(state.storage(), &identity).await?, + )) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Query)] +struct SinceQueryParams { + since: Option, +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + SinceQueryParams + ), + path = "/v1/status/gateway/{identity}/core-status-count", + responses( + (status = 200, body = GatewayCoreStatusResponse) + ) +)] +async fn gateway_core_status_count( + Path(identity): Path, + Query(SinceQueryParams { since }): Query, + State(state): State, +) -> AxumResult> { + Ok(Json( + _gateway_core_status_count(state.storage(), &identity, since).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/gateway/{identity}/avg_uptime", + responses( + (status = 200, body = GatewayUptimeResponse) + ) +)] +async fn get_gateway_avg_uptime( + Path(identity): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/report", + responses( + (status = 200, body = MixnodeStatusReportResponse) + ) +)] +async fn mixnode_report( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _mixnode_report(state.node_status_cache(), mix_id).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/history", + responses( + (status = 200, body = MixnodeUptimeHistoryResponse) + ) +)] +async fn mixnode_uptime_history( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _mixnode_uptime_history(state.storage(), mix_id).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + MixIdParam, SinceQueryParams + ), + path = "/v1/status/mixnode/{mix_id}/core-status-count", + responses( + (status = 200, body = MixnodeCoreStatusResponse) + ) +)] +async fn mixnode_core_status_count( + Path(MixIdParam { mix_id }): Path, + Query(SinceQueryParams { since }): Query, + State(state): State, +) -> AxumResult> { + Ok(Json( + _mixnode_core_status_count(state.storage(), mix_id, since).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/reward-estimation", + responses( + (status = 200, body = RewardEstimationResponse) + ) +)] +async fn get_mixnode_reward_estimation( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_mixnode_reward_estimation( + state.node_status_cache(), + state.nym_contract_cache(), + mix_id, + ) + .await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + post, + params( + ComputeRewardEstParam, MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation", + request_body = ComputeRewardEstParam, + responses( + (status = 200, body = RewardEstimationResponse) + ) +)] +async fn compute_mixnode_reward_estimation( + Path(MixIdParam { mix_id }): Path, + State(state): State, + Json(user_reward_param): Json, +) -> AxumResult> { + Ok(Json( + _compute_mixnode_reward_estimation( + &user_reward_param, + state.node_status_cache(), + state.nym_contract_cache(), + mix_id, + ) + .await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/avg_uptime", + responses( + (status = 200, body = UptimeResponse) + ) +)] +async fn get_mixnode_avg_uptime( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, + )) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/mixnodes/detailed-unfiltered", + responses( + (status = 200, body = MixNodeBondAnnotated) + ) +)] +pub async fn get_mixnodes_detailed_unfiltered( + State(state): State, +) -> Json> { + Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/gateways/detailed", + responses( + (status = 200, body = GatewayBondAnnotated) + ) +)] +pub async fn get_gateways_detailed( + State(state): State, +) -> Json> { + Json(_get_gateways_detailed(state.node_status_cache()).await) +} + +#[utoipa::path( + tag = "network-monitor-status", + get, + path = "/v1/status/gateways/detailed-unfiltered", + responses( + (status = 200, body = GatewayBondAnnotated) + ) +)] +pub async fn get_gateways_detailed_unfiltered( + State(state): State, +) -> Json> { + Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await) +} diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs new file mode 100644 index 0000000000..b8a30639b4 --- /dev/null +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -0,0 +1,279 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::helpers::PaginationRequest; +use crate::support::storage::NymApiStorage; +use crate::v2::AxumAppState; +use axum::extract::{Path, Query, State}; +use axum::Json; +use nym_api_requests::models::{ + GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, +}; +use nym_api_requests::pagination::Pagination; +use nym_mixnet_contract_common::MixId; +use std::cmp::min; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub type DbId = i64; + +// a simply in-memory cache of node details +#[derive(Debug, Clone, Default)] +pub struct NodeInfoCache { + inner: Arc>, +} + +impl NodeInfoCache { + async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { + { + let read_guard = self.inner.read().await; + if let Some(cached) = read_guard.mixnodes.get(&db_id) { + trace!("cache hit for mixnode {db_id}"); + return cached.clone(); + } + } + trace!("cache miss for mixnode {db_id}"); + + let mut write_guard = self.inner.write().await; + // double-check the cache in case somebody already updated it while we were waiting for the lock + if let Some(cached) = write_guard.mixnodes.get(&db_id) { + return cached.clone(); + } + + let details = match storage.get_mixnode_details_by_db_id(db_id).await { + Ok(Some(details)) => details.into(), + Ok(None) => { + error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!"); + TestNode::default() + } + Err(err) => { + // don't insert into the cache in case another request is successful + error!("failed to retrieve details for mixnode {db_id}: {err}"); + return TestNode::default(); + } + }; + + write_guard.mixnodes.insert(db_id, details.clone()); + details + } + + async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { + { + let read_guard = self.inner.read().await; + if let Some(cached) = read_guard.gateways.get(&db_id) { + trace!("cache hit for gateway {db_id}"); + return cached.clone(); + } + } + trace!("cache miss for gateway {db_id}"); + + let mut write_guard = self.inner.write().await; + // double-check the cache in case somebody already updated it while we were waiting for the lock + if let Some(cached) = write_guard.gateways.get(&db_id) { + return cached.clone(); + } + + let details = match storage.get_gateway_details_by_db_id(db_id).await { + Ok(Some(details)) => details.into(), + Ok(None) => { + error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!"); + TestNode::default() + } + Err(err) => { + // don't insert into the cache in case another request is successful + error!("failed to retrieve details for gateway {db_id}: {err}"); + return TestNode::default(); + } + }; + + write_guard.gateways.insert(db_id, details.clone()); + details + } +} + +#[derive(Debug, Clone, Default)] +struct NodeInfoCacheInner { + mixnodes: HashMap, + gateways: HashMap, +} + +const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; +const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; + +async fn _mixnode_test_results( + mix_id: MixId, + page: u32, + per_page: u32, + info_cache: &NodeInfoCache, + storage: &NymApiStorage, +) -> anyhow::Result { + // convert to db offset + // we're paging from page 0 like civilised people, + // so we have to skip (page * per_page) results + let offset = page * per_page; + let limit = per_page; + + let raw_results = storage + .get_mixnode_detailed_statuses(mix_id, limit, offset) + .await?; + let total = match raw_results.first() { + None => 0, + Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?, + }; + + let mut partial_results = Vec::new(); + for result in raw_results { + let gateway = info_cache + .get_gateway_details(result.gateway_id, storage) + .await; + let layer1 = info_cache + .get_mix_node_details(result.layer1_mix_id, storage) + .await; + let layer2 = info_cache + .get_mix_node_details(result.layer2_mix_id, storage) + .await; + let layer3 = info_cache + .get_mix_node_details(result.layer3_mix_id, storage) + .await; + + partial_results.push(PartialTestResult { + monitor_run_id: result.monitor_run_id, + timestamp: result.timestamp, + overall_reliability_for_all_routes_in_monitor_run: result.reliability, + test_routes: TestRoute { + gateway, + layer1, + layer2, + layer3, + }, + }) + } + + Ok(MixnodeTestResultResponse { + pagination: Pagination { + total, + page, + size: partial_results.len(), + }, + data: partial_results, + }) +} + +pub async fn mixnode_test_results( + Path(mix_id): Path, + Query(pagination): Query, + State(state): State, +) -> AxumResult> { + let page = pagination.page.unwrap_or_default(); + let per_page = min( + pagination + .per_page + .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), + MAX_TEST_RESULTS_PAGE_SIZE, + ); + + match _mixnode_test_results( + mix_id, + page, + per_page, + state.node_info_cache(), + state.storage(), + ) + .await + { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve mixnode test results for node {mix_id}: {err}" + ))), + } +} + +async fn _gateway_test_results( + gateway_identity: &str, + page: u32, + per_page: u32, + info_cache: &NodeInfoCache, + storage: &NymApiStorage, +) -> anyhow::Result { + // convert to db offset + // we're paging from page 0 like civilised people, + // so we have to skip (page * per_page) results + let offset = page * per_page; + let limit = per_page; + + let raw_results = storage + .get_gateway_detailed_statuses(gateway_identity, limit, offset) + .await?; + let total = match raw_results.first() { + None => 0, + Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?, + }; + + let mut partial_results = Vec::new(); + for result in raw_results { + let gateway = info_cache + .get_gateway_details(result.gateway_id, storage) + .await; + let layer1 = info_cache + .get_mix_node_details(result.layer1_mix_id, storage) + .await; + let layer2 = info_cache + .get_mix_node_details(result.layer2_mix_id, storage) + .await; + let layer3 = info_cache + .get_mix_node_details(result.layer3_mix_id, storage) + .await; + + partial_results.push(PartialTestResult { + monitor_run_id: result.monitor_run_id, + timestamp: result.timestamp, + overall_reliability_for_all_routes_in_monitor_run: result.reliability, + test_routes: TestRoute { + gateway, + layer1, + layer2, + layer3, + }, + }) + } + + Ok(GatewayTestResultResponse { + pagination: Pagination { + total, + page, + size: partial_results.len(), + }, + data: partial_results, + }) +} + +pub async fn gateway_test_results( + Path(gateway_identity): Path, + Query(pagination): Query, + State(state): State, +) -> AxumResult> { + let page = pagination.page.unwrap_or_default(); + let per_page = min( + pagination + .per_page + .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), + MAX_TEST_RESULTS_PAGE_SIZE, + ); + + match _gateway_test_results( + &gateway_identity, + page, + per_page, + state.node_info_cache(), + state.storage(), + ) + .await + { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" + ))), + } +} diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs new file mode 100644 index 0000000000..b431d38913 --- /dev/null +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -0,0 +1,176 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::handlers::MixIdParam; +use crate::node_status_api::helpers::{ + _get_active_set_detailed, _get_mixnode_inclusion_probabilities, + _get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status, + _get_mixnodes_detailed, _get_rewarded_set_detailed, +}; +use crate::node_status_api::models::AxumResult; +use crate::v2::AxumAppState; +use axum::extract::{Path, State}; +use axum::Json; +use axum::Router; +use nym_api_requests::models::{ + AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, + MixnodeStatusResponse, StakeSaturationResponse, +}; +use nym_mixnet_contract_common::MixId; + +pub(super) fn mandatory_routes() -> Router { + Router::new() + .nest( + "/mixnode/:mix_id", + Router::new() + .route("/status", axum::routing::get(get_mixnode_status)) + .route( + "/stake-saturation", + axum::routing::get(get_mixnode_stake_saturation), + ) + .route( + "/inclusion-probability", + axum::routing::get(get_mixnode_inclusion_probability), + ), + ) + .merge( + Router::new().nest( + "/mixnodes", + Router::new() + .route( + "/inclusion-probability", + axum::routing::get(get_mixnode_inclusion_probabilities), + ) + .route("/detailed", axum::routing::get(get_mixnodes_detailed)) + .route( + "/rewarded/detailed", + axum::routing::get(get_rewarded_set_detailed), + ) + .route( + "/active/detailed", + axum::routing::get(get_active_set_detailed), + ), + ), + ) +} + +#[utoipa::path( + tag = "status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/status", + responses( + (status = 200, body = MixnodeStatusResponse) + ) +)] +async fn get_mixnode_status( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> Json { + Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) +} + +#[utoipa::path( + tag = "status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/stake-saturation", + responses( + (status = 200, body = StakeSaturationResponse) + ) +)] +async fn get_mixnode_stake_saturation( + Path(mix_id): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_mixnode_stake_saturation( + state.node_status_cache(), + state.nym_contract_cache(), + mix_id, + ) + .await?, + )) +} + +#[utoipa::path( + tag = "status", + get, + params( + MixIdParam + ), + path = "/v1/status/mixnode/{mix_id}/inclusion-probability", + responses( + (status = 200, body = InclusionProbabilityResponse) + ) +)] +async fn get_mixnode_inclusion_probability( + Path(mix_id): Path, + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, + )) +} + +#[utoipa::path( + tag = "status", + get, + path = "/v1/status/mixnodes/inclusion-probability", + responses( + (status = 200, body = AllInclusionProbabilitiesResponse) + ) +)] +async fn get_mixnode_inclusion_probabilities( + State(state): State, +) -> AxumResult> { + Ok(Json( + _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, + )) +} + +#[utoipa::path( + tag = "status", + get, + path = "/v1/status/mixnodes/detailed", + responses( + (status = 200, body = MixNodeBondAnnotated) + ) +)] +pub async fn get_mixnodes_detailed( + State(state): State, +) -> Json> { + Json(_get_mixnodes_detailed(state.node_status_cache()).await) +} + +#[utoipa::path( + tag = "status", + get, + path = "/v1/status/mixnodes/rewarded/detailed", + responses( + (status = 200, body = MixNodeBondAnnotated) + ) +)] +pub async fn get_rewarded_set_detailed( + State(state): State, +) -> Json> { + Json(_get_rewarded_set_detailed(state.node_status_cache()).await) +} + +#[utoipa::path( + tag = "status", + get, + path = "/v1/status/mixnodes/active/detailed", + responses( + (status = 200, body = MixNodeBondAnnotated) + ) +)] +pub async fn get_active_set_detailed( + State(state): State, +) -> Json> { + Json(_get_active_set_detailed(state.node_status_cache()).await) +} diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 9ac54bd363..0d706b1156 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -1,7 +1,8 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::ErrorResponse; +use super::reward_estimate::compute_reward_estimate; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; use crate::support::caching::Cache; use crate::{NodeStatusCache, NymContractCache}; @@ -15,41 +16,31 @@ use nym_api_requests::models::{ UptimeResponse, }; use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; -use rocket::http::Status; -use rocket::State; - -use super::reward_estimate::compute_reward_estimate; async fn get_gateway_bond_annotated( cache: &NodeStatusCache, identity: &str, -) -> Result { +) -> AxumResult { cache .gateway_annotated(identity) .await - .ok_or(ErrorResponse::new( - "gateway bond not found", - Status::NotFound, - )) + .ok_or(AxumErrorResponse::not_found("gateway bond not found")) } async fn get_mixnode_bond_annotated( cache: &NodeStatusCache, mix_id: MixId, -) -> Result { +) -> AxumResult { cache .mixnode_annotated(mix_id) .await - .ok_or(ErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) + .ok_or(AxumErrorResponse::not_found("mixnode bond not found")) } pub(crate) async fn _gateway_report( cache: &NodeStatusCache, identity: &str, -) -> Result { +) -> AxumResult { let gateway = get_gateway_bond_annotated(cache, identity).await?; Ok(GatewayStatusReportResponse { @@ -64,23 +55,23 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, identity: &str, -) -> Result { +) -> AxumResult { storage .get_gateway_uptime_history(identity) .await .map(GatewayUptimeHistoryResponse::from) - .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) + .map_err(AxumErrorResponse::not_found) } pub(crate) async fn _gateway_core_status_count( - storage: &State, + storage: &NymApiStorage, identity: &str, since: Option, -) -> Result { +) -> AxumResult { let count = storage .get_core_gateway_status_count(identity, since) .await - .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?; + .map_err(AxumErrorResponse::not_found)?; Ok(GatewayCoreStatusResponse { identity: identity.to_string(), @@ -91,7 +82,7 @@ pub(crate) async fn _gateway_core_status_count( pub(crate) async fn _mixnode_report( cache: &NodeStatusCache, mix_id: MixId, -) -> Result { +) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; Ok(MixnodeStatusReportResponse { @@ -107,23 +98,23 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, mix_id: MixId, -) -> Result { +) -> AxumResult { storage .get_mixnode_uptime_history(mix_id) .await .map(MixnodeUptimeHistoryResponse::from) - .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) + .map_err(AxumErrorResponse::not_found) } pub(crate) async fn _mixnode_core_status_count( - storage: &State, + storage: &NymApiStorage, mix_id: MixId, since: Option, -) -> Result { +) -> AxumResult { let count = storage .get_core_mixnode_status_count(mix_id, since) .await - .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?; + .map_err(AxumErrorResponse::not_found)?; Ok(MixnodeCoreStatusResponse { mix_id, count }) } @@ -138,22 +129,22 @@ pub(crate) async fn _get_mixnode_status( } pub(crate) async fn _get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, + cache: &NodeStatusCache, + validator_cache: &NymContractCache, mix_id: MixId, -) -> Result { +) -> AxumResult { let (mixnode, status) = cache.mixnode_details(mix_id).await; if let Some(mixnode) = mixnode { let reward_params = validator_cache.interval_reward_params().await; let as_at = reward_params.timestamp(); let reward_params = reward_params .into_inner() - .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; + .ok_or_else(AxumErrorResponse::internal)?; let current_interval = validator_cache .current_interval() .await .into_inner() - .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; + .ok_or_else(AxumErrorResponse::internal)?; let reward_estimation = compute_reward_estimate( &mixnode.mixnode_details, @@ -170,31 +161,28 @@ pub(crate) async fn _get_mixnode_reward_estimation( as_at: as_at.unix_timestamp(), }) } else { - Err(ErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) + Err(AxumErrorResponse::not_found("mixnode bond not found")) } } pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: ComputeRewardEstParam, + user_reward_param: &ComputeRewardEstParam, cache: &NodeStatusCache, validator_cache: &NymContractCache, mix_id: MixId, -) -> Result { +) -> AxumResult { let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; if let Some(mut mixnode) = mixnode { let reward_params = validator_cache.interval_reward_params().await; let as_at = reward_params.timestamp(); let reward_params = reward_params .into_inner() - .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; + .ok_or_else(AxumErrorResponse::internal)?; let current_interval = validator_cache .current_interval() .await .into_inner() - .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; + .ok_or_else(AxumErrorResponse::internal)?; // For these parameters we either use the provided ones, or fall back to the system ones let performance = user_reward_param.performance.unwrap_or(mixnode.performance); @@ -222,21 +210,20 @@ pub(crate) async fn _compute_mixnode_reward_estimation( .profit_margin_percent = profit_margin_percent; } - if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost { + if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { mixnode .mixnode_details .rewarding_details .cost_params - .interval_operating_cost = interval_operating_cost; + .interval_operating_cost = interval_operating_cost.clone(); } if mixnode.mixnode_details.rewarding_details.operator + mixnode.mixnode_details.rewarding_details.delegates > reward_params.interval.staking_supply { - return Err(ErrorResponse::new( + return Err(AxumErrorResponse::unprocessable_entity( "Pledge plus delegation too large", - Status::UnprocessableEntity, )); } @@ -255,10 +242,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation( as_at: as_at.unix_timestamp(), }) } else { - Err(ErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) + Err(AxumErrorResponse::not_found("mixnode bond not found")) } } @@ -266,7 +250,7 @@ pub(crate) async fn _get_mixnode_stake_saturation( cache: &NodeStatusCache, validator_cache: &NymContractCache, mix_id: MixId, -) -> Result { +) -> AxumResult { let (mixnode, _) = cache.mixnode_details(mix_id).await; if let Some(mixnode) = mixnode { // Recompute the stake saturation just so that we can confidently state that the `as_at` @@ -275,7 +259,7 @@ pub(crate) async fn _get_mixnode_stake_saturation( let as_at = reward_params.timestamp(); let rewarding_params = reward_params .into_inner() - .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; + .ok_or_else(AxumErrorResponse::internal)?; Ok(StakeSaturationResponse { saturation: mixnode @@ -289,17 +273,14 @@ pub(crate) async fn _get_mixnode_stake_saturation( as_at: as_at.unix_timestamp(), }) } else { - Err(ErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) + Err(AxumErrorResponse::not_found("mixnode bond not found")) } } pub(crate) async fn _get_mixnode_inclusion_probability( cache: &NodeStatusCache, mix_id: MixId, -) -> Result { +) -> AxumResult { cache .inclusion_probabilities() .await @@ -309,13 +290,13 @@ pub(crate) async fn _get_mixnode_inclusion_probability( in_active: p.in_active.into(), in_reserve: p.in_reserve.into(), }) - .ok_or_else(|| ErrorResponse::new("mixnode bond not found", Status::NotFound)) + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found")) } pub(crate) async fn _get_mixnode_avg_uptime( cache: &NodeStatusCache, mix_id: MixId, -) -> Result { +) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; Ok(UptimeResponse { @@ -328,7 +309,7 @@ pub(crate) async fn _get_mixnode_avg_uptime( pub(crate) async fn _get_gateway_avg_uptime( cache: &NodeStatusCache, identity: &str, -) -> Result { +) -> AxumResult { let gateway = get_gateway_bond_annotated(cache, identity).await?; Ok(GatewayUptimeResponse { @@ -340,7 +321,7 @@ pub(crate) async fn _get_gateway_avg_uptime( pub(crate) async fn _get_mixnode_inclusion_probabilities( cache: &NodeStatusCache, -) -> Result { +) -> AxumResult { if let Some(prob) = cache.inclusion_probabilities().await { let as_at = prob.timestamp(); let prob = prob.into_inner(); @@ -353,10 +334,7 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities( as_at: as_at.unix_timestamp(), }) } else { - Err(ErrorResponse::new( - "No data available", - Status::ServiceUnavailable, - )) + Err(AxumErrorResponse::service_unavailable()) } } diff --git a/nym-api/src/node_status_api/helpers_deprecated.rs b/nym-api/src/node_status_api/helpers_deprecated.rs new file mode 100644 index 0000000000..0788cd2cb3 --- /dev/null +++ b/nym-api/src/node_status_api/helpers_deprecated.rs @@ -0,0 +1,405 @@ +// Copyright 2021-2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::RocketErrorResponse; +use crate::storage::NymApiStorage; +use crate::support::caching::Cache; +use crate::{NodeStatusCache, NymContractCache}; +use cosmwasm_std::Decimal; +use nym_api_requests::models::{ + AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, + GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, + GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, + MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, + UptimeResponse, +}; +use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; +use rocket::http::Status; +use rocket::State; + +use super::reward_estimate::compute_reward_estimate; + +async fn get_gateway_bond_annotated( + cache: &NodeStatusCache, + identity: &str, +) -> Result { + cache + .gateway_annotated(identity) + .await + .ok_or(RocketErrorResponse::new( + "gateway bond not found", + Status::NotFound, + )) +} + +async fn get_mixnode_bond_annotated( + cache: &NodeStatusCache, + mix_id: MixId, +) -> Result { + cache + .mixnode_annotated(mix_id) + .await + .ok_or(RocketErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) +} + +pub(crate) async fn _gateway_report( + cache: &NodeStatusCache, + identity: &str, +) -> Result { + let gateway = get_gateway_bond_annotated(cache, identity).await?; + + Ok(GatewayStatusReportResponse { + identity: gateway.identity().to_owned(), + owner: gateway.owner().to_string(), + most_recent: gateway.node_performance.most_recent.round_to_integer(), + last_hour: gateway.node_performance.last_hour.round_to_integer(), + last_day: gateway.node_performance.last_24h.round_to_integer(), + }) +} + +pub(crate) async fn _gateway_uptime_history( + storage: &NymApiStorage, + identity: &str, +) -> Result { + storage + .get_gateway_uptime_history(identity) + .await + .map(GatewayUptimeHistoryResponse::from) + .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) +} + +pub(crate) async fn _gateway_core_status_count( + storage: &State, + identity: &str, + since: Option, +) -> Result { + let count = storage + .get_core_gateway_status_count(identity, since) + .await + .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; + + Ok(GatewayCoreStatusResponse { + identity: identity.to_string(), + count, + }) +} + +pub(crate) async fn _mixnode_report( + cache: &NodeStatusCache, + mix_id: MixId, +) -> Result { + let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; + + Ok(MixnodeStatusReportResponse { + mix_id, + identity: mixnode.identity_key().to_owned(), + owner: mixnode.owner().to_string(), + most_recent: mixnode.node_performance.most_recent.round_to_integer(), + last_hour: mixnode.node_performance.last_hour.round_to_integer(), + last_day: mixnode.node_performance.last_24h.round_to_integer(), + }) +} + +pub(crate) async fn _mixnode_uptime_history( + storage: &NymApiStorage, + mix_id: MixId, +) -> Result { + storage + .get_mixnode_uptime_history(mix_id) + .await + .map(MixnodeUptimeHistoryResponse::from) + .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) +} + +pub(crate) async fn _mixnode_core_status_count( + storage: &State, + mix_id: MixId, + since: Option, +) -> Result { + let count = storage + .get_core_mixnode_status_count(mix_id, since) + .await + .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; + + Ok(MixnodeCoreStatusResponse { mix_id, count }) +} + +pub(crate) async fn _get_mixnode_status( + cache: &NymContractCache, + mix_id: MixId, +) -> MixnodeStatusResponse { + MixnodeStatusResponse { + status: cache.mixnode_status(mix_id).await, + } +} + +pub(crate) async fn _get_mixnode_reward_estimation( + cache: &State, + validator_cache: &State, + mix_id: MixId, +) -> Result { + let (mixnode, status) = cache.mixnode_details(mix_id).await; + if let Some(mixnode) = mixnode { + let reward_params = validator_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; + let current_interval = validator_cache + .current_interval() + .await + .into_inner() + .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + mixnode.performance, + status.into(), + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) + } else { + Err(RocketErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} + +pub(crate) async fn _compute_mixnode_reward_estimation( + user_reward_param: ComputeRewardEstParam, + cache: &NodeStatusCache, + validator_cache: &NymContractCache, + mix_id: MixId, +) -> Result { + let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; + if let Some(mut mixnode) = mixnode { + let reward_params = validator_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; + let current_interval = validator_cache + .current_interval() + .await + .into_inner() + .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; + + // For these parameters we either use the provided ones, or fall back to the system ones + let performance = user_reward_param.performance.unwrap_or(mixnode.performance); + + let status = match user_reward_param.active_in_rewarded_set { + Some(true) => Some(RewardedSetNodeStatus::Active), + Some(false) => Some(RewardedSetNodeStatus::Standby), + None => actual_status.into(), + }; + + if let Some(pledge_amount) = user_reward_param.pledge_amount { + mixnode.mixnode_details.rewarding_details.operator = + Decimal::from_ratio(pledge_amount, 1u64); + } + if let Some(total_delegation) = user_reward_param.total_delegation { + mixnode.mixnode_details.rewarding_details.delegates = + Decimal::from_ratio(total_delegation, 1u64); + } + + if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .profit_margin_percent = profit_margin_percent; + } + + if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .interval_operating_cost = interval_operating_cost; + } + + if mixnode.mixnode_details.rewarding_details.operator + + mixnode.mixnode_details.rewarding_details.delegates + > reward_params.interval.staking_supply + { + return Err(RocketErrorResponse::new( + "Pledge plus delegation too large", + Status::UnprocessableEntity, + )); + } + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + performance, + status, + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) + } else { + Err(RocketErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} + +pub(crate) async fn _get_mixnode_stake_saturation( + cache: &NodeStatusCache, + validator_cache: &NymContractCache, + mix_id: MixId, +) -> Result { + let (mixnode, _) = cache.mixnode_details(mix_id).await; + if let Some(mixnode) = mixnode { + // Recompute the stake saturation just so that we can confidently state that the `as_at` + // field is consistent and correct. Luckily this is very cheap. + let reward_params = validator_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let rewarding_params = reward_params + .into_inner() + .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; + + Ok(StakeSaturationResponse { + saturation: mixnode + .mixnode_details + .rewarding_details + .bond_saturation(&rewarding_params), + uncapped_saturation: mixnode + .mixnode_details + .rewarding_details + .uncapped_bond_saturation(&rewarding_params), + as_at: as_at.unix_timestamp(), + }) + } else { + Err(RocketErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} + +pub(crate) async fn _get_mixnode_inclusion_probability( + cache: &NodeStatusCache, + mix_id: MixId, +) -> Result { + cache + .inclusion_probabilities() + .await + .map(Cache::into_inner) + .and_then(|p| p.node(mix_id).cloned()) + .map(|p| InclusionProbabilityResponse { + in_active: p.in_active.into(), + in_reserve: p.in_reserve.into(), + }) + .ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound)) +} + +pub(crate) async fn _get_mixnode_avg_uptime( + cache: &NodeStatusCache, + mix_id: MixId, +) -> Result { + let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; + + Ok(UptimeResponse { + mix_id, + avg_uptime: mixnode.node_performance.last_24h.round_to_integer(), + node_performance: mixnode.node_performance, + }) +} + +pub(crate) async fn _get_gateway_avg_uptime( + cache: &NodeStatusCache, + identity: &str, +) -> Result { + let gateway = get_gateway_bond_annotated(cache, identity).await?; + + Ok(GatewayUptimeResponse { + identity: identity.to_string(), + avg_uptime: gateway.node_performance.last_24h.round_to_integer(), + node_performance: gateway.node_performance, + }) +} + +pub(crate) async fn _get_mixnode_inclusion_probabilities( + cache: &NodeStatusCache, +) -> Result { + if let Some(prob) = cache.inclusion_probabilities().await { + let as_at = prob.timestamp(); + let prob = prob.into_inner(); + Ok(AllInclusionProbabilitiesResponse { + inclusion_probabilities: prob.inclusion_probabilities, + samples: prob.samples, + elapsed: prob.elapsed, + delta_max: prob.delta_max, + delta_l2: prob.delta_l2, + as_at: as_at.unix_timestamp(), + }) + } else { + Err(RocketErrorResponse::new( + "No data available", + Status::ServiceUnavailable, + )) + } +} + +pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { + cache + .mixnodes_annotated_filtered() + .await + .unwrap_or_default() +} + +pub(crate) async fn _get_mixnodes_detailed_unfiltered( + cache: &NodeStatusCache, +) -> Vec { + cache.mixnodes_annotated_full().await.unwrap_or_default() +} + +pub(crate) async fn _get_rewarded_set_detailed( + cache: &NodeStatusCache, +) -> Vec { + cache + .rewarded_set_annotated() + .await + .unwrap_or_default() + .into_inner() +} + +pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { + cache + .active_set_annotated() + .await + .unwrap_or_default() + .into_inner() +} + +pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { + cache + .gateways_annotated_filtered() + .await + .unwrap_or_default() +} + +pub(crate) async fn _get_gateways_detailed_unfiltered( + cache: &NodeStatusCache, +) -> Vec { + cache.gateways_annotated_full().await.unwrap_or_default() +} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 4800b5957a..5f50e62964 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -15,10 +15,14 @@ use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; pub(crate) mod cache; +#[cfg(feature = "axum")] +pub(crate) mod handlers; +#[cfg(feature = "axum")] pub(crate) mod helpers; +pub(crate) mod helpers_deprecated; pub(crate) mod models; pub(crate) mod reward_estimate; -pub(crate) mod routes; +pub(crate) mod routes_deprecated; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -32,42 +36,42 @@ pub(crate) fn node_status_routes( ) -> (Vec, OpenApi) { if enabled { openapi_get_routes_spec![ - settings: routes::gateway_report, - routes::gateway_uptime_history, - routes::gateway_core_status_count, - routes::mixnode_report, - routes::mixnode_uptime_history, - routes::mixnode_core_status_count, - routes::get_mixnode_status, - routes::get_mixnode_reward_estimation, - routes::compute_mixnode_reward_estimation, - routes::get_mixnode_stake_saturation, - routes::get_mixnode_inclusion_probability, - routes::get_mixnode_avg_uptime, - routes::get_gateway_avg_uptime, - routes::get_mixnode_inclusion_probabilities, - routes::get_mixnodes_detailed, - routes::get_mixnodes_detailed_unfiltered, - routes::get_rewarded_set_detailed, - routes::get_active_set_detailed, - routes::get_gateways_detailed, - routes::get_gateways_detailed_unfiltered, - routes::unstable::mixnode_test_results, - routes::unstable::gateway_test_results, - routes::submit_gateway_monitoring_results, - routes::submit_node_monitoring_results, + settings: routes_deprecated::gateway_report, + routes_deprecated::gateway_uptime_history, + routes_deprecated::gateway_core_status_count, + routes_deprecated::mixnode_report, + routes_deprecated::mixnode_uptime_history, + routes_deprecated::mixnode_core_status_count, + routes_deprecated::get_mixnode_status, + routes_deprecated::get_mixnode_reward_estimation, + routes_deprecated::compute_mixnode_reward_estimation, + routes_deprecated::get_mixnode_stake_saturation, + routes_deprecated::get_mixnode_inclusion_probability, + routes_deprecated::get_mixnode_avg_uptime, + routes_deprecated::get_gateway_avg_uptime, + routes_deprecated::get_mixnode_inclusion_probabilities, + routes_deprecated::get_mixnodes_detailed, + routes_deprecated::get_mixnodes_detailed_unfiltered, + routes_deprecated::get_rewarded_set_detailed, + routes_deprecated::get_active_set_detailed, + routes_deprecated::get_gateways_detailed, + routes_deprecated::get_gateways_detailed_unfiltered, + routes_deprecated::unstable::mixnode_test_results, + routes_deprecated::unstable::gateway_test_results, + routes_deprecated::submit_gateway_monitoring_results, + routes_deprecated::submit_node_monitoring_results, ] } else { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage openapi_get_routes_spec![ - settings: routes::get_mixnode_status, - routes::get_mixnode_stake_saturation, - routes::get_mixnode_inclusion_probability, - routes::get_mixnode_inclusion_probabilities, - routes::get_mixnodes_detailed, - routes::get_rewarded_set_detailed, - routes::get_active_set_detailed, + settings: routes_deprecated::get_mixnode_status, + routes_deprecated::get_mixnode_stake_saturation, + routes_deprecated::get_mixnode_inclusion_probability, + routes_deprecated::get_mixnode_inclusion_probabilities, + routes_deprecated::get_mixnodes_detailed, + routes_deprecated::get_rewarded_set_detailed, + routes_deprecated::get_active_set_detailed, ] } } @@ -80,7 +84,7 @@ pub(crate) fn start_cache_refresh( config: &config::NodeStatusAPI, nym_contract_cache_state: &NymContractCache, node_status_cache_state: &NodeStatusCache, - storage: Option<&storage::NymApiStorage>, + storage: storage::NymApiStorage, nym_contract_cache_listener: tokio::sync::watch::Receiver, shutdown: &TaskManager, ) { @@ -89,7 +93,7 @@ pub(crate) fn start_cache_refresh( config.debug.caching_interval, nym_contract_cache_state.to_owned(), nym_contract_cache_listener, - storage.cloned(), + storage, ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index d0c172d7a3..480fae65d2 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -330,21 +330,22 @@ impl From for HistoricalUptimeResponse { } } -pub(crate) struct ErrorResponse { +#[deprecated(note = "TODO rocket remove once Rocket is phased out")] +pub(crate) struct RocketErrorResponse { error_message: RequestError, status: Status, } -impl ErrorResponse { +impl RocketErrorResponse { pub(crate) fn new(error_message: impl Into, status: Status) -> Self { - ErrorResponse { + RocketErrorResponse { error_message: RequestError::new(error_message), status, } } } -impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { +impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { // piggyback on the existing implementation // also prefer json over plain for ease of use in frontend @@ -355,7 +356,7 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { } } -impl JsonSchema for ErrorResponse { +impl JsonSchema for RocketErrorResponse { fn schema_name() -> String { "ErrorResponse".to_owned() } @@ -384,7 +385,7 @@ impl JsonSchema for ErrorResponse { } } -impl OpenApiResponderInner for ErrorResponse { +impl OpenApiResponderInner for RocketErrorResponse { fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { let mut responses = Responses::default(); ensure_status_code_exists(&mut responses, 404); @@ -392,6 +393,103 @@ impl OpenApiResponderInner for ErrorResponse { } } +#[cfg(feature = "axum")] +pub(crate) use axum_error::{AxumErrorResponse, AxumResult}; + +#[cfg(feature = "axum")] +/// TODO rocket: extract types from this module when axum becomes the only server in Nym API +mod axum_error { + pub use super::*; + use crate::ecash::error::{EcashError, RedemptionError}; + use std::fmt::Display; + + // TODO rocket remove smurf name after eliminating `rocket` + pub(crate) type AxumResult = Result; + pub(crate) struct AxumErrorResponse { + message: RequestError, + status: axum::http::StatusCode, + } + + impl AxumErrorResponse { + pub(crate) fn internal_msg(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn internal() -> Self { + Self { + message: RequestError::new("Internal server error"), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn not_implemented() -> Self { + Self { + message: RequestError::empty(), + status: axum::http::StatusCode::NOT_IMPLEMENTED, + } + } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: axum::http::StatusCode::NOT_FOUND, + } + } + + pub(crate) fn service_unavailable() -> Self { + Self { + message: RequestError::empty(), + status: axum::http::StatusCode::SERVICE_UNAVAILABLE, + } + } + + pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: axum::http::StatusCode::UNPROCESSABLE_ENTITY, + } + } + } + + impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message().to_string()).into_response() + } + } + + impl From for AxumErrorResponse { + fn from(value: NymApiStorageError) -> Self { + error!("{value}"); + Self { + message: RequestError::empty(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + } + + impl From for AxumErrorResponse { + fn from(value: EcashError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: axum::http::StatusCode::BAD_REQUEST, + } + } + } + + #[cfg(feature = "axum")] + impl From for AxumErrorResponse { + fn from(value: RedemptionError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: axum::http::StatusCode::BAD_REQUEST, + } + } + } +} + #[derive(Debug, thiserror::Error)] pub enum NymApiStorageError { #[error("could not find status report associated with mixnode {mix_id}")] diff --git a/nym-api/src/node_status_api/routes.rs b/nym-api/src/node_status_api/routes_deprecated.rs similarity index 91% rename from nym-api/src/node_status_api/routes.rs rename to nym-api/src/node_status_api/routes_deprecated.rs index 19b6462d1d..29ab89fb4d 100644 --- a/nym-api/src/node_status_api/routes.rs +++ b/nym-api/src/node_status_api/routes_deprecated.rs @@ -15,9 +15,9 @@ use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; -use super::helpers::_get_gateways_detailed; +use super::helpers_deprecated::_get_gateways_detailed; use super::NodeStatusCache; -use crate::node_status_api::helpers::{ +use crate::node_status_api::helpers_deprecated::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, _gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime, _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, @@ -26,7 +26,7 @@ use crate::node_status_api::helpers::{ _get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed, _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, }; -use crate::node_status_api::models::ErrorResponse; +use crate::node_status_api::models::RocketErrorResponse; use crate::storage::NymApiStorage; use crate::NymContractCache; @@ -35,23 +35,23 @@ use crate::NymContractCache; pub(crate) async fn submit_gateway_monitoring_results( message: Json, storage: &State, -) -> Result<(), ErrorResponse> { +) -> Result<(), RocketErrorResponse> { if !message.from_allowed() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Monitor not registered to submit results".to_string(), rocket::http::Status::Forbidden, )); } if !message.timely() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Message is too old".to_string(), rocket::http::Status::BadRequest, )); } if !message.verify() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Invalid signature".to_string(), rocket::http::Status::BadRequest, )); @@ -65,7 +65,7 @@ pub(crate) async fn submit_gateway_monitoring_results( Ok(_) => Ok(()), Err(err) => { error!("failed to submit gateway monitoring results: {}", err); - Err(ErrorResponse::new( + Err(RocketErrorResponse::new( "failed to submit gateway monitoring results".to_string(), rocket::http::Status::InternalServerError, )) @@ -78,23 +78,23 @@ pub(crate) async fn submit_gateway_monitoring_results( pub(crate) async fn submit_node_monitoring_results( message: Json, storage: &State, -) -> Result<(), ErrorResponse> { +) -> Result<(), RocketErrorResponse> { if !message.from_allowed() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Monitor not registered to submit results".to_string(), rocket::http::Status::Forbidden, )); } if !message.timely() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Message is too old".to_string(), rocket::http::Status::BadRequest, )); } if !message.verify() { - return Err(ErrorResponse::new( + return Err(RocketErrorResponse::new( "Invalid signature".to_string(), rocket::http::Status::BadRequest, )); @@ -108,7 +108,7 @@ pub(crate) async fn submit_node_monitoring_results( Ok(_) => Ok(()), Err(err) => { error!("failed to submit node monitoring results: {}", err); - Err(ErrorResponse::new( + Err(RocketErrorResponse::new( "failed to submit node monitoring results".to_string(), rocket::http::Status::InternalServerError, )) @@ -121,7 +121,7 @@ pub(crate) async fn submit_node_monitoring_results( pub(crate) async fn gateway_report( cache: &State, identity: &str, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_gateway_report(cache, identity).await?)) } @@ -130,7 +130,7 @@ pub(crate) async fn gateway_report( pub(crate) async fn gateway_uptime_history( storage: &State, identity: &str, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_gateway_uptime_history(storage, identity).await?)) } @@ -140,7 +140,7 @@ pub(crate) async fn gateway_core_status_count( storage: &State, identity: &str, since: Option, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _gateway_core_status_count(storage, identity, since).await?, )) @@ -151,7 +151,7 @@ pub(crate) async fn gateway_core_status_count( pub(crate) async fn mixnode_report( cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_mixnode_report(cache, mix_id).await?)) } @@ -160,7 +160,7 @@ pub(crate) async fn mixnode_report( pub(crate) async fn mixnode_uptime_history( storage: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) } @@ -170,7 +170,7 @@ pub(crate) async fn mixnode_core_status_count( storage: &State, mix_id: MixId, since: Option, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _mixnode_core_status_count(storage, mix_id, since).await?, )) @@ -191,7 +191,7 @@ pub(crate) async fn get_mixnode_reward_estimation( cache: &State, validator_cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, )) @@ -207,7 +207,7 @@ pub(crate) async fn compute_mixnode_reward_estimation( cache: &State, validator_cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _compute_mixnode_reward_estimation( user_reward_param.into_inner(), @@ -225,7 +225,7 @@ pub(crate) async fn get_mixnode_stake_saturation( cache: &State, validator_cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, )) @@ -236,7 +236,7 @@ pub(crate) async fn get_mixnode_stake_saturation( pub(crate) async fn get_mixnode_inclusion_probability( cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json( _get_mixnode_inclusion_probability(cache, mix_id).await?, )) @@ -247,7 +247,7 @@ pub(crate) async fn get_mixnode_inclusion_probability( pub(crate) async fn get_mixnode_avg_uptime( cache: &State, mix_id: MixId, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?)) } @@ -256,7 +256,7 @@ pub(crate) async fn get_mixnode_avg_uptime( pub(crate) async fn get_gateway_avg_uptime( cache: &State, identity: &str, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) } @@ -264,7 +264,7 @@ pub(crate) async fn get_gateway_avg_uptime( #[get("/mixnodes/inclusion_probability")] pub(crate) async fn get_mixnode_inclusion_probabilities( cache: &State, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) } @@ -317,7 +317,7 @@ pub async fn get_gateways_detailed_unfiltered( } pub mod unstable { - use crate::node_status_api::models::ErrorResponse; + use crate::node_status_api::models::RocketErrorResponse; use crate::support::http::helpers::PaginationRequest; use crate::support::storage::NymApiStorage; use nym_api_requests::models::{ @@ -486,7 +486,7 @@ pub mod unstable { pagination: PaginationRequest, info_cache: &State, storage: &State, - ) -> Result, ErrorResponse> { + ) -> Result, RocketErrorResponse> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -497,7 +497,7 @@ pub mod unstable { match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await { Ok(res) => Ok(Json(res)), - Err(err) => Err(ErrorResponse::new( + Err(err) => Err(RocketErrorResponse::new( format!("failed to retrieve mixnode test results for node {mix_id}: {err}"), Status::InternalServerError, )), @@ -570,7 +570,7 @@ pub mod unstable { pagination: PaginationRequest, info_cache: &State, storage: &State, - ) -> Result, ErrorResponse> { + ) -> Result, RocketErrorResponse> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -581,7 +581,7 @@ pub mod unstable { match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await { Ok(res) => Ok(Json(res)), - Err(err) => Err(ErrorResponse::new( + Err(err) => Err(RocketErrorResponse::new( format!( "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" ), diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index fc09185b53..2a33a0118f 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -118,8 +118,8 @@ impl HistoricalUptimeUpdater { } } - pub(crate) fn start(storage: &NymApiStorage, shutdown: &TaskManager) { - let uptime_updater = HistoricalUptimeUpdater::new(storage.to_owned()); + pub(crate) fn start(storage: NymApiStorage, shutdown: &TaskManager) { + let uptime_updater = HistoricalUptimeUpdater::new(storage); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { uptime_updater.run(shutdown_listener).await }); } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 811e44f347..efaeac74a6 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -31,13 +31,14 @@ pub struct NymContractCache { } impl NymContractCache { - fn new() -> Self { + pub(crate) fn new() -> Self { NymContractCache { initialised: Arc::new(AtomicBool::new(false)), inner: Arc::new(RwLock::new(ValidatorCacheData::new())), } } + #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] pub fn stage() -> AdHoc { AdHoc::on_ignite("Validator Cache Stage", |rocket| async { rocket.manage(Self::new()) diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs new file mode 100644 index 0000000000..84f2c70fd2 --- /dev/null +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -0,0 +1,270 @@ +// Copyright 2021-2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + node_status_api::helpers_deprecated::{ + _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, + }, + v2::AxumAppState, +}; +use axum::{extract, Router}; +use nym_api_requests::models::MixNodeBondAnnotated; +use nym_mixnet_contract_common::{ + mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, +}; +use std::collections::HashSet; + +pub(crate) fn nym_contract_cache_routes() -> Router { + Router::new() + .route("/mixnodes", axum::routing::get(get_mixnodes)) + .route( + "/mixnodes/detailed", + axum::routing::get(get_mixnodes_detailed), + ) + .route("/gateways", axum::routing::get(get_gateways)) + .route("/mixnodes/rewarded", axum::routing::get(get_rewarded_set)) + .route( + "/mixnodes/rewarded/detailed", + axum::routing::get(get_rewarded_set_detailed), + ) + .route("/mixnodes/active", axum::routing::get(get_active_set)) + .route( + "/mixnodes/active/detailed", + axum::routing::get(get_active_set_detailed), + ) + .route( + "/mixnodes/blacklisted", + axum::routing::get(get_blacklisted_mixnodes), + ) + .route( + "/gateways/blacklisted", + axum::routing::get(get_blacklisted_gateways), + ) + .route( + "/epoch/reward_params", + axum::routing::get(get_interval_reward_params), + ) + .route("/epoch/current", axum::routing::get(get_current_epoch)) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_mixnodes( + extract::State(state): extract::State, +) -> axum::Json> { + state.nym_contract_cache().mixnodes_filtered().await.into() +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_mixnodes_detailed() -> Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/detailed")) +// } +// ``` +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/detailed", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_mixnodes_detailed( + extract::State(state): extract::State, +) -> axum::Json> { + _get_mixnodes_detailed(state.node_status_cache()) + .await + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/gateways", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_gateways( + extract::State(state): extract::State, +) -> axum::Json> { + state.nym_contract_cache().gateways_filtered().await.into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/rewarded", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_rewarded_set( + extract::State(state): extract::State, +) -> axum::Json> { + state + .nym_contract_cache() + .rewarded_set() + .await + .to_owned() + .into() +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_mixnodes_set_detailed() -> Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) +// } +// ``` +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/rewarded/detailed", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_rewarded_set_detailed( + extract::State(state): extract::State, +) -> axum::Json> { + _get_rewarded_set_detailed(state.node_status_cache()) + .await + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/active", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_active_set( + extract::State(state): extract::State, +) -> axum::Json> { + state + .nym_contract_cache() + .active_set() + .await + .to_owned() + .into() +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_active_set_detailed() -> Redirect { +// Redirect::to(uri!("/status/mixnodes/active/detailed")) +// } +// ``` + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/active/detailed", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_active_set_detailed( + extract::State(state): extract::State, +) -> axum::Json> { + _get_active_set_detailed(state.node_status_cache()) + .await + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/blacklisted", + responses( + (status = 200, body = Option>) + ) +)] +async fn get_blacklisted_mixnodes( + extract::State(state): extract::State, +) -> axum::Json>> { + let blacklist = state + .nym_contract_cache() + .mixnodes_blacklist() + .await + .to_owned(); + if blacklist.is_empty() { + None + } else { + Some(blacklist) + } + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/gateways/blacklisted", + responses( + (status = 200, body = Option>) + ) +)] +async fn get_blacklisted_gateways( + extract::State(state): extract::State, +) -> axum::Json>> { + let blacklist = state + .nym_contract_cache() + .gateways_blacklist() + .await + .to_owned(); + if blacklist.is_empty() { + None + } else { + Some(blacklist) + } + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/epoch/reward_params", + responses( + (status = 200, body = Option) + ) +)] +async fn get_interval_reward_params( + extract::State(state): extract::State, +) -> axum::Json> { + state + .nym_contract_cache() + .interval_reward_params() + .await + .to_owned() + .into() +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/epoch/current", + responses( + (status = 200, body = Option) + ) +)] +async fn get_current_epoch( + extract::State(state): extract::State, +) -> axum::Json> { + state + .nym_contract_cache() + .current_interval() + .await + .to_owned() + .into() +} diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs index 15d861e51e..9b1f48d9ad 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/nym_contract_cache/mod.rs @@ -12,6 +12,8 @@ use rocket_okapi::settings::OpenApiSettings; use self::cache::refresher::NymContractCacheRefresher; pub(crate) mod cache; +#[cfg(feature = "axum")] +pub(crate) mod handlers; pub(crate) mod routes; pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs index ec2d61b2e4..cef7f04c9c 100644 --- a/nym-api/src/nym_contract_cache/routes.rs +++ b/nym-api/src/nym_contract_cache/routes.rs @@ -3,7 +3,9 @@ use crate::{ node_status_api::{ - helpers::{_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed}, + helpers_deprecated::{ + _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, + }, NodeStatusCache, }, nym_contract_cache::cache::NymContractCache, diff --git a/nym-api/src/nym_nodes/handlers.rs b/nym-api/src/nym_nodes/handlers.rs new file mode 100644 index 0000000000..7f775d8de4 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers.rs @@ -0,0 +1,95 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::v2::AxumAppState; +use axum::{extract::State, Json, Router}; +use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; +use nym_mixnet_contract_common::MixNodeBond; +use std::ops::Deref; + +// obviously this should get refactored later on because gateways will go away. +// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD + +pub(crate) fn nym_node_routes() -> axum::Router { + Router::new() + .route( + "/gateways/described", + axum::routing::get(get_gateways_described), + ) + .route( + "/mixnodes/described", + axum::routing::get(get_mixnodes_described), + ) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/v1/gateways/described", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_gateways_described(State(state): State) -> Json> { + let gateways = state.nym_contract_cache().gateways_filtered().await; + if gateways.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = state.described_nodes_state().get().await else { + return Json(gateways.into_iter().map(Into::into).collect()); + }; + + // TODO: this is extremely inefficient, but given we don't have many gateways, + // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, + // but at that point (I hope) the whole caching situation should get refactored + Json( + gateways + .into_iter() + .map(|bond| DescribedGateway { + self_described: self_descriptions.deref().get(bond.identity()).cloned(), + bond, + }) + .collect(), + ) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/v1/mixnodes/described", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_mixnodes_described(State(state): State) -> Json> { + let mixnodes = state + .nym_contract_cache() + .mixnodes_filtered() + .await + .into_iter() + .map(|m| m.bond_information) + .collect::>(); + if mixnodes.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = state.described_nodes_state().get().await else { + return Json(mixnodes.into_iter().map(Into::into).collect()); + }; + + // TODO: this is extremely inefficient, but given we don't have many gateways, + // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, + // but at that point (I hope) the whole caching situation should get refactored + Json( + mixnodes + .into_iter() + .map(|bond| DescribedMixNode { + self_described: self_descriptions.deref().get(bond.identity()).cloned(), + bond, + }) + .collect(), + ) +} diff --git a/nym-api/src/nym_nodes/handlers_unstable.rs b/nym-api/src/nym_nodes/handlers_unstable.rs new file mode 100644 index 0000000000..f0a7d8cc8b --- /dev/null +++ b/nym-api/src/nym_nodes/handlers_unstable.rs @@ -0,0 +1,377 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! All routes/nodes are split into three tiers: +//! +//! `/skimmed` +//! - used by clients +//! - returns the very basic information for routing purposes +//! +//! `/semi-skimmed` +//! - used by other nodes/VPN +//! - returns more additional information such noise keys +//! +//! `/full-fat` +//! - used by explorers, et al. +//! - returns almost everything there is about the nodes +//! +//! There's also additional split based on the role: +//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) +//! - `/mixnodes/` => only returns mixnode role data +//! - `/gateway/` => only returns (entry) gateway role data + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::v2::AxumAppState; +use axum::extract::Query; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::nym_nodes::{ + CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, +}; +use nym_bin_common::version_checker; +use serde::Deserialize; +use std::cmp::min; +use std::ops::Deref; + +pub(crate) fn nym_node_routes_unstable() -> axum::Router { + Router::new() + .route("/skimmed", axum::routing::get(nodes_basic)) + .route("/semi-skimmed", axum::routing::get(nodes_expanded)) + .route("/full-fat", axum::routing::get(nodes_detailed)) + .nest( + "/gateways", + Router::new() + .route("/skimmed", axum::routing::get(gateways_basic)) + .route("/semi-skimmed", axum::routing::get(gateways_expanded)) + .route("/full-fat", axum::routing::get(gateways_detailed)), + ) + .nest( + "/mixnodes", + Router::new() + .route("/skimmed", axum::routing::get(mixnodes_basic)) + .route("/semi-skimmed", axum::routing::get(mixnodes_expanded)) + .route("/full-fat", axum::routing::get(mixnodes_detailed)), + ) +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +struct NodesParams { + #[param(inline)] + role: Option, + semver_compatibility: Option, +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +struct SemverCompatibilityQueryParam { + semver_compatibility: Option, +} + +impl SemverCompatibilityQueryParam { + pub fn new(semver_compatibility: Option) -> Self { + Self { + semver_compatibility, + } + } +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn nodes_basic( + state: State, + Query(NodesParams { + role, + semver_compatibility, + }): Query, +) -> AxumResult>> { + if let Some(role) = role { + match role { + NodeRoleQueryParam::ActiveMixnode => { + return mixnodes_basic( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await + } + NodeRoleQueryParam::EntryGateway => { + return gateways_basic( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await; + } + _ => {} + } + } + + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/v1/unstable/nym-nodes/semi-skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn nodes_expanded( + state: State, + Query(NodesParams { + role, + semver_compatibility, + }): Query, +) -> AxumResult>> { + if let Some(role) = role { + match role { + NodeRoleQueryParam::ActiveMixnode => { + return mixnodes_expanded( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await + } + NodeRoleQueryParam::EntryGateway => { + return gateways_expanded( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await + } + _ => {} + } + } + + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/v1/unstable/nym-nodes/full-fat", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn nodes_detailed( + state: State, + Query(NodesParams { + role, + semver_compatibility, + }): Query, +) -> AxumResult>> { + if let Some(role) = role { + match role { + NodeRoleQueryParam::ActiveMixnode => { + return mixnodes_detailed( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await + } + NodeRoleQueryParam::EntryGateway => { + return gateways_detailed( + state, + Query(SemverCompatibilityQueryParam::new(semver_compatibility)), + ) + .await + } + _ => {} + } + } + + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/gateways/skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn gateways_basic( + state: State, + Query(SemverCompatibilityQueryParam { + semver_compatibility, + }): Query, +) -> AxumResult>> { + let status_cache = state.node_status_cache(); + let describe_cache = state.described_nodes_state(); + let gateways_cache = + status_cache + .gateways_cache() + .await + .ok_or(AxumErrorResponse::internal_msg( + "could not obtain gateways cache", + ))?; + + if gateways_cache.is_empty() { + return Ok(Json(CachedNodesResponse { + refreshed_at: gateways_cache.timestamp().into(), + nodes: vec![], + })); + } + + // if the self describe cache is unavailable don't try to use self-describe data + let Ok(self_descriptions) = describe_cache.get().await else { + return Ok(Json(CachedNodesResponse { + refreshed_at: gateways_cache.timestamp().into(), + nodes: gateways_cache.values().map(Into::into).collect(), + })); + }; + + let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); + + // the same comment holds as with `get_gateways_described`. + // this is inefficient and will have to get refactored with directory v3 + Ok(Json(CachedNodesResponse { + refreshed_at: refreshed_at.into(), + nodes: gateways_cache + .values() + .filter(|annotated_bond| { + if let Some(semver_compatibility) = semver_compatibility.as_ref() { + version_checker::is_minor_version_compatible( + &annotated_bond.gateway_bond.gateway.version, + semver_compatibility, + ) + } else { + true + } + }) + .map(|annotated_bond| { + SkimmedNode::from_described_gateway( + annotated_bond, + self_descriptions.deref().get(annotated_bond.identity()), + ) + }) + .collect(), + })) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/gateways/semi-skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn gateways_expanded( + State(_state): State, + Query(SemverCompatibilityQueryParam { + semver_compatibility: _semver_compatibility, + }): Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/gateways/full-fat", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn gateways_detailed( + State(_state): State, + Query(SemverCompatibilityQueryParam { + semver_compatibility: _semver_compatibility, + }): Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/mixnodes/skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn mixnodes_basic( + state: State, + Query(SemverCompatibilityQueryParam { + semver_compatibility, + }): Query, +) -> AxumResult>> { + let mixnodes_cache = state + .node_status_cache() + .active_mixnodes_cache() + .await + .ok_or(AxumErrorResponse::internal_msg( + "could not obtain mixnodes cache", + ))?; + Ok(Json(CachedNodesResponse { + refreshed_at: mixnodes_cache.timestamp().into(), + nodes: mixnodes_cache + .iter() + .filter(|annotated_bond| { + if let Some(semver_compatibility) = semver_compatibility.as_ref() { + version_checker::is_minor_version_compatible( + &annotated_bond + .mixnode_details + .bond_information + .mix_node + .version, + semver_compatibility, + ) + } else { + true + } + }) + .map(Into::into) + .collect(), + })) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/mixnodes/semi-skimmed", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn mixnodes_expanded( + State(_state): State, + Query(SemverCompatibilityQueryParam { + semver_compatibility: _semver_compatibility, + }): Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(SemverCompatibilityQueryParam), + path = "/v1/unstable/nym-nodes/mixnodes/full-fat", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +async fn mixnodes_detailed( + State(_state): State, + Query(SemverCompatibilityQueryParam { + semver_compatibility: _semver_compatibility, + }): Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index 350fd320a3..d7d5b98a7e 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -6,11 +6,16 @@ use rocket::Route; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; +#[cfg(feature = "axum")] +pub(crate) mod handlers; +#[cfg(feature = "axum")] +pub(crate) mod handlers_unstable; + pub(crate) mod routes; mod unstable_routes; /// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn nym_node_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { +pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ settings: routes::get_gateways_described, routes::get_mixnodes_described ] diff --git a/nym-api/src/nym_nodes/unstable_routes.rs b/nym-api/src/nym_nodes/unstable_routes.rs index 93692b9c22..aa12e5b7f3 100644 --- a/nym-api/src/nym_nodes/unstable_routes.rs +++ b/nym-api/src/nym_nodes/unstable_routes.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::models::ErrorResponse; +use crate::node_status_api::models::RocketErrorResponse; use crate::node_status_api::NodeStatusCache; use crate::support::caching::cache::SharedCache; use nym_api_requests::nym_nodes::{ @@ -39,7 +39,7 @@ pub async fn nodes_basic( describe_cache: &State>, role: Option, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { if let Some(role) = role { match role { NodeRoleQueryParam::ActiveMixnode => { @@ -52,7 +52,10 @@ pub async fn nodes_basic( } } - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -61,7 +64,7 @@ pub async fn nodes_expanded( cache: &State, role: Option, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { if let Some(role) = role { match role { NodeRoleQueryParam::ActiveMixnode => { @@ -74,7 +77,10 @@ pub async fn nodes_expanded( } } - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -83,7 +89,7 @@ pub async fn nodes_detailed( cache: &State, role: Option, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { if let Some(role) = role { match role { NodeRoleQueryParam::ActiveMixnode => { @@ -96,7 +102,10 @@ pub async fn nodes_detailed( } } - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -105,11 +114,11 @@ pub async fn gateways_basic( status_cache: &State, describe_cache: &State>, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let gateways_cache = status_cache .gateways_cache() .await - .ok_or(ErrorResponse::new( + .ok_or(RocketErrorResponse::new( "could not obtain gateways cache", Status::InternalServerError, ))?; @@ -162,10 +171,13 @@ pub async fn gateways_basic( pub async fn gateways_expanded( cache: &State, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let _ = cache; let _ = semver_compatibility; - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -173,10 +185,13 @@ pub async fn gateways_expanded( pub async fn gateways_detailed( cache: &State, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let _ = cache; let _ = semver_compatibility; - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -184,11 +199,11 @@ pub async fn gateways_detailed( pub async fn mixnodes_basic( cache: &State, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let mixnodes_cache = cache .active_mixnodes_cache() .await - .ok_or(ErrorResponse::new( + .ok_or(RocketErrorResponse::new( "could not obtain mixnodes cache", Status::InternalServerError, ))?; @@ -220,10 +235,13 @@ pub async fn mixnodes_basic( pub async fn mixnodes_expanded( cache: &State, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let _ = cache; let _ = semver_compatibility; - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } #[openapi(tag = "Unstable Nym Nodes")] @@ -231,8 +249,11 @@ pub async fn mixnodes_expanded( pub async fn mixnodes_detailed( cache: &State, semver_compatibility: Option, -) -> Result>, ErrorResponse> { +) -> Result>, RocketErrorResponse> { let _ = cache; let _ = semver_compatibility; - Err(ErrorResponse::new("unimplemented", Status::NotImplemented)) + Err(RocketErrorResponse::new( + "unimplemented", + Status::NotImplemented, + )) } diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs new file mode 100644 index 0000000000..ea34727f9a --- /dev/null +++ b/nym-api/src/status/handlers.rs @@ -0,0 +1,92 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::status::ApiStatusState; +use crate::v2::AxumAppState; +use axum::Json; +use axum::Router; +use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_compact_ecash::Base58; +use std::sync::Arc; + +pub(crate) fn api_status_routes() -> Router { + let api_status_state = Arc::new(ApiStatusState::new()); + + Router::new() + .route( + "/health", + axum::routing::get({ + let state = Arc::clone(&api_status_state); + || health(state) + }), + ) + .route( + "/build-information", + axum::routing::get({ + let state = Arc::clone(&api_status_state); + || build_information(state) + }), + ) + .route( + "/signer-information", + axum::routing::get({ + let state = Arc::clone(&api_status_state); + || signer_information(state) + }), + ) +} + +#[utoipa::path( + tag = "API Status", + get, + path = "/v1/api-status/health", + responses( + (status = 200, body = ApiHealthResponse) + ) +)] +async fn health(state: Arc) -> Json { + let uptime = state.startup_time.elapsed(); + let health = ApiHealthResponse::new_healthy(uptime); + Json(health) +} + +#[utoipa::path( + tag = "API Status", + get, + path = "/v1/api-status/build-information", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +async fn build_information(state: Arc) -> Json { + Json(state.build_information.to_owned()) +} + +#[utoipa::path( + tag = "API Status", + get, + path = "/v1/api-status/signer-information", + responses( + (status = 200, body = SignerInformationResponse) + ) +)] +async fn signer_information( + state: Arc, +) -> AxumResult> { + let signer_state = state.signer_information.as_ref().ok_or_else(|| { + AxumErrorResponse::internal_msg("this api does not expose zk-nym signing functionalities") + })?; + + Ok(Json(SignerInformationResponse { + cosmos_address: signer_state.cosmos_address.clone(), + identity: signer_state.identity.clone(), + announce_address: signer_state.announce_address.clone(), + verification_key: signer_state + .coconut_keypair + .verification_key() + .await + .map(|maybe_vk| maybe_vk.to_bs58()), + })) +} diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 053cb8ba88..1d09b341c8 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -10,6 +10,8 @@ use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; use tokio::time::Instant; +#[cfg(feature = "axum")] +pub(crate) mod handlers; pub(crate) mod routes; pub(crate) struct ApiStatusState { diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 560ac765d9..804744df18 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::ErrorResponse; +use crate::node_status_api::models::RocketErrorResponse; use crate::status::ApiStatusState; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -31,9 +31,9 @@ pub(crate) async fn build_information( #[get("/signer-information")] pub(crate) async fn signer_information( state: &State, -) -> Result, ErrorResponse> { +) -> Result, RocketErrorResponse> { let signer_state = state.signer_information.as_ref().ok_or_else(|| { - ErrorResponse::new( + RocketErrorResponse::new( "this api does not expose zk-nym signing functionalities", Status::InternalServerError, ) diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 9bb58c585e..a19d2c1c29 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -3,7 +3,7 @@ use crate::start_nym_api_tasks; use crate::support::config::helpers::try_load_current_config; -use anyhow::bail; +use cfg_if::cfg_if; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -43,7 +43,7 @@ pub(crate) struct Args { pub(crate) enable_zk_nym: Option, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect - /// to obtain their credentials + /// to obtain their credentials /// default: None - config value will be used instead #[clap(long)] pub(crate) announce_address: Option, @@ -62,18 +62,40 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - let mut shutdown_handlers = start_nym_api_tasks(config).await?; + #[cfg(feature = "axum")] + let mut axum_shutdown = crate::v2::start_nym_api_tasks_v2(&config).await?; + let mut rocket_shutdown = start_nym_api_tasks(config).await?; + + // it doesn't matter which server catches the interrupt: it needs only be caught once + if let Err(err) = rocket_shutdown.task_manager_handle.catch_interrupt().await { + error!("Error stopping Rocket tasks: {err}"); + } - let res = shutdown_handlers - .task_manager_handle - .catch_interrupt() - .await; log::info!("Stopping nym API"); - shutdown_handlers.rocket_handle.notify(); + rocket_shutdown.rocket_handle.notify(); - if let Err(err) = res { - // that's a nasty workaround, but anyhow errors are generally nicer, especially on exit - bail!("{err}") + // because Rocket caught the interrupt, it had already signalled its + // background tasks to retire. Now do that for axum + cfg_if! { + if #[cfg(feature = "axum")] { + axum_shutdown.task_manager_mut().signal_shutdown().ok(); + axum_shutdown.task_manager_mut().wait_for_shutdown().await; + + let running_server = axum_shutdown.shutdown_axum(); + + match running_server.await { + Ok(Ok(_)) => { + info!("Axum HTTP server shut down without errors"); + }, + Ok(Err(err)) => { + error!("Axum HTTP server terminated with: {err}"); + anyhow::bail!(err) + }, + Err(err) => { + error!("Server task panicked: {err}"); + } + }; + } } Ok(()) diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index c5191b7960..a26384652f 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -16,6 +16,7 @@ use nym_config::{ }; use serde::{Deserialize, Serialize}; use std::io; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; use url::Url; @@ -225,6 +226,17 @@ impl Config { } } +// TODO rocket: when axum becomes the main server, change its bind addr default here +fn default_http_socket_addr() -> SocketAddr { + cfg_if::cfg_if! { + if #[cfg(debug_assertions)] { + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081) + } else { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8081) + } + } +} + // we only really care about the mnemonic being zeroized #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize, ZeroizeOnDrop)] pub struct Base { @@ -234,6 +246,11 @@ pub struct Base { #[zeroize(skip)] pub local_validator: Url, + /// Socket address Axum will use for binding its HTTP API. + #[zeroize(skip)] + #[serde(default = "default_http_socket_addr")] + pub bind_address: SocketAddr, + /// Mnemonic used for rewarding and/or multisig operations // TODO: similarly to the note in gateway, this should get moved to a separate file #[serde(deserialize_with = "de_maybe_stringified")] @@ -256,6 +273,7 @@ impl Base { storage_paths: NymApiPaths::new_default(&id), id, local_validator: default_validator, + bind_address: default_http_socket_addr(), mnemonic: None, } } diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 170cc73ba3..108cc0f426 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -15,6 +15,10 @@ id = '{{ base.id }}' # Validator server to which the API will be getting information about the network. local_validator = '{{ base.local_validator }}' +# Socket address this api will use for binding its http API. +# Note: only used if `axum` feature is enabled. +bind_address = '{{ base.bind_address }}' + # Mnemonic used for rewarding and validator interaction mnemonic = '{{ base.mnemonic }}' @@ -58,7 +62,7 @@ route_test_packets = {{ network_monitor.debug.route_test_packets }} # Number of test packets sent to each node during regular monitor test run. per_node_test_packets = {{ network_monitor.debug.per_node_test_packets }} - + ##### node status api config options ##### diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index a0696d4955..612dcb9333 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -8,19 +8,18 @@ use crate::ecash::{self, comm::QueryCommunicationChannel}; use crate::network::models::NetworkDetails; use crate::network::network_routes; use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::routes::unstable; +use crate::node_status_api::routes_deprecated::unstable; use crate::node_status_api::{self, NodeStatusCache}; use crate::nym_contract_cache::cache::NymContractCache; -use crate::nym_nodes::nym_node_routes_next; +use crate::nym_nodes::{nym_node_routes_deprecated, nym_node_routes_next}; use crate::status::{api_status_routes, ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; +use crate::{circulating_supply_api, nym_contract_cache}; use anyhow::{bail, Context, Result}; use nym_crypto::asymmetric::identity; use nym_validator_client::nyxd::Coin; -use rocket::http::Method; use rocket::{Ignite, Rocket}; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; use rocket_okapi::mount_endpoints_and_merged_docs; @@ -52,33 +51,26 @@ pub(crate) async fn setup_rocket( "/network" => network_routes(&openapi_settings), "/api-status" => api_status_routes(&openapi_settings), "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), - "" => nym_node_routes(&openapi_settings), + "" => nym_node_routes_deprecated(&openapi_settings), // => when we move those routes, we'll need to add a redirection for backwards compatibility "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) } + let storage = + storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + let rocket = rocket .manage(network_details) .manage(SharedCache::::new()) .mount("/swagger", make_swagger_ui(&openapi::get_docs())) - .attach(setup_cors()?) + .attach(setup_rocket_cors()?) .attach(NymContractCache::stage()) .attach(NodeStatusCache::stage()) .attach(CirculatingSupplyCache::stage(mix_denom.clone())) + .attach(storage::NymApiStorage::stage(storage.clone())) .manage(unstable::NodeInfoCache::default()); - // This is not a very nice approach. A lazy value would be more suitable, but that's still - // a nightly feature: https://github.com/rust-lang/rust/issues/74465 - let storage = if config.coconut_signer.enabled || config.network_monitor.enabled { - Some( - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path) - .await?, - ) - } else { - None - }; - let mut status_state = ApiStatusState::new(); let rocket = if config.coconut_signer.enabled { @@ -117,7 +109,7 @@ pub(crate) async fn setup_rocket( identity_keypair, coconut_keypair, comm_channel, - storage.clone().unwrap(), + storage.clone(), ) .await?; @@ -126,23 +118,16 @@ pub(crate) async fn setup_rocket( rocket }; - // see if we should start up network monitor - let rocket = if config.network_monitor.enabled { - rocket.attach(storage::NymApiStorage::stage(storage.unwrap())) - } else { - rocket - }; - Ok(rocket.manage(status_state).ignite().await?) } -fn setup_cors() -> Result { +fn setup_rocket_cors() -> Result { let allowed_origins = AllowedOrigins::all(); // You can also deserialize this let cors = rocket_cors::CorsOptions { allowed_origins, - allowed_methods: vec![Method::Post, Method::Get] + allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] .into_iter() .map(From::from) .collect(), diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index 32621aa619..d4cdc64c02 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -64,6 +64,7 @@ impl NymApiStorage { Ok(storage) } + #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] pub(crate) fn stage(storage: NymApiStorage) -> AdHoc { AdHoc::try_on_ignite("SQLx Database", |rocket| async { Ok(rocket.manage(storage)) diff --git a/nym-api/src/v2/api_docs.rs b/nym-api/src/v2/api_docs.rs new file mode 100644 index 0000000000..9b248bb803 --- /dev/null +++ b/nym-api/src/v2/api_docs.rs @@ -0,0 +1,85 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::network::handlers::ContractVersionSchemaResponse; +use nym_api_requests::models; +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: +// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) +// for automatic model discovery based on ToSchema / IntoParams implementation. +// Then you can remove `components(schemas)` manual imports below + +#[utoipauto(paths = "./nym-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym API"), + tags(), + components(schemas( + models::CirculatingSupplyResponse, + models::CoinSchema, + nym_mixnet_contract_common::Interval, + nym_api_requests::models::GatewayStatusReportResponse, + nym_api_requests::models::GatewayUptimeHistoryResponse, + nym_api_requests::models::HistoricalUptimeResponse, + nym_api_requests::models::GatewayCoreStatusResponse, + nym_api_requests::models::GatewayUptimeResponse, + nym_api_requests::models::RewardEstimationResponse, + nym_api_requests::models::UptimeResponse, + nym_api_requests::models::ComputeRewardEstParam, + nym_api_requests::models::MixNodeBondAnnotated, + nym_api_requests::models::GatewayBondAnnotated, + nym_api_requests::models::MixnodeTestResultResponse, + nym_api_requests::models::StakeSaturationResponse, + nym_api_requests::models::InclusionProbabilityResponse, + nym_api_requests::models::AllInclusionProbabilitiesResponse, + nym_api_requests::models::InclusionProbability, + nym_api_requests::models::SelectionChance, + crate::network::models::NetworkDetails, + nym_config::defaults::NymNetworkDetails, + nym_config::defaults::ChainDetails, + nym_config::defaults::DenomDetailsOwned, + nym_config::defaults::ValidatorDetails, + nym_config::defaults::NymContracts, + ContractVersionSchemaResponse, + crate::network::models::ContractInformation, + nym_api_requests::models::ApiHealthResponse, + nym_api_requests::models::ApiStatus, + nym_bin_common::build_information::BinaryBuildInformationOwned, + nym_api_requests::models::SignerInformationResponse, + nym_api_requests::models::DescribedGateway, + nym_api_requests::models::MixNodeDetailsSchema, + nym_mixnet_contract_common::Gateway, + nym_mixnet_contract_common::GatewayBond, + nym_api_requests::models::NymNodeDescription, + nym_api_requests::models::HostInformation, + nym_api_requests::models::HostKeys, + nym_node_requests::api::v1::node::models::AuxiliaryDetails, + nym_api_requests::models::NetworkRequesterDetails, + nym_api_requests::models::IpPacketRouterDetails, + nym_api_requests::models::AuthenticatorDetails, + nym_api_requests::models::WebSockets, + nym_api_requests::nym_nodes::NodeRole, + nym_api_requests::models::DescribedMixNode, + nym_api_requests::ecash::VerificationKeyResponse, + nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, + nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EpochCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialResponse, + nym_api_requests::ecash::models::IssuedTicketbookBody, + nym_api_requests::ecash::models::BlindedSignatureResponse, + nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, + nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EcashTicketVerificationResponse, + nym_api_requests::ecash::models::EcashTicketVerificationRejection, + nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, + nym_api_requests::ecash::models::SpentCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialsResponse, + nym_api_requests::nym_nodes::SkimmedNode, + nym_api_requests::nym_nodes::BasicEntryInformation, + nym_api_requests::nym_nodes::SemiSkimmedNode, + nym_api_requests::nym_nodes::NodeRoleQueryParam, + )) +)] +pub(super) struct ApiDoc; diff --git a/nym-api/src/v2/mod.rs b/nym-api/src/v2/mod.rs new file mode 100644 index 0000000000..59109d16b7 --- /dev/null +++ b/nym-api/src/v2/mod.rs @@ -0,0 +1,313 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use super::support::nyxd; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; +use crate::ecash::client::Client; +use crate::ecash::comm::QueryCommunicationChannel; +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, +}; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::state::EcashState; +use crate::epoch_operations::{self, RewardedSetUpdater}; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::{self, DescribedNodes}; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::node_status_api::{self, NodeStatusCache}; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::status::{ApiStatusState, SignerState}; +use crate::support::caching::cache::SharedCache; +use crate::support::config::Config; +use crate::support::storage; +use crate::{circulating_supply_api, ecash, network_monitor, nym_contract_cache}; +use anyhow::{bail, Context}; +use nym_config::defaults::NymNetworkDetails; +use nym_sphinx::receiver::SphinxMessageReceiver; +use nym_task::TaskManager; +use nym_validator_client::nyxd::Coin; +use router::RouterBuilder; +use std::sync::Arc; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +pub(crate) mod api_docs; +pub(crate) mod router; + +const TASK_MANAGER_TIMEOUT_S: u64 = 10; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Ofcourse, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +pub(crate) struct ShutdownHandles { + task_manager: TaskManager, + axum_shutdown_button: ShutdownAxum, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub(crate) fn new( + task_manager: TaskManager, + axum_server_handle: AxumJoinHandle, + shutdown_button: CancellationToken, + ) -> Self { + Self { + task_manager, + axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), + axum_join_handle: axum_server_handle, + } + } + + pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { + &mut self.task_manager + } + + /// Signal server to shut down, then return join handle to its + /// `tokio` task + /// + /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html + #[must_use] + pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { + self.axum_shutdown_button.0.cancel(); + self.axum_join_handle + } +} + +struct ShutdownAxum(CancellationToken); + +type AxumJoinHandle = JoinHandle>; + +#[derive(Clone)] +// TODO rocket remove smurf name after eliminating rocket +pub(crate) struct AxumAppState { + nym_contract_cache: NymContractCache, + node_status_cache: NodeStatusCache, + circulating_supply_cache: CirculatingSupplyCache, + storage: storage::NymApiStorage, + described_nodes_state: SharedCache, + network_details: NetworkDetails, + node_info_cache: unstable::NodeInfoCache, +} + +impl AxumAppState { + pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { + &self.nym_contract_cache + } + + pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { + &self.node_status_cache + } + + pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { + &self.circulating_supply_cache + } + + pub(crate) fn network_details(&self) -> &NetworkDetails { + &self.network_details + } + + pub(crate) fn described_nodes_state(&self) -> &SharedCache { + &self.described_nodes_state + } + + pub(crate) fn storage(&self) -> &storage::NymApiStorage { + &self.storage + } + + pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { + &self.node_info_cache + } +} + +pub(crate) async fn start_nym_api_tasks_v2(config: &Config) -> anyhow::Result { + let nyxd_client = nyxd::Client::new(config); + let connected_nyxd = config.get_nyxd_url(); + let nym_network_details = NymNetworkDetails::new_from_env(); + let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); + + let coconut_keypair = ecash::keys::KeyPair::new(); + + // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { + let issued_for = loaded_keys.issued_for_epoch; + coconut_keypair.set(loaded_keys).await; + + if can_validate_coconut_keys(&nyxd_client, issued_for).await? { + coconut_keypair.validate() + } + } + + let identity_keypair = config.base.storage_paths.load_identity()?; + let identity_public_key = *identity_keypair.public_key(); + + let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); + + let nym_contract_cache_state = NymContractCache::new(); + let node_status_cache_state = NodeStatusCache::new(); + let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); + let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); + let described_nodes_state = SharedCache::::new(); + let storage = + storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + let node_info_cache = unstable::NodeInfoCache::default(); + + let mut status_state = ApiStatusState::new(); + + // if coconut signer is enabled, add /coconut to server + let router = if config.coconut_signer.enabled { + // make sure we have some tokens to cover multisig fees + let balance = nyxd_client.balance(&mix_denom).await?; + if balance.amount < ecash::MINIMUM_BALANCE { + let address = nyxd_client.address().await; + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); + bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") + } + + let cosmos_address = nyxd_client.address().await.to_string(); + let announce_address = config + .coconut_signer + .announce_address + .clone() + .map(|u| u.to_string()) + .unwrap_or_default(); + status_state.add_zk_nym_signer(SignerState { + cosmos_address, + identity: identity_keypair.public_key().to_base58_string(), + announce_address, + coconut_keypair: coconut_keypair.clone(), + }); + + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); + + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client.clone(), + identity_keypair, + coconut_keypair.clone(), + comm_channel, + storage.clone(), + ) + .await?; + + router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + } else { + router + }; + + let router = router.with_state(AxumAppState { + nym_contract_cache: nym_contract_cache_state.clone(), + node_status_cache: node_status_cache_state.clone(), + circulating_supply_cache: circulating_supply_cache.clone(), + storage: storage.clone(), + described_nodes_state: described_nodes_state.clone(), + network_details, + node_info_cache, + }); + + let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + nym_contract_cache_state.clone(), + described_nodes_state, + ) + .named("node-self-described-data-refresher") + .start(task_manager.subscribe_named("node-self-described-data-refresher")); + + // start all the caches first + let nym_contract_cache_listener = nym_contract_cache::start_refresher( + &config.node_status_api, + &nym_contract_cache_state, + nyxd_client.clone(), + &task_manager, + ); + node_status_api::start_cache_refresh( + &config.node_status_api, + &nym_contract_cache_state, + &node_status_cache_state, + storage.clone(), + nym_contract_cache_listener, + &task_manager, + ); + circulating_supply_api::start_cache_refresh( + &config.circulating_supply_cacher, + nyxd_client.clone(), + &circulating_supply_cache, + &task_manager, + ); + + // start dkg task + if config.coconut_signer.enabled { + let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; + + DkgController::start( + &config.coconut_signer, + nyxd_client.clone(), + coconut_keypair, + dkg_bte_keypair, + identity_public_key, + rand::rngs::OsRng, + &task_manager, + )?; + } + + // and then only start the uptime updater (and the monitor itself, duh) + // if the monitoring is enabled + if config.network_monitor.enabled { + network_monitor::start::( + &config.network_monitor, + &nym_contract_cache_state, + &storage, + nyxd_client.clone(), + &task_manager, + ) + .await; + + HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + + // start 'rewarding' if its enabled + if config.rewarding.enabled { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + RewardedSetUpdater::start( + nyxd_client, + &nym_contract_cache_state, + storage, + &task_manager, + ); + } + } + + let bind_address = config.base.bind_address.to_owned(); + let server = router.build_server(&bind_address).await?; + + let cancellation_token = CancellationToken::new(); + let shutdown_button = cancellation_token.clone(); + let axum_shutdown_receiver = cancellation_token.cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + + Ok(shutdown) +} diff --git a/nym-api/src/v2/router.rs b/nym-api/src/v2/router.rs new file mode 100644 index 0000000000..6ece0b7323 --- /dev/null +++ b/nym-api/src/v2/router.rs @@ -0,0 +1,136 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::circulating_supply_api::handlers::circulating_supply_routes; +use crate::network::handlers::nym_network_routes; +use crate::node_status_api::handlers::node_status_routes; +use crate::nym_contract_cache::handlers::nym_contract_cache_routes; +use crate::nym_nodes::handlers::nym_node_routes; +use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable; +use crate::status; +use crate::v2::AxumAppState; +use anyhow::anyhow; +use axum::Router; +use core::net::SocketAddr; +use nym_http_api_common::logging::logger; +use tokio::net::TcpListener; +use tokio_util::sync::WaitForCancellationFutureOwned; +use tower_http::cors::CorsLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +/// Wrapper around `axum::Router` which ensures correct [order of layers][order]. +/// Add new routes as if you were working directly with `axum`. +/// +/// Why? Middleware like logger, CORS, TLS which need to handle request before other +/// layers should be added last. Using this builder pattern ensures that. +/// +/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + /// All routes should be, if possible, added here. Exceptions are e.g. + /// routes which are added conditionally in other places based on some `if`. + pub(crate) fn with_default_routes(network_monitor: bool) -> Self { + // https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html + // TODO rocket use tracing instead of env_logger + // https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs + // .layer( + // TraceLayer::new_for_http() + // .make_span_with(DefaultMakeSpan::new().include_headers(true)) + // .on_request(DefaultOnRequest::new()) + // .on_response(DefaultOnResponse::new().latency_unit(tower_http::LatencyUnit::Micros)), + // ) + // .route("/", axum::routing::get(|| async {axum::response::Redirect::permanent("/swagger")})) + // .route("/swagger", axum::routing::get(hello)) + let default_routes = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .nest( + "/v1", + Router::new() + .nest("/circulating-supply", circulating_supply_routes()) + .merge(nym_contract_cache_routes()) + .nest("/status", node_status_routes(network_monitor)) + .nest("/network", nym_network_routes()) + .nest("/api-status", status::handlers::api_status_routes()) + .merge(nym_node_routes()) + .nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes + ); + + Self { + unfinished_router: default_routes, + } + } + + pub(crate) fn nest(self, path: &str, router: Router) -> Self { + Self { + unfinished_router: self.unfinished_router.nest(path, router), + } + } + + /// Invoke this as late as possible before constructing HTTP server + /// (after all routes were added). + pub(crate) fn with_state(self, state: AxumAppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + /// Middleware added here intercepts the request before it gets to other routes. + fn finalize_routes(self) -> Router { + self.unfinished_router + .layer(setup_cors()) + .layer(axum::middleware::from_fn(logger)) + } +} + +fn setup_cors() -> CorsLayer { + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: &SocketAddr, + ) -> anyhow::Result { + let listener = tokio::net::TcpListener::bind(bind_address) + .await + .map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?; + + Ok(ApiHttpServer { + router: self.router, + listener, + }) + } +} + +pub(crate) struct ApiHttpServer { + router: Router, + listener: TcpListener, +} + +impl ApiHttpServer { + pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-node/tests/definitions.sh b/nym-node/tests/definitions.sh new file mode 100755 index 0000000000..80b6886aa4 --- /dev/null +++ b/nym-node/tests/definitions.sh @@ -0,0 +1,217 @@ +#!/bin/bash + +# color codes +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +DEFAULT='\033[0m' + +axum_server_addr="http://localhost:8081" +rocket_server_addr="http://localhost:8000" + +function compare_responses() { + local api="$1" + shift + local url_1="${axum_server_addr}${api}" + local url_2="${rocket_server_addr}${api}" + + local response_1=$(curl -s "${url_1}") + local normalized_resp_1=$(echo "${response_1}" | jq --sort-keys '.') + local response_2=$(curl -s "${url_2}") + local normalized_resp_2=$(echo "${response_2}" | jq --sort-keys '.') + + echo -e "Response: \n${response_1}" + # jq . "${response_1}" + + if [[ "${response_1}" == "${response_2}" ]]; then + # if cmp -s <(jq -S . "$response_1") <(jq -S ."$response_2"); then + echo -e "${GREEN}Responses are the same ${DEFAULT}" + else + echo -e "${RED}" + echo "${response_2}" + # jq . "${response_2}" + echo -e "Responses are different" + diff <(echo "${normalized_resp_1}") <(echo "${normalized_resp_2}") + echo -e "${DEFAULT}" + fi +} + +function validate_response() { + local expected_status="$1" + shift + local url=("$@") + local query=(curl -I -X 'GET' "${url[@]}" -H 'accept: application/json') + # execute given curl + echo -e "Executing\n${BLUE}\t${query[*]}${DEFAULT}" + local response=$("${query[@]}" -w "\n%{http_code}\n%{content_type}") + + # parse status code & content type + local http_code=$(echo "$response" | tail -n2 | head -n1) + local content_type=$(echo "$response" | tail -n1) + + # -e flag for coloured output + if [ "$http_code" -eq "$expected_status" ]; then + echo -e "${GREEN}HTTP code $http_code as expected, Content-Type is $content_type ${DEFAULT}" + return 0 + else + echo -e "${RED}Unexpected HTTP code: $http_code, Content-Type is $content_type ${DEFAULT}" + return 1 + fi +} + +function title() { + local title="$1" + echo -e "\n${BLUE}\t╔═══ ${title} ═══╗${DEFAULT}" +} + +function circulating_supply() { + title "/circulating-supply" + + local api="/v1/circulating-supply" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/circulating-supply/total-supply-value" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/circulating-supply/circulating-supply-value" + validate_response 200 "${target_address}${api}" + compare_responses ${api} +} + +function contract_cache() { + title "/contract-cache" + + local api="/v1/mixnodes" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/detailed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/gateways" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/active" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/active/detailed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/rewarded" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/rewarded/detailed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/mixnodes/blacklisted" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/gateways/blacklisted" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/epoch/reward_params" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/epoch/current" + validate_response 200 "${target_address}${api}" + compare_responses ${api} +} + +function network() { + title "/network" + local api="/v1/network/details" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/network/nym-contracts" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/network/nym-contracts-detailed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} +} + +function api_status() { + title "/api-status" + local api="/v1/api-status/health" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/api-status/build-information" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/api-status/signer-information" + validate_response 200 "${target_address}${api}" + compare_responses ${api} +} + +function unstable() { + title "/unstable" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/skimmed" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/semi-skimmed" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/full-fat" + + local api="/v1/unstable/nym-nodes/gateways/skimmed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/unstable/nym-nodes/gateways/skimmed?semver_compatibility" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/unstable/nym-nodes/gateways/skimmed?semver_compatibility=2.0.0" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed?semver_compatibility" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed?semver_compatibility=2.0.0" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat?semver_compatibility" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat?semver_compatibility=2.0.0" + + local api="/v1/unstable/nym-nodes/mixnodes/skimmed" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/unstable/nym-nodes/mixnodes/skimmed?semver_compatibility" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + local api="/v1/unstable/nym-nodes/mixnodes/skimmed?semver_compatibility=2.0.0" + validate_response 200 "${target_address}${api}" + compare_responses ${api} + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed?semver_compatibility" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed?semver_compatibility=2.0.0" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat?semver_compatibility" + + validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat?semver_compatibility=2.0.0" +} diff --git a/nym-node/tests/test_apis.sh b/nym-node/tests/test_apis.sh new file mode 100755 index 0000000000..31df14f48c --- /dev/null +++ b/nym-node/tests/test_apis.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Very basic validation, only that endpoints are reachable +clear + +source $(dirname "$0")/definitions.sh + +function fire_on_all_apis() { + local target_address="$1" + + circulating_supply + + contract_cache + + network + + api_status + + unstable +} + +fire_on_all_apis "http://localhost:8081" + +fire_on_all_apis "http://localhost:8000"