Compare commits

..

2 Commits

Author SHA1 Message Date
durch 7712d15e79 Remove unwraps 2025-06-12 17:30:41 +02:00
durch eb886c0860 Add shared derivation material support for thread-safe key generation
- Add serialization support to DerivationMaterial for persistence/sharing
- Implement shared derivation material in BaseClientBuilder with Arc<Mutex<>> for thread safety
- Add network monitor support for deterministic key generation from file
- Extend MixnetClientBuilder with shared derivation material functionality
- Add comprehensive documentation explaining thread-safe sharing and key generation priority

This enables multiple clients to derive keys from the same source material while maintaining
thread safety, particularly useful for network monitoring where consistent client identities
are needed across multiple instances.
2025-06-12 17:24:32 +02:00
58 changed files with 617 additions and 2696 deletions
Generated
+38 -416
View File
@@ -1457,16 +1457,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-models"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"pastey",
"rand 0.9.1",
]
[[package]]
name = "cosmos-sdk-proto"
version = "0.26.1"
@@ -3151,40 +3141,6 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "hax-lib"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"hax-lib-macros",
"num-bigint",
"num-traits",
]
[[package]]
name = "hax-lib-macros"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"hax-lib-macros-types",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "hax-lib-macros-types"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "hdrhistogram"
version = "7.5.4"
@@ -3294,7 +3250,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.1",
"rand 0.9.0",
"ring",
"rustls 0.23.25",
"thiserror 2.0.12",
@@ -3319,7 +3275,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.1",
"rand 0.9.0",
"resolv-conf",
"rustls 0.23.25",
"smallvec",
@@ -4194,194 +4150,6 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libcrux-chacha20poly1305"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-poly1305",
]
[[package]]
name = "libcrux-curve25519"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
]
[[package]]
name = "libcrux-ecdh"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-curve25519",
"libcrux-p256",
"rand 0.9.1",
]
[[package]]
name = "libcrux-ed25519"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
"rand_core 0.9.1",
]
[[package]]
name = "libcrux-hacl-rs"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-macros",
]
[[package]]
name = "libcrux-hkdf"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-hmac",
]
[[package]]
name = "libcrux-hmac"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
]
[[package]]
name = "libcrux-intrinsics"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"core-models",
"hax-lib",
]
[[package]]
name = "libcrux-kem"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-ecdh",
"libcrux-ml-kem",
"libcrux-sha3",
"libcrux-traits",
"rand 0.9.1",
]
[[package]]
name = "libcrux-macros"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"quote",
"syn 2.0.98",
]
[[package]]
name = "libcrux-ml-kem"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"libcrux-intrinsics",
"libcrux-platform",
"libcrux-secrets",
"libcrux-sha3",
"rand 0.9.1",
]
[[package]]
name = "libcrux-p256"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
]
[[package]]
name = "libcrux-platform"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libc",
]
[[package]]
name = "libcrux-poly1305"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
]
[[package]]
name = "libcrux-psq"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-chacha20poly1305",
"libcrux-ecdh",
"libcrux-ed25519",
"libcrux-hkdf",
"libcrux-hmac",
"libcrux-kem",
"libcrux-traits",
"rand 0.9.1",
]
[[package]]
name = "libcrux-secrets"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
]
[[package]]
name = "libcrux-sha2"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-traits",
]
[[package]]
name = "libcrux-sha3"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"libcrux-intrinsics",
"libcrux-platform",
]
[[package]]
name = "libcrux-traits"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"rand 0.9.1",
]
[[package]]
name = "libm"
version = "0.2.11"
@@ -5124,18 +4892,18 @@ dependencies = [
"getset",
"hex",
"humantime-serde",
"nym-compact-ecash",
"nym-config",
"nym-contracts-common",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-time",
"nym-mixnet-contract-common",
"nym-network-defaults",
"nym-node-requests",
"nym-compact-ecash 0.1.0",
"nym-config 0.1.0",
"nym-contracts-common 0.5.0",
"nym-credentials-interface 0.1.0",
"nym-crypto 0.4.0",
"nym-ecash-time 0.1.0",
"nym-mixnet-contract-common 0.6.0",
"nym-network-defaults 0.1.0",
"nym-node-requests 0.1.0",
"nym-noise-keys",
"nym-serde-helpers",
"nym-ticketbooks-merkle",
"nym-serde-helpers 0.1.0",
"nym-ticketbooks-merkle 0.1.0",
"rand_chacha 0.3.1",
"schemars",
"serde",
@@ -5538,7 +5306,6 @@ dependencies = [
"nym-sphinx 0.1.0",
"nym-task 0.1.0",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"time",
"tokio",
@@ -5806,7 +5573,6 @@ dependencies = [
"nym-ecash-time 0.1.0",
"serde",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"tokio",
"zeroize",
@@ -6493,10 +6259,10 @@ version = "0.1.0"
dependencies = [
"dashmap",
"futures",
"nym-crypto",
"nym-crypto 0.4.0",
"nym-noise",
"nym-sphinx",
"nym-task",
"nym-sphinx 0.1.0",
"nym-task 0.1.0",
"rand 0.8.5",
"tokio",
"tokio-stream",
@@ -6762,7 +6528,7 @@ dependencies = [
"nym-mixnet-client",
"nym-network-requester",
"nym-node-metrics",
"nym-node-requests",
"nym-node-requests 0.1.0",
"nym-noise",
"nym-noise-keys",
"nym-nonexhaustive-delayqueue",
@@ -6825,12 +6591,12 @@ dependencies = [
"celes",
"humantime",
"humantime-serde",
"nym-bin-common",
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-bin-common 0.6.0",
"nym-crypto 0.4.0",
"nym-exit-policy 0.1.0",
"nym-http-api-client 0.1.0",
"nym-noise-keys",
"nym-wireguard-types",
"nym-wireguard-types 0.1.0",
"rand_chacha 0.3.1",
"schemars",
"serde",
@@ -6883,7 +6649,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "3.1.0"
version = "3.0.0"
dependencies = [
"ammonia",
"anyhow",
@@ -7009,12 +6775,14 @@ dependencies = [
name = "nym-noise"
version = "0.1.0"
dependencies = [
"anyhow",
"arc-swap",
"bytes",
"futures",
"nym-crypto",
"nym-crypto 0.4.0",
"nym-noise-keys",
"pin-project",
"rand_chacha 0.3.1",
"sha2 0.10.9",
"snow",
"strum 0.26.3",
@@ -7028,7 +6796,7 @@ dependencies = [
name = "nym-noise-keys"
version = "0.1.0"
dependencies = [
"nym-crypto",
"nym-crypto 0.4.0",
"schemars",
"serde",
"utoipa",
@@ -7136,29 +6904,6 @@ dependencies = [
"time",
]
[[package]]
name = "nym-psq-kkt"
version = "0.1.0"
dependencies = [
"arc-swap",
"bytes",
"criterion",
"futures",
"libcrux-kem",
"libcrux-psq",
"libcrux-traits",
"nym-crypto",
"pin-project",
"rand 0.9.1",
"sha2 0.10.9",
"snow",
"strum 0.26.3",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "nym-sdk"
version = "0.1.0"
@@ -8539,12 +8284,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261"
[[package]]
name = "peg"
version = "0.8.4"
@@ -8865,7 +8604,7 @@ dependencies = [
"hmac",
"md-5",
"memchr",
"rand 0.9.1",
"rand 0.9.0",
"sha2 0.10.9",
"stringprep",
]
@@ -8952,15 +8691,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc_pidinfo"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a"
dependencies = [
"libc",
]
[[package]]
name = "prometheus"
version = "0.14.0"
@@ -9116,7 +8846,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [
"bytes",
"getrandom 0.3.1",
"rand 0.9.1",
"rand 0.9.0",
"ring",
"rustc-hash",
"rustls 0.23.25",
@@ -9170,12 +8900,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.1",
"zerocopy 0.8.20",
]
[[package]]
@@ -10547,19 +10278,6 @@ dependencies = [
"whoami",
]
[[package]]
name = "sqlx-pool-guard"
version = "0.1.0"
dependencies = [
"proc_pidinfo",
"sqlx",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"windows 0.61.1",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
@@ -11240,7 +10958,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand 0.9.1",
"rand 0.9.0",
"socket2",
"tokio",
"tokio-util",
@@ -12574,28 +12292,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -12630,30 +12326,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
@@ -12676,17 +12348,6 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -12709,32 +12370,11 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-link"
version = "0.1.1"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
]
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-registry"
@@ -12742,7 +12382,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.4",
"windows-result 0.3.1",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@@ -12767,9 +12407,9 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
dependencies = [
"windows-link",
]
@@ -12793,15 +12433,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -12900,15 +12531,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
-3
View File
@@ -64,7 +64,6 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nym-id",
"common/nym-psq-kkt",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
@@ -127,7 +126,6 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
@@ -289,7 +287,6 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use std::{io, path::PathBuf};
use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -18,6 +19,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -30,6 +32,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -63,6 +63,7 @@ use std::os::raw::c_int as RawFd;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
use tokio::sync::Mutex;
use url::Url;
#[cfg(all(
@@ -195,6 +196,10 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
derivation_material: Option<DerivationMaterial>,
// Shared derivation material wrapped in Arc<Mutex<>> for thread-safe access
// across multiple clients. This allows multiple clients to share the same
// derivation source while maintaining safe concurrent access.
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
}
impl<C, S> BaseClientBuilder<C, S>
@@ -220,6 +225,7 @@ where
#[cfg(unix)]
connection_fd_callback: None,
derivation_material: None,
shared_derivation_material: None,
}
}
@@ -232,6 +238,18 @@ where
self
}
/// Set shared derivation material for thread-safe sharing across multiple clients.
/// This is useful when multiple clients need to derive keys from the same source
/// while ensuring thread-safe access through Arc<Mutex<>>.
#[must_use]
pub fn with_shared_derivation_material(
mut self,
derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) -> Self {
self.shared_derivation_material = derivation_material;
self
}
#[must_use]
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
self.config.debug.forget_me = *forget_me;
@@ -704,6 +722,7 @@ where
key_store: &S::KeyStore,
details_store: &S::GatewaysDetailsStore,
derivation_material: Option<DerivationMaterial>,
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) -> Result<InitialisationResult, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Sync + Send,
@@ -713,12 +732,24 @@ where
if key_store.load_keys().await.is_err() {
info!("could not find valid client keys - a new set will be generated");
let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
} else {
ClientKeys::generate_new(&mut rng)
// Key generation priority: individual derivation material > shared derivation material > random generation
let keys = match (derivation_material, shared_derivation_material) {
// Individual derivation material takes precedence if provided
(Some(derivation_material), _) => {
ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
}
// Use shared derivation material if no individual material is provided
(None, Some(shared_derivation_material)) => {
let shared_derivation_material = shared_derivation_material.lock().await;
ClientKeys::from_master_key(&mut rng, &shared_derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
}
// Fall back to random key generation if no derivation material is available
(None, None) => ClientKeys::generate_new(&mut rng),
};
store_client_keys(keys, key_store).await?;
}
@@ -741,6 +772,7 @@ where
self.client_store.key_store(),
self.client_store.gateway_details_store(),
self.derivation_material,
self.shared_derivation_material,
)
.await?;
@@ -1,18 +1,20 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
config,
config::Config,
error::ClientCoreError,
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
};
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use std::{io, path::Path};
use nym_validator_client::nyxd;
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use time::OffsetDateTime;
use url::Url;
@@ -20,11 +22,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> {
info!("Creating fresh surb database");
info!("creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
error!("failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -38,15 +40,14 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold,
);
match storage_backend.init_fresh(&mem_store).await {
Ok(()) => Ok(storage_backend),
Err(err) => {
storage_backend.shutdown().await;
Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
})
}
}
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err),
})?;
Ok(storage_backend)
}
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -57,11 +58,12 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// )
// }
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref();
debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted");
let new_extension =
@@ -70,15 +72,11 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
} else {
suffix
};
let renamed = db_path.with_extension(new_extension);
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
let mut renamed = db_path.to_owned();
renamed.set_extension(new_extension);
fs::rename(db_path, renamed)
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -89,12 +87,13 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("Loading existing surb database");
info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path)?;
setup_fresh_backend(db_path, surb_config).await
}
}
+1 -12
View File
@@ -17,26 +17,15 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -1,7 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{io, path::PathBuf};
use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -29,6 +30,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -41,6 +43,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -15,11 +15,9 @@ use sqlx::{
};
use std::path::Path;
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)]
pub struct StorageManager {
connection_pool: SqlitePoolGuard,
pub connection_pool: sqlx::SqlitePool,
}
// all SQL goes here
@@ -39,7 +37,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -51,15 +49,11 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&*connection_pool)
.run(&connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
@@ -67,43 +61,38 @@ impl StorageManager {
Ok(StorageManager { connection_pool })
}
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
.map(|r| r.is_some())
}
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
@@ -111,14 +100,14 @@ impl StorageManager {
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
@@ -126,21 +115,21 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -152,21 +141,21 @@ impl StorageManager {
stored_tag.recipient,
stored_tag.tag
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -182,14 +171,14 @@ impl StorageManager {
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -204,7 +193,7 @@ impl StorageManager {
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
@@ -222,17 +211,17 @@ impl StorageManager {
"#,
sender_id
)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
@@ -250,7 +239,7 @@ impl StorageManager {
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -264,7 +253,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
}
@@ -278,7 +267,7 @@ impl StorageManager {
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&*self.connection_pool).await?;
).execute(&self.connection_pool).await?;
Ok(())
}
}
@@ -1,21 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{
backend::fs_backend::{
manager::StorageManager,
models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
UsedSenderTags,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
@@ -44,17 +41,15 @@ impl Backend {
}
let manager = StorageManager::init(database_path, true).await?;
match manager.create_status_table().await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(err) => {
manager.close_pool().await;
Err(err.into())
}
}
manager.create_status_table().await?;
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
};
Ok(backend)
}
pub async fn try_load<P: AsRef<Path>>(
@@ -69,28 +64,7 @@ impl Backend {
}
let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager, fresh_sender_tags).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(
manager: &StorageManager,
fresh_sender_tags: bool,
) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
@@ -152,11 +126,20 @@ impl Backend {
manager.delete_all_tags().await?;
}
Ok(())
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.manager.close_pool().await;
self.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
@@ -169,8 +152,7 @@ impl Backend {
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
tokio::fs::rename(&self.database_path, &temp_old)
.await
fs::rename(&self.database_path, &temp_old)
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
@@ -179,10 +161,9 @@ impl Backend {
Ok(())
}
async fn remove_old(&mut self) -> Result<(), StorageError> {
fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
tokio::fs::remove_file(old_path)
.await
fs::remove_file(old_path)
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
@@ -354,7 +335,7 @@ impl ReplyStorageBackend for Backend {
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old().await?;
self.remove_old()?;
self.end_storage_flush().await
}
@@ -33,6 +33,7 @@ where
self.backend.load_surb_storage().await
}
// this will have to get enabled after merging develop
pub async fn flush_on_shutdown(
mut self,
mem_state: CombinedReplyStorage,
@@ -49,6 +50,7 @@ where
shutdown.recv().await;
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
@@ -5,7 +5,7 @@ use dashmap::DashMap;
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use std::io;
@@ -250,7 +250,7 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new(
address.into(),
address,
noise_config,
receiver,
initial_connection_timeout,
@@ -25,7 +25,9 @@ use nym_api_requests::models::{
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
use nym_mixnet_contract_common::EpochRewardedSet;
@@ -565,31 +567,6 @@ impl NymApiClient {
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
/// retrieve expanded information for all bonded nodes on the network
pub async fn get_all_expanded_nodes(
&self,
) -> Result<Vec<SemiSkimmedNode>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
loop {
let mut res = self
.nym_api
.get_expanded_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
@@ -18,8 +18,8 @@ use nym_api_requests::models::{
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
SemiSkimmedNode,
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
PaginatedCachedNodesResponseV2,
};
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
@@ -676,39 +676,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_expanded_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SemiSkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"semi-skimmed",
],
&params,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
+2 -9
View File
@@ -20,8 +20,6 @@ nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../sqlx-pool-guard"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -33,13 +31,8 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
persistent-storage = ["bincode", "serde"]
@@ -7,11 +7,10 @@ use crate::models::{
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
connection_pool: SqlitePoolGuard,
connection_pool: sqlx::SqlitePool,
}
impl SqliteEcashTicketbookManager {
@@ -20,7 +19,7 @@ impl SqliteEcashTicketbookManager {
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
SqliteEcashTicketbookManager { connection_pool }
}
@@ -34,7 +33,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -61,7 +60,7 @@ impl SqliteEcashTicketbookManager {
data,
expiration_date,
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
@@ -91,7 +90,7 @@ impl SqliteEcashTicketbookManager {
epoch_id,
total_tickets,
used_tickets,
).execute(&*self.connection_pool).await?;
).execute(&self.connection_pool).await?;
Ok(())
}
@@ -106,7 +105,7 @@ impl SqliteEcashTicketbookManager {
"#,
)
.bind(data)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await?
.is_some();
@@ -122,7 +121,7 @@ impl SqliteEcashTicketbookManager {
FROM ecash_ticketbook
"#,
)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -144,7 +143,7 @@ impl SqliteEcashTicketbookManager {
ticketbook_id,
expected_current_total_spent
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
@@ -154,7 +153,7 @@ impl SqliteEcashTicketbookManager {
&self,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -166,7 +165,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -183,7 +182,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -209,7 +208,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -226,7 +225,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -252,7 +251,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id,
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -270,7 +269,7 @@ impl SqliteEcashTicketbookManager {
"#,
expiration_date
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -299,7 +298,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
expiration_date
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -37,7 +37,6 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use sqlx_pool_guard::SqlitePoolGuard;
use std::path::Path;
use zeroize::Zeroizing;
@@ -55,15 +54,15 @@ impl PersistentStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -75,17 +74,13 @@ impl PersistentStorage {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
})
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ use hkdf::{
},
Hkdf,
};
use serde::{Deserialize, Serialize};
use sha2::{Sha256, Sha512};
pub use hkdf::InvalidLength;
@@ -60,7 +61,7 @@ where
/// // Prepare for the next derivation
/// let next_material = material.next();
/// ```
#[derive(ZeroizeOnDrop)]
#[derive(ZeroizeOnDrop, Serialize, Deserialize)]
pub struct DerivationMaterial {
master_key: [u8; 32],
index: u32,
+2 -2
View File
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
// TODO: we can inject here more stuff based on our gateway global config
+2 -2
View File
@@ -82,8 +82,8 @@ impl GatewayStorage {
message_retrieval_limit: i64,
) -> Result<Self, GatewayStorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
// TODO: we can inject here more stuff based on our gateway global config
+2 -28
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "network")]
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
use crate::{DenomDetails, ValidatorDetails};
pub const NETWORK_NAME: &str = "mainnet";
@@ -29,36 +29,10 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
#[cfg(feature = "network")]
pub const NYM_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_API,
front_hosts: None,
},
ApiUrlConst {
url: "https://nym-fronntdoor.vercel.app/api/",
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
#[cfg(feature = "network")]
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
pub const EXIT_POLICY_URL: &str =
-35
View File
@@ -37,37 +37,6 @@ pub struct NymNetworkDetails {
pub contracts: NymContracts,
pub explorer_api: Option<String>,
pub nym_vpn_api_url: Option<String>,
pub nym_api_urls: Option<Vec<ApiUrl>>,
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ApiUrl {
/// Expects a string formatted Url
///
/// see https://docs.rs/url/latest/url/struct.Url.html
pub url: String,
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
///
/// see https://docs.rs/url/latest/url/enum.Host.html
pub front_hosts: Option<Vec<String>>,
}
pub struct ApiUrlConst<'a> {
pub url: &'a str,
pub front_hosts: Option<&'a [&'a str]>,
}
impl From<ApiUrlConst<'_>> for ApiUrl {
fn from(value: ApiUrlConst) -> Self {
ApiUrl {
url: value.url.to_string(),
front_hosts: value
.front_hosts
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
}
}
}
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
@@ -98,8 +67,6 @@ impl NymNetworkDetails {
contracts: Default::default(),
explorer_api: Default::default(),
nym_vpn_api_url: Default::default(),
nym_api_urls: Default::default(),
nym_vpn_api_urls: Default::default(),
}
}
@@ -187,8 +154,6 @@ impl NymNetworkDetails {
},
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
nym_api_urls: None,
nym_vpn_api_urls: None,
}
}
-37
View File
@@ -1,37 +0,0 @@
[package]
name = "nym-psq-kkt"
version = "0.1.0"
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
edition = "2021"
license.workspace = true
[dependencies]
arc-swap = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
pin-project = { workspace = true }
sha2 = { workspace = true }
snow = { workspace = true }
strum = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true, features = ["codec"] }
# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
rand = "0.9.1"
[dev-dependencies]
criterion = {workspace = true}
[[bench]]
name = "benches"
harness = false
[lints]
workspace = true
-135
View File
@@ -1,135 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use criterion::{criterion_group, criterion_main, Criterion};
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use nym_psq_kkt::kkt::{
KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN,
};
use rand::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut s);
ed25519::KeyPair::from_secret(s, 0)
});
});
}
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (_, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
// initialize parties
let initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
c.bench_function("Initialize Initiator", |b| {
b.iter(|| KKTInitiator::<MlKem768>::init(initiator_ed25519_keypair.private_key()));
});
let responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
c.bench_function("Initialize Responder", |b| {
b.iter(|| {
KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
});
});
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
c.bench_function("Initiator: Generate Request", |b| {
b.iter(|| {
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
});
});
// generate request
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
c.bench_function("Responder: Ingest Request and Generate Response", |b| {
b.iter(|| {
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
});
});
// ingest request, generate response
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
c.bench_function("Initiator: Ingest Response and Store Key", |b| {
b.iter(|| {
let _ = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
});
});
// ingest response
let received_responder_key = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
}
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
kkt_benchmark
);
criterion_main!(benches);
-66
View File
@@ -1,66 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PSQError {
#[error("encountered a decryption error")]
DecryptionError,
#[error("encountered a KEM error")]
KEMError,
#[error("encountered a PSQ error")]
PSQError,
#[error("encountered an IO error: {0}")]
IoError(#[from] io::Error),
#[error("Incorrect state")]
IncorrectStateError,
#[error("Handshake did not complete")]
HandshakeError,
#[error("Unknown noise version")]
UnknownVersion,
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl From<libcrux_kem::Error> for PSQError {
fn from(err: libcrux_kem::Error) -> Self {
match err {
// Error::Decrypt => PSQError::DecryptionError,
err => PSQError::KEMError,
}
}
}
impl From<libcrux_psq::Error> for PSQError {
fn from(err: libcrux_psq::Error) -> Self {
match err {
// Error::Decrypt => PSQError::DecryptionError,
err => PSQError::PSQError,
}
}
}
#[derive(Error, Debug)]
pub enum KKTError {
#[error("Signature constructor error")]
SigConstructorError,
#[error("Signature verification error")]
SigVerifError,
// #[error("Protocol did not complete")]
// ProtocolError,
// #[error("encountered an IO error: {0}")]
// IoError(#[from] io::Error),
// #[error("Handshake timeout")]
// HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
-227
View File
@@ -1,227 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::marker::PhantomData;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use libcrux_kem::MlKem768PublicKey;
use libcrux_psq::{impls::MlKem768, traits::PSQ};
use libcrux_traits::kem::KEM;
use crate::error::KKTError;
const REQ_STR: &[u8] = "KEM_REQ".as_bytes();
const RES_STR: &[u8] = "KEM_RES".as_bytes();
const REQ_LEN: usize = REQ_STR.len() + KKT_TAG_LEN;
const RES_LEN_MLKEM768: usize = REQ_LEN + MlKem768PublicKey::len();
pub const KKT_TAG_LEN: usize = 16;
pub const KKT_REQ_LEN: usize = REQ_LEN + ed25519::SIGNATURE_LENGTH;
pub const KKT_RES_LEN_MLKEM768: usize = RES_LEN_MLKEM768 + ed25519::SIGNATURE_LENGTH;
pub struct KKTInitiator<'a, T: PSQ> {
signing_key: &'a ed25519::PrivateKey,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> KKTInitiator<'a, T> {
pub fn init(signing_key: &'a ed25519::PrivateKey) -> Self {
Self {
signing_key,
_t: PhantomData,
}
}
pub fn request_kem_pk(
&self,
request_buffer: &mut [u8; KKT_REQ_LEN],
tag_buffer: &mut [u8; KKT_TAG_LEN],
) {
// request_buffer[0..7] <- request string
request_buffer[0..REQ_STR.len()].copy_from_slice(REQ_STR);
// tag_buffer <- generate tag
rand::rng().fill_bytes(tag_buffer);
// request_buffer[7..23] <- tag
request_buffer[REQ_STR.len()..REQ_LEN].copy_from_slice(tag_buffer);
// sig <- Sign request_buffer[0..23]
let sig = self.signing_key.sign(&request_buffer[0..REQ_LEN]);
// request_buffer[23..87] <- sig
request_buffer[REQ_LEN..].copy_from_slice(&sig.to_bytes());
}
}
impl<'a> KKTInitiator<'a, MlKem768> {
pub fn ingest_response_kem_pk<T: PSQ>(
&self,
response: &[u8],
tag: &[u8; KKT_TAG_LEN],
responder_verification_key: &ed25519::PublicKey,
) -> Result<libcrux_kem::PublicKey, KKTError> {
// TODO: Refactor asserts into errors
// Check size of message
assert_eq!(response.len(), KKT_RES_LEN_MLKEM768);
// Check if the response string is there at the start of the message
assert_eq!(&response[0..RES_STR.len()], RES_STR);
// Check if the tag is the one we expect
assert_eq!(
&response[RES_STR.len()..RES_STR.len() + KKT_TAG_LEN],
&tag[..]
);
// Attempt to reconstruct a signature from the received bytes
match ed25519::Signature::from_bytes(&response[RES_LEN_MLKEM768..]) {
Ok(sig) => {
// Attempt to verify signature
match responder_verification_key.verify(&response[0..RES_LEN_MLKEM768], &sig) {
Ok(()) => {
// Extract key from bytes
// (has to be an owned sized array, unless I'm missing some function that works with slices)
let mut key_bytes: [u8; MlKem768PublicKey::len()] =
[0u8; MlKem768PublicKey::len()];
key_bytes.copy_from_slice(
&response[RES_STR.len() + KKT_TAG_LEN..RES_LEN_MLKEM768],
);
// Create key from bytes and return it
Ok(libcrux_kem::PublicKey::MlKem768(MlKem768PublicKey::from(
key_bytes,
)))
}
Err(_) => Err(KKTError::SigVerifError),
}
}
Err(_) => Err(KKTError::SigConstructorError),
}
}
}
pub struct KKTResponder<'a, T: PSQ> {
signing_key: &'a ed25519::PrivateKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
}
impl<'a> KKTResponder<'a, MlKem768> {
pub fn init(
signing_key: &'a ed25519::PrivateKey,
kem_public_key: &'a libcrux_kem::PublicKey,
) -> Self {
Self {
signing_key,
kem_public_key,
}
}
pub fn respond_kem_pk(
&self,
response_buffer: &mut [u8; KKT_RES_LEN_MLKEM768],
initiator_verification_key: &ed25519::PublicKey,
request: &[u8],
) -> Result<(), KKTError> {
// TODO: Refactor asserts into errors
// Check request size
assert_eq!(request.len(), KKT_REQ_LEN);
// Check request string
assert_eq!(&request[0..REQ_STR.len()], REQ_STR);
match ed25519::Signature::from_bytes(&request[REQ_LEN..]) {
Ok(sig) => match initiator_verification_key.verify(&request[0..REQ_LEN], &sig) {
Ok(()) => {
// response_buffer[0..7] <- RES_STR (7 bytes)
response_buffer[0..RES_STR.len()].copy_from_slice(RES_STR);
// response_buffer[7..23] <- tag (16 bytes, sent by initiator)
response_buffer[RES_STR.len()..REQ_LEN]
.copy_from_slice(&request[REQ_STR.len()..REQ_LEN]);
// response_buffer[23..1207] <- MlKem768 Public Key (1184 bytes)
response_buffer[REQ_LEN..RES_LEN_MLKEM768]
.copy_from_slice(self.kem_public_key.encode().as_slice());
// sign response_buffer[0..1207]
let sig = self.signing_key.sign(&response_buffer[0..RES_LEN_MLKEM768]);
// response_buffer[1207..1271] <- signature (64 bytes)
response_buffer[RES_LEN_MLKEM768..].copy_from_slice(&sig.to_bytes());
Ok(())
}
Err(_) => Err(KKTError::SigConstructorError),
},
Err(_) => Err(KKTError::SigVerifError),
}
}
}
#[cfg(test)]
mod test {
use crate::kkt::{KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN};
use super::{KKTInitiator, KKTResponder};
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
#[test]
fn test_kkt_e2e() {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (_, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
// initialize parties
let initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
let responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
// generate request
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
// ingest request, generate response
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
// ingest response
let received_responder_key = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
}
}
-94
View File
@@ -1,94 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod kkt;
pub mod psq;
#[cfg(test)]
mod test {
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use crate::{
kkt::{KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN},
psq::{PSQInitiator, PSQResponder},
};
#[test]
fn test_kkt_psq_e2e() {
let mut rng = rand::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
// initialize parties
let kkt_initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
let kkt_responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
// generate request
kkt_initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
// ingest request, generate response
kkt_responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
// ingest response
let received_responder_key = kkt_initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
let mut psq_initiator: PSQInitiator<MlKem768> =
PSQInitiator::init(&initiator_ed25519_keypair);
let psq_responder: PSQResponder<MlKem768> =
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
let initiator_psq_msg = psq_initiator
.initiator_message(&mut rng, &received_responder_key)
.unwrap();
let (responder_psk, responder_psq_msg) = psq_responder
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_psq_msg)
.unwrap();
let initiator_psk = psq_initiator.finalize(&responder_psq_msg).unwrap();
assert_eq!(initiator_psk, responder_psk);
}
}
-182
View File
@@ -1,182 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{marker::PhantomData, time::Duration};
use libcrux_psq::{
cred::Ed25519,
psk_registration::{Initiator, InitiatorMsg, Responder, ResponderMsg},
traits::PSQ,
};
use libcrux_traits::kem::KEM;
use nym_crypto::asymmetric::ed25519;
use rand::CryptoRng;
use crate::error::PSQError;
pub const PSK_LENGTH: usize = 32;
pub struct PSQInitiator<'a, T: PSQ> {
signing_keypair: &'a ed25519::KeyPair,
state: Option<Initiator>,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> PSQInitiator<'a, T> {
pub fn init(signing_keypair: &'a ed25519::KeyPair) -> Self {
Self {
signing_keypair,
state: None,
_t: PhantomData,
}
}
pub fn initiator_message(
&mut self,
rng: &mut impl CryptoRng,
responder_kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
) -> Result<InitiatorMsg<T::InnerKEM>, PSQError> {
let (state, message) = Initiator::send_initial_message::<Ed25519, T>(
&[0u8],
Duration::from_secs(3600),
&responder_kem_public_key,
&self.signing_keypair.private_key().to_bytes(),
&self.signing_keypair.public_key().to_bytes(),
rng,
)?;
self.state = Some(state);
Ok(message)
}
pub fn finalize(&self, responder_message: &ResponderMsg) -> Result<[u8; PSK_LENGTH], PSQError> {
match &self.state {
Some(state) => match state.complete_handshake(responder_message) {
Ok(psk) => Ok(psk.psk),
Err(err) => Err(PSQError::KEMError),
},
None => Err(PSQError::IncorrectStateError),
}
}
}
pub struct PSQResponder<'a, T: PSQ> {
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> PSQResponder<'a, T> {
pub fn init(
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
) -> Self {
Self {
kem_private_key,
kem_public_key,
_t: PhantomData,
}
}
pub fn responder_msg(
&self,
initiator_verification_key: &'a ed25519::PublicKey,
initiator_message: &InitiatorMsg<T::InnerKEM>,
) -> Result<([u8; PSK_LENGTH], ResponderMsg), PSQError> {
let (registered_psk, responder_msg) = Responder::send::<Ed25519, T>(
&[0u8],
Duration::from_secs(3600),
&[0u8],
self.kem_public_key,
self.kem_private_key,
&initiator_verification_key.to_bytes(),
initiator_message,
)?;
Ok((registered_psk.psk, responder_msg))
}
}
#[cfg(test)]
mod test {
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
use libcrux_psq::traits::PSQ;
use libcrux_traits::kem::KEM;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use super::{PSQInitiator, PSQResponder};
fn test_helper<T: PSQ>(
rng: &mut impl CryptoRng,
responder_kem_private_key: <T::InnerKEM as KEM>::DecapsulationKey,
responder_kem_public_key: <T::InnerKEM as KEM>::EncapsulationKey,
) {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut initiator: PSQInitiator<T> = PSQInitiator::init(&initiator_ed25519_keypair);
let responder: PSQResponder<T> =
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
let initiator_msg = initiator
.initiator_message(rng, &responder_kem_public_key)
.unwrap();
let (responder_psk, responder_msg) = responder
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_msg)
.unwrap();
let initiator_psk = initiator.finalize(&responder_msg).unwrap();
assert_eq!(initiator_psk, responder_psk);
}
#[test]
fn test_psq_e2e_mlkem() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
test_helper::<MlKem768>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
#[test]
fn test_psq_e2e_xwing() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng())
.unwrap();
test_helper::<XWingKemDraft06>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
#[test]
fn test_psq_e2e_dhkem() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap();
test_helper::<X25519>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
}
+7
View File
@@ -22,5 +22,12 @@ tokio-util = { workspace = true, features = ["codec"] }
nym-crypto = { path = "../crypto" }
nym-noise-keys = { path = "keys" }
[dev-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
[lints]
workspace = true
+8 -5
View File
@@ -8,22 +8,25 @@ use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(from = "u8", into = "u8")]
pub enum NoiseVersion {
V1 = 1,
Unknown, //Implies a newer version we don't know
V1,
Unknown(u8), //Implies a newer version we don't know
}
impl From<u8> for NoiseVersion {
fn from(value: u8) -> Self {
match value {
1 => NoiseVersion::V1,
_ => NoiseVersion::Unknown,
other => NoiseVersion::Unknown(other),
}
}
}
impl From<NoiseVersion> for u8 {
fn from(version: NoiseVersion) -> Self {
version as u8
match version {
NoiseVersion::V1 => 1,
NoiseVersion::Unknown(other) => other,
}
}
}
@@ -31,7 +34,7 @@ impl From<NoiseVersion> for u8 {
pub struct VersionedNoiseKey {
#[schemars(with = "u8")]
#[schema(value_type = u8)]
pub version: NoiseVersion,
pub supported_version: NoiseVersion,
#[schemars(with = "String")]
#[serde(with = "bs58_x25519_pubkey")]
+8 -6
View File
@@ -13,17 +13,19 @@ use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use snow::params::NoiseParams;
use strum::EnumIter;
use strum::{EnumIter, FromRepr};
#[derive(Default, Debug, Clone, Copy, EnumIter)]
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
#[non_exhaustive]
pub enum NoisePattern {
#[default]
XKpsk3,
IKpsk2,
XKpsk3 = 1,
IKpsk2 = 2,
}
impl NoisePattern {
pub(crate) fn as_str(&self) -> &'static str {
pub(crate) const fn as_str(&self) -> &'static str {
match self {
Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256",
Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though)
@@ -79,7 +81,7 @@ impl NoiseNetworkView {
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKey>) {
let noise_support = new
.iter()
.map(|(s_addr, key)| (s_addr.ip(), key.version))
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
.collect::<HashMap<_, _>>();
self.keys.inner.store(Arc::new(new));
self.support.inner.store(Arc::new(noise_support));
+16 -22
View File
@@ -4,30 +4,21 @@
use std::io;
use pin_project::pin_project;
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::stream::NoiseStream;
//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream
#[pin_project(project = ConnectionProj)]
pub enum Connection {
Tcp(#[pin] TcpStream),
Noise(#[pin] NoiseStream),
pub enum Connection<C> {
Raw(#[pin] C),
Noise(#[pin] Box<NoiseStream<C>>),
}
impl Connection {
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
match self {
Self::Noise(stream) => stream.peer_addr(),
Self::Tcp(stream) => stream.peer_addr(),
}
}
}
impl AsyncRead for Connection {
impl<C> AsyncRead for Connection<C>
where
C: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -35,12 +26,15 @@ impl AsyncRead for Connection {
) -> std::task::Poll<io::Result<()>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_read(cx, buf),
ConnectionProj::Tcp(stream) => stream.poll_read(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_read(cx, buf),
}
}
}
impl AsyncWrite for Connection {
impl<C> AsyncWrite for Connection<C>
where
C: AsyncWrite + AsyncRead + Unpin,
{
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -48,7 +42,7 @@ impl AsyncWrite for Connection {
) -> std::task::Poll<Result<usize, io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_write(cx, buf),
ConnectionProj::Tcp(stream) => stream.poll_write(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_write(cx, buf),
}
}
fn poll_flush(
@@ -57,7 +51,7 @@ impl AsyncWrite for Connection {
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_flush(cx),
ConnectionProj::Tcp(stream) => stream.poll_flush(cx),
ConnectionProj::Raw(stream) => stream.poll_flush(cx),
}
}
@@ -67,7 +61,7 @@ impl AsyncWrite for Connection {
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_shutdown(cx),
ConnectionProj::Tcp(stream) => stream.poll_shutdown(cx),
ConnectionProj::Raw(stream) => stream.poll_shutdown(cx),
}
}
}
+54 -2
View File
@@ -1,6 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::NoiseVersion;
use snow::Error;
use std::io;
use thiserror::Error;
@@ -22,13 +23,64 @@ pub enum NoiseError {
#[error("Handshake did not complete")]
HandshakeError,
#[error("Unknown noise version")]
UnknownVersion,
#[error("unknown noise version (encoded value: {encoded})")]
UnknownVersion { encoded: u8 },
#[error("unknown noise pattern (encoded value: {encoded})")]
UnknownPattern { encoded: u8 },
#[error("unknown noise message type (encoded value: {encoded})")]
UnknownMessageType { encoded: u8 },
#[error("failed to generate psk for requested version {noise_version}")]
PskGenerationFailure { noise_version: u8 },
#[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")]
UnknownVersionHandshake { noise_version: u8 },
#[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")]
UnexpectedNoisePattern {
configured: &'static str,
received: &'static str,
},
#[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedHandshakeVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedDataVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("received a non-handshake message during noise handshake")]
NonHandshakeMessageReceived,
#[error("received a non-data message post noise handshake")]
NonDataMessageReceived,
#[error("handshake message exceeded maximum size (got {size} bytes)")]
HandshakeTooBig { size: usize },
#[error("noise message exceeded maximum size (got {size} bytes)")]
DataTooBig { size: usize },
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl NoiseError {
pub(crate) fn naive_to_io_error(self) -> std::io::Error {
match self {
NoiseError::IoError(err) => err,
other => std::io::Error::other(other),
}
}
}
impl From<Error> for NoiseError {
fn from(err: Error) -> Self {
match err {
+67 -79
View File
@@ -1,55 +1,63 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::NoiseConfig;
use crate::connection::Connection;
use crate::error::NoiseError;
use crate::stream::NoiseStream;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use sha2::{Digest, Sha256};
use snow::{error::Prerequisite, Error};
use snow::error::Prerequisite;
use snow::Error;
use tokio::net::TcpStream;
use tracing::*;
use tracing::{error, warn};
pub mod config;
pub mod connection;
pub mod error;
pub mod stream;
use crate::config::NoiseConfig;
use crate::connection::Connection;
use crate::error::NoiseError;
use crate::stream::NoiseStreamBuilder;
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
fn generate_psk_v1(responder_pub_key: &x25519::PublicKey) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.finalize().into()
}
// TODO: this should be behind some trait because presumably, depending on the version,
// other arguments would be needed
mod psk_gen {
use crate::error::NoiseError;
use crate::stream::Psk;
use crate::NOISE_PSK_PREFIX;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use sha2::{Digest, Sha256};
async fn upgrade_noise_initiator_v1(
conn: TcpStream,
config: &NoiseConfig,
remote_pub_key: &x25519::PublicKey,
) -> Result<Connection, NoiseError> {
trace!("Perform Noise Handshake, initiator side");
pub(crate) fn generate_psk(
responder_pub_key: x25519::PublicKey,
version: NoiseVersion,
) -> Result<Psk, NoiseError> {
match version {
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
NoiseVersion::Unknown(noise_version) => {
Err(NoiseError::PskGenerationFailure { noise_version })
}
}
}
let secret_hash = generate_psk_v1(remote_pub_key);
let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?;
Ok(Connection::Noise(
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
))
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.finalize().into()
}
}
pub async fn upgrade_noise_initiator(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection, NoiseError> {
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Tcp(conn));
return Ok(Connection::Raw(conn));
}
//Get init material
@@ -58,46 +66,34 @@ pub async fn upgrade_noise_initiator(
Error::Prereq(Prerequisite::RemotePublicKey)
})?;
match config.get_noise_key(&responder_addr) {
Some(key) => match key.version {
NoiseVersion::V1 => upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey).await,
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
// If that node sees we're older, it will try to adapt too.
NoiseVersion::Unknown => {
warn!("{responder_addr} is announcing an unknown version of Noise, we will still attempt our latest known version");
upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey)
.await
.or(Err(NoiseError::UnknownVersion))
}
},
None => {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
Ok(Connection::Tcp(conn))
let Some(key) = config.get_noise_key(&responder_addr) else {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
return Ok(Connection::Raw(conn));
};
let handshake_version = match key.supported_version {
NoiseVersion::V1 => NoiseVersion::V1,
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
// If that node sees we're older, it will try to adapt too.
NoiseVersion::Unknown(version) => {
warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version");
LATEST_NOISE_VERSION
}
}
};
NoiseStreamBuilder::new(conn)
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
async fn upgrade_noise_responder_v1(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection, NoiseError> {
trace!("Perform Noise Handshake, responder side");
let secret_hash = generate_psk_v1(config.local_key.public_key());
let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?;
Ok(Connection::Noise(
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
))
}
pub async fn upgrade_noise_responder(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection, NoiseError> {
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Tcp(conn));
return Ok(Connection::Raw(conn));
}
//Get init material
@@ -109,22 +105,14 @@ pub async fn upgrade_noise_responder(
}
};
// Port is random and we just need the support info
match config.get_noise_support(initiator_addr.ip()) {
None => {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
Ok(Connection::Tcp(conn))
}
// responder's info on version is shaky, so ideally, initiator has to adapt.
// if we are newer, it won't ba able to, so let's try to meet him on his ground.
Some(LATEST_NOISE_VERSION) | Some(NoiseVersion::Unknown) => {
// Node is announcing the same version as us, great or
// Node is announcing a newer version than us, it should adapt to us though
upgrade_noise_responder_v1(conn, config).await
} //SW sample of code to allow backwards compatibility when we introduce new versions
// Some(IntermediateNoiseVersion) => {
// Node is announcing an older version, let's try to adapt
// upgrade_noise_responder_Vwhatever
// }
}
// if responder doesn't announce noise support, we fallback to tcp
if config.get_noise_support(initiator_addr.ip()).is_none() {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
NoiseStreamBuilder::new(conn)
.perform_responder_handshake(config)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
-224
View File
@@ -1,224 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{config::NoiseConfig, error::NoiseError};
use bytes::BytesMut;
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::x25519;
use pin_project::pin_project;
use snow::{Builder, HandshakeState, TransportState};
use std::io;
use std::pin::Pin;
use std::task::Poll;
use std::{cmp::min, task::ready};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::TcpStream,
};
use tokio_util::codec::{Framed, LengthDelimitedCodec};
const TAGLEN: usize = 16;
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
pub(crate) type Psk = [u8; 32];
/// Wrapper around a TcpStream
#[pin_project]
pub struct NoiseStream {
#[pin]
inner_stream: Framed<TcpStream, LengthDelimitedCodec>,
handshake: Option<HandshakeState>,
noise: Option<TransportState>,
dec_buffer: BytesMut,
}
impl NoiseStream {
pub(crate) fn new_initiator(
inner_stream: TcpStream,
config: &NoiseConfig,
remote_pub_key: &x25519::PublicKey,
psk: &Psk,
) -> Result<NoiseStream, NoiseError> {
let handshake = Builder::new(config.pattern.as_noise_params())
.local_private_key(config.local_key.private_key().as_bytes())
.remote_public_key(&remote_pub_key.to_bytes())
.psk(config.pattern.psk_position(), psk)
.build_initiator()?;
Ok(NoiseStream::new_inner(inner_stream, handshake))
}
pub(crate) fn new_responder(
inner_stream: TcpStream,
config: &NoiseConfig,
psk: &Psk,
) -> Result<NoiseStream, NoiseError> {
let handshake = Builder::new(config.pattern.as_noise_params())
.local_private_key(config.local_key.private_key().as_bytes())
.psk(config.pattern.psk_position(), psk)
.build_responder()?;
Ok(NoiseStream::new_inner(inner_stream, handshake))
}
fn new_inner(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream {
NoiseStream {
inner_stream: LengthDelimitedCodec::builder()
.length_field_type::<u16>()
.new_framed(inner_stream),
handshake: Some(handshake),
noise: None,
dec_buffer: BytesMut::new(),
}
}
pub(crate) async fn perform_handshake(mut self) -> Result<Self, NoiseError> {
//Check if we are in the correct state
let Some(mut handshake) = self.handshake.take() else {
return Err(NoiseError::IncorrectStateError);
};
while !handshake.is_handshake_finished() {
if handshake.is_my_turn() {
self.send_handshake_msg(&mut handshake).await?;
} else {
self.recv_handshake_msg(&mut handshake).await?;
}
}
self.noise = Some(handshake.into_transport_mode()?);
Ok(self)
}
async fn send_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
) -> Result<(), NoiseError> {
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
let len = handshake.write_message(&[], &mut buf)?;
buf.truncate(len);
self.inner_stream.send(buf.into()).await?;
Ok(())
}
async fn recv_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
) -> Result<(), NoiseError> {
match self.inner_stream.next().await {
Some(Ok(msg)) => {
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
handshake.read_message(&msg, &mut buf)?;
Ok(())
}
Some(Err(err)) => Err(NoiseError::IoError(err)),
None => Err(NoiseError::HandshakeError),
}
}
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
self.inner_stream.get_ref().peer_addr()
}
}
impl AsyncRead for NoiseStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let projected_self = self.project();
let pending = match projected_self.inner_stream.poll_next(cx) {
Poll::Pending => {
//no new data, a return value of Poll::Pending means the waking is already scheduled
//Nothing new to decrypt, only check if we can return something from dec_storage, happens after
true
}
Poll::Ready(Some(Ok(noise_msg))) => {
// We have a new noise msg
let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN);
let len = match projected_self.noise {
Some(transport_state) => {
match transport_state.read_message(&noise_msg, &mut dec_msg) {
Ok(len) => len,
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
}
}
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
};
projected_self.dec_buffer.extend(&dec_msg[..len]);
false
}
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)),
Poll::Ready(None) => {
//Stream is done, we might still have data in the buffer though, happens afterwards
false
}
};
// Checking if there is something to return from the buffer
let read_len = min(buf.remaining(), projected_self.dec_buffer.len());
if read_len > 0 {
buf.put_slice(&projected_self.dec_buffer.split_to(read_len));
return Poll::Ready(Ok(()));
}
// buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream
if pending {
//If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled
Poll::Pending
} else {
Poll::Ready(Ok(()))
}
}
}
impl AsyncWrite for NoiseStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
let mut projected_self = self.project();
// returns on Poll::Pending and Poll:Ready(Err)
ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?;
// Ready to send, encrypting message
let mut noise_buf = BytesMut::zeroed(buf.len() + TAGLEN);
let Ok(len) = (match projected_self.noise {
Some(transport_state) => transport_state.write_message(buf, &mut noise_buf),
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
}) else {
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
};
noise_buf.truncate(len);
// Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104
match projected_self
.inner_stream
.as_mut()
.start_send(noise_buf.into())
{
Ok(()) => Poll::Ready(Ok(buf.len())),
Err(e) => Poll::Ready(Err(e)),
}
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner_stream.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner_stream.poll_close(cx)
}
}
+26 -1
View File
@@ -895,15 +895,39 @@ pub struct HostKeys {
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SphinxKey {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub public_key: x25519::PublicKey,
}
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
SphinxKey {
rotation_id: value.rotation_id,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
@@ -1084,10 +1108,11 @@ impl NymNodeDescription {
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(role, performance);
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
+1 -1
View File
@@ -8,7 +8,7 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{Interval, NodeId};
use nym_mixnet_contract_common::{EpochId, Interval, NodeId};
use nym_noise_keys::VersionedNoiseKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -1,97 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::models::AxumResult;
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation};
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::state::AppState;
use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole;
use axum::extract::{Query, State};
use nym_api_requests::models::{
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
};
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode};
use nym_api_requests::pagination::PaginatedResponse;
use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode};
use nym_http_api_common::FormattedResponse;
use nym_mixnet_contract_common::NodeId;
use nym_topology::CachedEpochRewardedSet;
use std::collections::HashMap;
use tracing::trace;
use utoipa::ToSchema;
pub type PaginatedSemiSkimmedNodes =
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SemiSkimmedNode>>>;
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
fn build_nym_nodes_response<'a, NI>(
rewarded_set: &CachedEpochRewardedSet,
nym_nodes_subset: NI,
annotations: &HashMap<NodeId, NodeAnnotation>,
active_only: bool,
) -> Vec<SemiSkimmedNode>
where
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
let node_id = nym_node.node_id;
let role: NodeRole = rewarded_set.role(node_id).into();
// if the role is inactive, see if our filter allows it
if active_only && role.is_inactive() {
continue;
}
// honestly, not sure under what exact circumstances this value could be missing,
// but in that case just use 0 performance
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
nodes.push(nym_node.to_semi_skimmed_node(role, annotation.last_24h_performance));
}
nodes
}
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
fn add_legacy<LN>(
nodes: &mut Vec<SemiSkimmedNode>,
rewarded_set: &CachedEpochRewardedSet,
describe_cache: &DescribedNodes,
annotated_legacy_nodes: &HashMap<NodeId, LN>,
) where
LN: LegacyAnnotation,
{
for (node_id, legacy) in annotated_legacy_nodes.iter() {
let role: NodeRole = rewarded_set.role(*node_id).into();
// if we have self-described info, prefer it over contract data
if let Some(described) = describe_cache.get_node(node_id) {
nodes.push(described.to_semi_skimmed_node(role, legacy.performance()))
} else {
match legacy.try_to_semi_skimmed_node(role) {
Ok(node) => nodes.push(node),
Err(err) => {
let id = legacy.identity();
trace!("node {id} is malformed: {err}")
}
}
}
}
}
#[allow(dead_code)] // not dead, used in OpenAPI docs
#[derive(ToSchema)]
#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")]
pub struct PaginatedCachedNodesExpandedResponseSchema {
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
#[schema(value_type = SemiSkimmedNode)]
pub nodes: PaginatedResponse<SemiSkimmedNode>,
}
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
/// that are currently bonded.
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
@@ -99,43 +15,13 @@ pub struct PaginatedCachedNodesExpandedResponseSchema {
path = "",
context_path = "/v1/unstable/nym-nodes/semi-skimmed",
responses(
(status = 200, content(
(PaginatedCachedNodesExpandedResponseSchema = "application/json"),
(PaginatedCachedNodesExpandedResponseSchema = "application/yaml"),
(PaginatedCachedNodesExpandedResponseSchema = "application/bincode")
))
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
(status = 501)
)
)]
pub(super) async fn nodes_expanded(
state: State<AppState>,
query_params: Query<NodesParamsWithRole>,
) -> PaginatedSemiSkimmedNodes {
// 1. grab all relevant described nym-nodes
let rewarded_set = state.rewarded_set().await?;
let describe_cache = state.describe_nodes_cache_data().await?;
let all_nym_nodes = describe_cache.all_nym_nodes();
let annotations = state.node_annotations().await?;
let legacy_mixnodes = state.legacy_mixnode_annotations().await?;
let legacy_gateways = state.legacy_gateways_annotations().await?;
let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, false);
// add legacy gateways to the response
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways);
// add legacy mixnodes to the response
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes);
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
legacy_mixnodes.timestamp(),
legacy_gateways.timestamp(),
]);
let output = query_params.output.unwrap_or_default();
Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes)))
pub(crate) async fn nodes_expanded(
_state: State<AppState>,
_query_params: Query<NodesParamsWithRole>,
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
Err(AxumErrorResponse::not_implemented())
}
+46 -6
View File
@@ -6,6 +6,7 @@ use log::{info, warn};
use nym_bin_common::bin_info;
use nym_client_core::config::ForgetMe;
use nym_crypto::asymmetric::ed25519::PrivateKey;
use nym_crypto::hkdf::DerivationMaterial;
use nym_network_defaults::setup_env;
use nym_network_defaults::var_names::NYM_API;
use nym_sdk::mixnet::{self, MixnetClient};
@@ -13,6 +14,7 @@ use nym_sphinx::chunking::monitoring;
use nym_topology::{HardcodedTopologyProvider, NymTopology, NymTopologyMetadata};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::time::Duration;
use std::{
@@ -21,6 +23,7 @@ use std::{
str::FromStr,
sync::Arc,
};
use tokio::sync::Mutex;
use tokio::sync::OnceCell;
use tokio::{signal::ctrl_c, sync::RwLock};
use tokio_util::sync::CancellationToken;
@@ -45,6 +48,7 @@ async fn make_clients(
n_clients: usize,
lifetime: u64,
topology: NymTopology,
derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) {
loop {
let spawned_clients = clients.read().await.len();
@@ -71,7 +75,12 @@ async fn make_clients(
}
}
info!("Spawning new client");
let client = match make_client(topology.clone()).await {
let client = match make_client(
topology.clone(),
derivation_material.as_ref().map(Arc::clone),
)
.await
{
Ok(client) => client,
Err(err) => {
warn!("{}, moving on", err);
@@ -85,16 +94,29 @@ async fn make_clients(
}
}
async fn make_client(topology: NymTopology) -> Result<MixnetClient> {
/// Creates a new mixnet client, optionally using shared derivation material
/// for deterministic key generation. This allows multiple monitor clients
/// to derive keys from the same source while maintaining thread safety.
async fn make_client(
topology: NymTopology,
derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
) -> Result<MixnetClient> {
let net = mixnet::NymNetworkDetails::new_from_env();
let topology_provider = Box::new(HardcodedTopologyProvider::new(topology));
let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral()
let mut mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral()
.network_details(net)
.custom_topology_provider(topology_provider)
.debug_config(mixnet_debug_config(0))
.with_forget_me(ForgetMe::new_all())
// .enable_credentials_mode()
.build()?;
.with_forget_me(ForgetMe::new_all());
// Configure the client with shared derivation material if available
// This ensures all monitor clients use the same key derivation source
if let Some(derivation_material) = derivation_material {
mixnet_client =
mixnet_client.with_shared_derivation_material(Arc::clone(&derivation_material));
}
let mixnet_client = mixnet_client.build()?;
let client = mixnet_client.connect_to_mixnet().await?;
Ok(client)
@@ -138,6 +160,12 @@ struct Args {
#[arg(long, env = "DATABASE_URL")]
database_url: Option<String>,
/// Path to a JSON file containing serialized DerivationMaterial for deterministic
/// client key generation. When provided, all monitor clients will derive keys
/// from this shared material instead of generating random keys.
#[arg(long, env = "DERIVATION_MATERIAL_PATH")]
derivation_material_path: Option<PathBuf>,
}
fn generate_key_pair() -> Result<()> {
@@ -214,12 +242,24 @@ async fn main() -> Result<()> {
MIXNET_TIMEOUT.set(args.mixnet_timeout).ok();
// Load shared derivation material from file if provided
// This enables deterministic key generation across all monitor clients
let derivation_material = args
.derivation_material_path
.map(|derivation_material_path| {
let file = File::open(derivation_material_path)?;
let derivation_material: DerivationMaterial = serde_json::from_reader(file)?;
Ok::<_, anyhow::Error>(Arc::new(Mutex::new(derivation_material)))
})
.transpose()?;
let spawn_clients = Arc::clone(&clients);
tokio::spawn(make_clients(
spawn_clients,
args.n_clients,
args.client_lifetime,
TOPOLOGY.get().expect("Topology not set yet!").clone(),
derivation_material,
));
let clients_server = clients.clone();
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-api"
version = "3.1.0"
version = "3.0.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -28,7 +28,7 @@ moka = { workspace = true, features = ["future"] }
# Nym API: revert after Cheddar is out
nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["openapi"]}
nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
@@ -14,7 +14,6 @@ pub(crate) mod metrics;
pub(crate) mod mixnodes;
pub(crate) mod nym_nodes;
pub(crate) mod services;
pub(crate) mod status;
pub(crate) mod summary;
pub(crate) mod testruns;
@@ -40,8 +39,7 @@ impl RouterBuilder {
.nest("/mixnodes", mixnodes::routes())
.nest("/services", services::routes())
.nest("/summary", summary::routes())
.nest("/metrics", metrics::routes())
.nest("/status", status::routes()),
.nest("/metrics", metrics::routes()),
)
.nest(
"/explorer/v3",
@@ -1,46 +0,0 @@
use axum::{extract::State, Json, Router};
use nym_validator_client::models::BinaryBuildInformationOwned;
use tracing::instrument;
use crate::http::{
error::HttpResult,
state::{AppState, HealthInfo},
};
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/build_information", axum::routing::get(build_information))
.route("/health", axum::routing::get(health))
}
#[utoipa::path(
tag = "Status",
get,
path = "/build_information",
context_path = "/v2/status",
responses(
(status = 200, body = BinaryBuildInformationOwned)
)
)]
#[instrument(level = tracing::Level::INFO, skip_all)]
async fn build_information(
State(state): State<AppState>,
) -> HttpResult<Json<BinaryBuildInformationOwned>> {
let build_info = state.build_information().to_owned();
Ok(Json(build_info))
}
#[utoipa::path(
tag = "Status",
get,
path = "/health",
context_path = "/v2/status",
responses(
(status = 200, body = HealthInfo)
)
)]
#[instrument(level = tracing::Level::INFO, skip_all)]
async fn health(State(state): State<AppState>) -> HttpResult<Json<HealthInfo>> {
Ok(Json(state.health()))
}
@@ -1,20 +1,15 @@
use cosmwasm_std::Decimal;
use itertools::Itertools;
use moka::{future::Cache, Entry};
use nym_bin_common::bin_info_owned;
use nym_contracts_common::NaiveFloat;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_mixnet_contract_common::NodeId;
use nym_validator_client::nym_api::SkimmedNode;
use semver::Version;
use serde::Serialize;
use std::{collections::HashMap, sync::Arc, time::Duration};
use time::UtcDateTime;
use tokio::sync::RwLock;
use tracing::{error, instrument, warn};
use utoipa::ToSchema;
use super::models::SessionStats;
use crate::{
db::{queries, DbPool},
http::models::{
@@ -23,7 +18,7 @@ use crate::{
monitor::{DelegationsCache, NodeGeoCache},
};
pub(crate) use nym_validator_client::models::BinaryBuildInformationOwned;
use super::models::SessionStats;
#[derive(Debug, Clone)]
pub(crate) struct AppState {
@@ -33,7 +28,6 @@ pub(crate) struct AppState {
agent_max_count: i64,
node_geocache: NodeGeoCache,
node_delegations: Arc<RwLock<DelegationsCache>>,
bin_info: BinaryInfo,
}
impl AppState {
@@ -52,7 +46,6 @@ impl AppState {
agent_max_count,
node_geocache,
node_delegations,
bin_info: BinaryInfo::new(),
}
}
@@ -85,15 +78,6 @@ impl AppState {
.await
.delegations_owned(node_id)
}
pub(crate) fn health(&self) -> HealthInfo {
let uptime = (UtcDateTime::now() - self.bin_info.startup_time).whole_seconds();
HealthInfo { uptime }
}
pub(crate) fn build_information(&self) -> &BinaryBuildInformationOwned {
&self.bin_info.build_info
}
}
static GATEWAYS_LIST_KEY: &str = "gateways";
@@ -647,23 +631,3 @@ async fn aggregate_node_info_from_db(
Ok(parsed_nym_nodes)
}
#[derive(Debug, Clone)]
pub(crate) struct BinaryInfo {
startup_time: UtcDateTime,
build_info: BinaryBuildInformationOwned,
}
impl BinaryInfo {
fn new() -> Self {
Self {
startup_time: UtcDateTime::now(),
build_info: bin_info_owned!(),
}
}
}
#[derive(Serialize, ToSchema)]
pub(crate) struct HealthInfo {
uptime: i64,
}
+32 -128
View File
@@ -10,7 +10,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use v1::node::models::LegacyHostInformationV3;
#[cfg(feature = "client")]
pub mod client;
@@ -131,7 +130,7 @@ impl Display for ErrorResponse {
mod tests {
use super::*;
use crate::api::v1::node::models::HostInformation;
use crate::api::v1::node::models::{HostKeys, SphinxKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use rand_chacha::rand_core::SeedableRng;
@@ -141,8 +140,9 @@ mod tests {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_sphinx2 = x25519::KeyPair::new(&mut rng);
let x25519_versioned_noise = VersionedNoiseKey {
version: NoiseVersion::V1,
supported_version: NoiseVersion::V1,
x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
};
@@ -155,6 +155,11 @@ mod tests {
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
@@ -169,6 +174,11 @@ mod tests {
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(x25519_versioned_noise),
},
};
@@ -319,84 +329,6 @@ mod tests {
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v3_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_noise = x25519::KeyPair::new(&mut rng);
let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_noise: None,
},
};
//technically this variant should never happen
let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_noise: Some(*x25519_noise.public_key()),
},
};
let host_info_no_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_no_noise.ip_address.clone(),
hostname: legacy_info_no_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_no_noise.keys.ed25519_identity,
x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx,
x25519_versioned_noise: None,
},
};
let host_info_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_noise.ip_address.clone(),
hostname: legacy_info_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_noise.keys.ed25519_identity,
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx,
x25519_versioned_noise: Some(VersionedNoiseKey {
version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(),
}),
},
};
// signature on legacy data
let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key())
.unwrap()
.signature;
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct_no_noise = SignedData {
data: host_info_no_noise,
signature: signature_no_noise,
};
let current_struct_noise = SignedData {
data: host_info_noise,
signature: signature_noise,
};
assert!(!current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
assert!(!current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v2_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
@@ -430,7 +362,12 @@ mod tests {
hostname: legacy_info_no_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
@@ -441,9 +378,14 @@ mod tests {
hostname: legacy_info_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(VersionedNoiseKey {
version: NoiseVersion::V1,
supported_version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
}),
},
@@ -476,49 +418,6 @@ mod tests {
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_current_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_noise = x25519::KeyPair::new(&mut rng);
let host_info_no_noise = HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_versioned_noise: None,
},
};
let host_info_noise = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_versioned_noise: Some(VersionedNoiseKey {
version: NoiseVersion::V1,
x25519_pubkey: *x25519_noise.public_key(),
}),
},
};
// signature on legacy data
let current_struct_no_noise =
SignedData::new(host_info_no_noise, ed22519.private_key()).unwrap();
let current_struct_noise = SignedData::new(host_info_noise, ed22519.private_key()).unwrap();
assert!(current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
// if noise key is present, the signature is actually valid
assert!(current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v1_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
@@ -540,7 +439,12 @@ mod tests {
hostname: legacy_info.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(),
x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
@@ -149,6 +149,77 @@ pub struct HostKeys {
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
// we need the intermediate struct to help us with the new explicit sphinx key fields
#[allow(deprecated)]
impl From<HostKeysDeHelper> for HostKeys {
fn from(value: HostKeysDeHelper) -> Self {
let primary_x25519_sphinx_key = match value.primary_x25519_sphinx_key {
None => {
// legacy
SphinxKey::new_legacy(value.x25519_sphinx)
}
Some(primary_x25519_sphinx_key) => primary_x25519_sphinx_key,
};
HostKeys {
ed25519_identity: value.ed25519_identity,
x25519_sphinx: value.x25519_sphinx,
primary_x25519_sphinx_key,
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key,
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct HostKeysDeHelper {
/// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity.
#[serde(alias = "ed25519")]
#[serde(with = "bs58_ed25519_pubkey")]
pub ed25519_identity: ed25519::PublicKey,
#[deprecated(note = "use explicit primary_x25519_sphinx_key instead")]
#[serde(alias = "x25519")]
#[serde(with = "bs58_x25519_pubkey")]
pub x25519_sphinx: x25519::PublicKey,
/// Current, active, x25519 sphinx key clients are expected to use when constructing packets
/// with this node in the route.
pub primary_x25519_sphinx_key: Option<SphinxKey>,
/// Pre-announced x25519 sphinx key clients will use during the following key rotation
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
/// Base58-encoded x25519 public key of this node used for the noise protocol.
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SphinxKey {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub public_key: x25519::PublicKey,
}
impl SphinxKey {
pub fn new_legacy(public_key: x25519::PublicKey) -> SphinxKey {
SphinxKey {
rotation_id: u32::MAX,
public_key,
}
}
pub fn is_legacy(&self) -> bool {
self.rotation_id == u32::MAX
}
}
#[derive(Serialize)]
pub struct LegacyHostKeysV3 {
#[serde(alias = "ed25519")]
@@ -181,7 +252,7 @@ impl From<HostKeys> for LegacyHostKeysV3 {
fn from(value: HostKeys) -> Self {
LegacyHostKeysV3 {
ed25519_identity: value.ed25519_identity,
x25519_sphinx: value.x25519_sphinx,
x25519_sphinx: value.primary_x25519_sphinx_key.public_key,
x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey),
}
}
-35
View File
@@ -1,39 +1,4 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::Config;
use crate::error::NymNodeError;
use crate::node::http::api::api_requests;
use crate::node::http::error::NymNodeHttpError;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_node_requests::api::SignedHostInformation;
use nym_noise_keys::VersionedNoiseKey;
pub mod system_info;
pub(crate) fn sign_host_details(
config: &Config,
x22519_sphinx: &x25519::PublicKey,
x25519_versioned_noise: &VersionedNoiseKey,
ed22519_identity: &ed25519::KeyPair,
) -> Result<SignedHostInformation, NymNodeError> {
let x25519_versioned_noise = if config.mixnet.debug.unsafe_disable_noise {
None
} else {
Some(*x25519_versioned_noise)
};
let host_info = api_requests::v1::node::models::HostInformation {
ip_address: config.host.public_ips.clone(),
hostname: config.host.hostname.clone(),
keys: api_requests::v1::node::models::HostKeys {
ed25519_identity: *ed22519_identity.public_key(),
x25519_sphinx: *x22519_sphinx,
x25519_versioned_noise,
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key())
.map_err(NymNodeHttpError::from)?;
Ok(signed_info)
}
+1 -1
View File
@@ -484,7 +484,7 @@ impl ConnectionHandler {
pub(crate) async fn handle_stream(
&mut self,
mut mixnet_connection: Framed<Connection, NymCodec>,
mut mixnet_connection: Framed<Connection<TcpStream>, NymCodec>,
) {
loop {
tokio::select! {
+2 -10
View File
@@ -702,16 +702,6 @@ impl NymNode {
}
pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> {
let host_details = sign_host_details(
&self.config,
self.x25519_sphinx_keys.public_key(),
&VersionedNoiseKey {
version: nym_noise::LATEST_NOISE_VERSION,
x25519_pubkey: *self.x25519_noise_keys.public_key(),
},
&self.ed25519_identity_keys,
)?;
let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails {
location: self.config.host.location,
announce_ports: AnnouncePorts {
@@ -1161,6 +1151,8 @@ impl NymNode {
let network_refresher = self.build_network_refresher().await?;
let active_clients_store = ActiveClientsStore::new();
let bloomfilters_manager = self.setup_replay_detection().await?;
let noise_config = nym_noise::config::NoiseConfig::new(
self.x25519_noise_keys.clone(),
network_refresher.noise_view(),
+26 -16
View File
@@ -16,10 +16,13 @@ use nym_topology::{
TopologyProvider,
};
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode};
use nym_validator_client::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata,
};
use nym_validator_client::{NymApiClient, ValidatorClientError};
use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
@@ -61,7 +64,9 @@ impl NodesQuerier {
res
}
async fn current_nymnodes(&mut self) -> Result<Vec<SemiSkimmedNode>, ValidatorClientError> {
async fn current_nymnodes(
&mut self,
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
let res = self
.client
.get_all_expanded_nodes()
@@ -143,20 +148,24 @@ impl TopologyProvider for CachedTopologyProvider {
let network_guard = self.cached_network.inner.read().await;
let self_node = self.gateway_node.identity_key;
let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone())
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}),
);
let mut topology = NymTopology::new(
network_guard.topology_metadata,
network_guard.rewarded_set.clone(),
Vec::new(),
)
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}),
);
if !topology.has_node(self.gateway_node.identity_key) {
debug!("{self_node} didn't exist in topology. inserting it.",);
@@ -187,6 +196,7 @@ impl CachedNetwork {
struct CachedNetworkInner {
rewarded_set: EpochRewardedSet,
topology_metadata: NymTopologyMetadata,
network_nodes: Vec<SemiSkimmedNode>,
}
@@ -55,7 +55,5 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails {
},
explorer_api: parse_optional_str(EXPLORER_API),
nym_vpn_api_url: None,
nym_vpn_api_urls: None,
nym_api_urls: None,
}
}
+1 -1
View File
@@ -459,7 +459,7 @@ impl fmt::Display for OptionalValidators {
.as_ref()
.map(|validators| format!(",\nsandbox: [\n{}\n]", validators.iter().format("\n")))
.unwrap_or_default();
write!(f, "{s1}{s2}")
write!(f, "{s1}{s2}{s3}")
}
}
+28 -1
View File
@@ -39,6 +39,7 @@ use std::path::Path;
use std::path::PathBuf;
#[cfg(unix)]
use std::sync::Arc;
use tokio::sync::Mutex;
use url::Url;
use zeroize::Zeroizing;
@@ -67,6 +68,9 @@ pub struct MixnetClientBuilder<S: MixnetClientStorage = Ephemeral> {
forget_me: ForgetMe,
remember_me: RememberMe,
derivation_material: Option<DerivationMaterial>,
// Shared derivation material for thread-safe access across multiple clients
// Wrapped in Arc<Mutex<>> to allow concurrent access while maintaining safety
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
}
impl MixnetClientBuilder<Ephemeral> {
@@ -106,6 +110,7 @@ impl MixnetClientBuilder<OnDiskPersistent> {
forget_me: Default::default(),
remember_me: Default::default(),
derivation_material: None,
shared_derivation_material: None,
})
}
}
@@ -140,6 +145,7 @@ where
forget_me: Default::default(),
remember_me: Default::default(),
derivation_material: None,
shared_derivation_material: None,
}
}
@@ -163,6 +169,7 @@ where
forget_me: self.forget_me,
remember_me: self.remember_me,
derivation_material: self.derivation_material,
shared_derivation_material: self.shared_derivation_material,
}
}
@@ -172,6 +179,18 @@ where
self
}
/// Set shared derivation material for deterministic key generation across multiple clients.
/// This allows multiple client instances to derive keys from the same source material
/// while ensuring thread-safe access through Arc<Mutex<>>.
#[must_use]
pub fn with_shared_derivation_material(
mut self,
derivation_material: Arc<Mutex<DerivationMaterial>>,
) -> Self {
self.shared_derivation_material = Some(derivation_material);
self
}
/// Change the underlying storage of this builder to use default implementation of on-disk disk_persistence.
#[must_use]
pub fn set_default_storage(
@@ -335,6 +354,7 @@ where
client.forget_me = self.forget_me;
client.remember_me = self.remember_me;
client.derivation_material = self.derivation_material;
client.shared_derivation_material = self.shared_derivation_material;
Ok(client)
}
}
@@ -394,6 +414,11 @@ where
/// The derivation material to use for the client keys, its up to the caller to save this for rederivation later
derivation_material: Option<DerivationMaterial>,
/// Shared derivation material that can be safely accessed across multiple threads/clients.
/// This is useful when multiple clients need to derive keys from the same source while
/// maintaining thread safety through Arc<Mutex<>> wrapping.
shared_derivation_material: Option<Arc<Mutex<DerivationMaterial>>>,
}
impl<S> DisconnectedMixnetClient<S>
@@ -451,6 +476,7 @@ where
forget_me,
remember_me,
derivation_material: None,
shared_derivation_material: None,
})
}
@@ -678,7 +704,8 @@ where
.with_wait_for_gateway(self.wait_for_gateway)
.with_forget_me(&self.forget_me)
.with_remember_me(&self.remember_me)
.with_derivation_material(self.derivation_material);
.with_derivation_material(self.derivation_material)
.with_shared_derivation_material(self.shared_derivation_material);
if let Some(user_agent) = self.user_agent {
base_builder = base_builder.with_user_agent(user_agent);
-36
View File
@@ -1,36 +0,0 @@
[package]
name = "sqlx-pool-guard"
version = "0.1.0"
edition = "2024"
license.workspace = true
[lints]
workspace = true
[dependencies]
tracing.workspace = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["rt-multi-thread", "macros", "time", "fs"]
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
proc_pidinfo.workspace = true
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32",
"Win32_System",
"Win32_System_Memory",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
"Wdk_System_SystemInformation",
] }
[dev-dependencies]
tempfile.workspace = true
tracing-subscriber.workspace = true
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{io, path::Path};
use proc_pidinfo::{
ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self,
};
/// Check if there are no open file descriptors for the given files.
///
/// Uses `proc_pidinfo` (`sys/proc_info.h`)
/// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/
pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> {
let fd_list = proc_pidinfo_list_self::<ProcFDInfo>()?;
for fd in fd_list
.iter()
.filter(|s| s.fd_type() == Ok(ProcFDType::VNODE))
{
let Some(vnode) = proc_pidfdinfo_self::<VnodeFdInfoWithPath>(fd.proc_fd)
.inspect_err(|e| {
tracing::warn!("proc_pidfdinfo_self::<VnodeFdInfoWithPath>() failure: {e}");
})
.ok()
.flatten()
else {
continue;
};
if let Ok(true) = vnode
.path()
.map(|vnode_path| file_paths.contains(&vnode_path))
.inspect_err(|e| {
tracing::warn!("vnode.path() failure: {e:?}");
})
{
return Ok(false);
}
}
Ok(true)
}
-175
View File
@@ -1,175 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{
io,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
time::Duration,
};
#[cfg(windows)]
#[path = "windows.rs"]
mod imp;
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[path = "apple.rs"]
mod imp;
#[cfg(any(target_os = "linux", target_os = "android"))]
#[path = "linux.rs"]
mod imp;
/// Max number of retry attempts
const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 20;
/// Delay between file checks
const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100);
/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217).
/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed.
#[derive(Debug, Clone)]
pub struct SqlitePoolGuard {
/// Path to sqlite database file.
database_path: PathBuf,
/// Inner connection pool.
connection_pool: sqlx::SqlitePool,
}
impl SqlitePoolGuard {
/// Create new instance providing path to database and connection pool
pub fn new(database_path: PathBuf, connection_pool: sqlx::SqlitePool) -> Self {
Self {
database_path,
connection_pool,
}
}
/// Returns database path
pub fn database_path(&self) -> &Path {
&self.database_path
}
/// Close udnerlying sqlite pool and wait for files to be closed before returning.
pub async fn close(&self) {
// Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database.
if !self.connection_pool.is_closed() {
tracing::info!("Closing sqlite pool: {}", self.database_path.display());
self.close_pool_inner().await.ok();
}
}
async fn close_pool_inner(&self) -> std::io::Result<()> {
self.connection_pool.close().await;
self.wait_for_db_files_close().await.inspect_err(|e| {
tracing::error!("Failed to wait for file to close: {e}");
})
}
/// Returns all database files, including shm and wal files.
fn all_database_files(&self) -> Vec<PathBuf> {
let mut database_files = vec![];
let canonical_path = self
.database_path
.canonicalize()
.inspect_err(|e| {
tracing::error!(
"Failed to canonicalize path: {}. Cause: {e}",
self.database_path.display()
);
})
.unwrap_or(self.database_path.clone());
if let Some(ext) = canonical_path.extension() {
for added_ext in ["-shm", "-wal"] {
let mut new_ext = ext.to_owned();
new_ext.push(added_ext);
database_files.push(canonical_path.with_extension(new_ext));
}
}
database_files.push(canonical_path);
database_files
}
/// Wait for database files to be closed before returning.
async fn wait_for_db_files_close(&self) -> std::io::Result<()> {
let database_files = self.all_database_files();
let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect();
for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS {
match imp::check_files_closed(&paths)
.await
.inspect_err(|e| tracing::error!("imp::check_files_closed() failure: {e}"))
{
Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await,
Ok(true) => return Ok(()),
}
}
Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for sqlite files to be closed",
))
}
}
impl Deref for SqlitePoolGuard {
type Target = sqlx::SqlitePool;
fn deref(&self) -> &Self::Target {
&self.connection_pool
}
}
impl DerefMut for SqlitePoolGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.connection_pool
}
}
#[cfg(test)]
mod tests {
use sqlx::{
ConnectOptions, Executor,
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
};
use super::*;
#[tokio::test]
async fn test_wait_close() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.init();
let temp_dir = tempfile::tempdir().unwrap();
let database_path = temp_dir.path().join("storage.db");
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path.clone())
.create_if_missing(true)
.disable_statement_logging();
let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
connection_pool
.execute("create table test (col int)")
.await
.unwrap();
let guard = SqlitePoolGuard::new(database_path.clone(), connection_pool);
assert!(
guard
.wait_for_db_files_close()
.await
.err()
.is_some_and(|e| e.kind() == io::ErrorKind::TimedOut)
);
assert!(guard.close_pool_inner().await.is_ok());
tokio::fs::remove_file(database_path).await.unwrap();
}
}
-36
View File
@@ -1,36 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{io, path::Path};
static PROC_SELF_FD_DIR: &str = "/proc/self/fd/";
/// Check if there are no open file descriptors for the given files.
///
/// Linux, Android: uses `/proc/self/fd/` to list open file descriptors
/// See: https://stackoverflow.com/a/59797198/351305
pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> {
let mut dir = tokio::fs::read_dir(PROC_SELF_FD_DIR).await?;
while let Ok(Some(entry)) = dir.next_entry().await {
if entry
.file_type()
.await
.inspect_err(|e| tracing::warn!("entry.file_type() failure: {e}"))
.is_ok_and(|entry_type| entry_type.is_symlink())
{
match tokio::fs::read_link(entry.path()).await {
Ok(resolved_path) => {
if file_paths.contains(&resolved_path.as_ref()) {
return Ok(false);
}
}
Err(e) => {
tracing::error!("Failed to read symlink: {e}");
}
}
}
}
Ok(true)
}
-188
View File
@@ -1,188 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{
ffi::{OsString, c_uchar, c_ulong, c_ushort, c_void},
io,
os::windows::ffi::OsStringExt,
path::{Path, PathBuf},
};
use windows::{
Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS},
Win32::{
Foundation::{HANDLE, MAX_PATH, NTSTATUS, STATUS_INFO_LENGTH_MISMATCH},
Storage::FileSystem::{
FILE_NAME_NORMALIZED, FILE_TYPE_DISK, GetFileType, GetFinalPathNameByHandleW,
},
System::{
Memory::{
GetProcessHeap, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc,
},
Threading::GetCurrentProcessId,
},
},
};
/// Private information class used to retrieve open file handles
const SYSTEM_HANDLE_INFORMATION_CLASS: SYSTEM_INFORMATION_CLASS = SYSTEM_INFORMATION_CLASS(0x10);
/// Initial buffer size holding the handle info
/// The number is based on what I observe on a pretty standard Windows 11
const SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE: usize = 2_500_000;
/// Check if there are no open handles to the given files.
///
/// Uses undocumented NT API to obtain open handles on the system.
/// See: https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland
pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result<bool> {
let current_pid = unsafe { GetCurrentProcessId() };
// Allocate info struct on heap with some initial value
let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE;
let mut handle_table_info = HeapGuard::<SystemHandleInformation>::new(reserved_memory)?;
// Request system handle information
let mut status: NTSTATUS = NTSTATUS::default();
let mut return_len = reserved_memory as u32;
for _ in 0..2 {
status = unsafe {
NtQuerySystemInformation(
SYSTEM_HANDLE_INFORMATION_CLASS,
handle_table_info.as_mut_ptr() as _,
return_len,
&mut return_len,
)
};
// Buffer is too small, resize memory and retry again.
if status == STATUS_INFO_LENGTH_MISMATCH {
tracing::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}");
reserved_memory = return_len as usize;
handle_table_info.reallocate(reserved_memory)?;
} else {
break;
}
}
status.ok()?;
// Convert returned data into slice
let num_handles = unsafe { (*handle_table_info.inner).number_of_handles };
let proc_entries = unsafe {
std::slice::from_raw_parts(
(*handle_table_info.as_mut_ptr()).handles.as_ptr(),
num_handles as usize,
)
};
// Iterate over open file handle entries
for entry in proc_entries {
if entry.unique_process_id == current_pid {
let file_handle = HANDLE(entry.handle_value as _);
// Filter everything except disk files
if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK {
// Obtain canonical path for file handle
let mut file_handle_path = vec![0u16; MAX_PATH as usize];
let num_chars_without_nul = unsafe {
GetFinalPathNameByHandleW(
file_handle,
&mut file_handle_path,
FILE_NAME_NORMALIZED,
) as usize
};
if num_chars_without_nul > 0 {
let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]);
let file_handle_pathbuf = PathBuf::from(path_str);
if file_paths.contains(&file_handle_pathbuf.as_path()) {
return Ok(false);
}
}
}
}
}
Ok(true)
}
#[repr(C)]
#[derive(Copy, Clone)]
struct SystemHandleInformation {
pub number_of_handles: c_ulong,
pub handles: [SystemHandleTableEntryInfo; 1],
}
#[repr(C)]
#[derive(Copy, Clone)]
struct SystemHandleTableEntryInfo {
pub unique_process_id: c_ulong,
pub object_type_index: c_uchar,
pub handle_attributes: c_uchar,
pub handle_value: c_ushort,
pub object: *mut c_void,
pub granted_access: c_ulong,
}
/// Managed heap memory
struct HeapGuard<T> {
inner: *mut T,
process_heap: HANDLE,
}
impl<T> HeapGuard<T> {
/// Allocate new memory using `HealAlloc`
fn new(length: usize) -> io::Result<Self> {
let process_heap = unsafe { GetProcessHeap()? };
let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ };
if inner.is_null() {
Err(io::Error::other("Failed to allocate memory"))
} else {
Ok(Self {
inner,
process_heap,
})
}
}
/// Reallocate existing chunk of memory
///
/// On success: the internal memory pointer is replaced.
/// On failure: the internal memory pointer remains the same and still valid.
fn reallocate(&mut self, new_length: usize) -> io::Result<()> {
let new_ptr: *mut T = unsafe {
HeapReAlloc(
self.process_heap,
HEAP_ZERO_MEMORY,
Some(self.inner as _),
new_length,
) as _
};
if new_ptr.is_null() {
Err(io::Error::other("Failed to reallocate memory"))
} else {
self.inner = new_ptr;
Ok(())
}
}
fn as_mut_ptr(&self) -> *mut T {
self.inner
}
}
impl<T> Drop for HeapGuard<T> {
fn drop(&mut self) {
#[allow(clippy::expect_used)]
unsafe {
HeapFree(
self.process_heap,
HEAP_FLAGS(0),
Some(self.inner as *mut c_void),
)
}
.expect("HeapFree failure");
}
}
@@ -83,8 +83,6 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails {
contracts,
explorer_api: None,
nym_vpn_api_url: None,
nym_vpn_api_urls: None,
nym_api_urls: None,
}
}
}
@@ -40,8 +40,6 @@ impl InitCtx {
contracts: Default::default(),
explorer_api: None,
nym_vpn_api_url: None,
nym_vpn_api_urls: None,
nym_api_urls: None,
};
Ok(Config::try_from_nym_network_details(&network_details)?)
}