Compare commits

...

83 Commits

Author SHA1 Message Date
Georgio Nicolas 5e2135812e update benchmarking setup 2025-10-29 14:55:12 +01:00
Georgio Nicolas 48304afd3a Update manifests 2025-10-29 08:08:21 +01:00
Georgio Nicolas a66d4002a1 Add KKT 2025-10-29 08:08:03 +01:00
Georgio Nicolas 40e07e2a0c add xwing support 2025-10-07 19:11:47 +02:00
Georgio Nicolas d5194722cf Switch back to cryspen repo 2025-08-19 13:56:00 +02:00
Georgio Nicolas 7d8a31339e temporary support for modified v1 api 2025-08-08 17:23:56 +02:00
Georgio Nicolas 79bf97ceaa Tests pass 2025-08-08 12:50:11 +02:00
Georgio Nicolas 84fcf81852 update psq implementation 2025-08-08 11:07:35 +02:00
Georgio Nicolas 481ad79376 Integrated but not tested 2025-08-08 11:07:07 +02:00
Georgio Nicolas 1a9995535f temp changes 2025-07-07 16:44:45 +02:00
Andrej Mihajlov 0de4aea77b Merge pull request #5796 from nymtech/am/close-sqlite-pool
Close sqlite pool before moving or reopening databases
2025-06-17 19:01:25 +02:00
dynco-nym b9339b8f0c Add /status endpoints (#5857)
* Add /status endpoints

* Bump package version

* pub use instead of import
2025-06-16 13:19:35 +02:00
Andrej Mihajlov 43a7360399 Merge pull request #5856 from nymtech/am/remove-surb-screaming-logs
Clear out screaming logs
2025-06-16 11:39:27 +02:00
Andrej Mihajlov 5f9f7f0fac Clear out screaming logs 2025-06-13 11:00:48 +02:00
Andrej Mihajlov df0e2fe489 Merge pull request #5853 from nymtech/am/path-display
Use display when printing paths
2025-06-13 10:54:12 +02:00
benedetta davico bc33cc4c8d Merge pull request #5855 from nymtech/fix-qa-removal 2025-06-13 09:40:56 +02:00
Simon Wicky a31597aca9 fix removal of qa env 2025-06-13 09:30:00 +02:00
Jack Wampler 378229b04e HTTP Discovery objects & network defaults (#5814)
add extended (optional) fields to the NetworkDiscovery and configure fallback hosts
2025-06-12 11:15:36 -06:00
Andrej Mihajlov fec196c097 Use display when printing paths 2025-06-12 17:17:00 +02:00
Andrej Mihajlov 1d7ffc1bb6 test: remove file after closing for a test 2025-06-12 15:39:26 +02:00
Andrej Mihajlov 0caa627960 Fix missing await on self.close_pool_inner() 2025-06-12 15:12:46 +02:00
import this d6b3d7fc0a [DOCs/operators]: Release notes for v2025.11 cheddar (#5852)
* bump up version

* add dev features

* add operator updates

* add updated stats

* update prebuild
2025-06-12 11:19:00 +00:00
dynco-nym ac273480f8 Fix CI version check (#5851)
* Fix version

* Test .rc version

* Undo cargo.toml version

* Remove comment

* Apply to statistics service
2025-06-12 11:17:56 +02:00
benedettadavico 79603d61d7 fix for QA 2025-06-12 10:02:40 +02:00
dynco-nym e8e9a70ef4 Feature/node status dvpn directory (#5829)
* wip - dvpn directory cache

* Endpoint & cache

* /gateways works
- SkimmedNode data still missing
- need to move probe models to monorepo

* Rest of the data for /gateways

* Revert before merge: pin deps to cheddar release

* Filter gw by country

* Return percent string instead of u8

* Filter by semver

* Bump package version

* Fix probe types

* Reorg

* Add exit, entry endpoints

* Different entry/exit selection criteria

* Date fix migration

* Unpin from cheddar

* Revert "Unpin from cheddar"

This reverts commit f17239075b.

* Validation with celes

* PR feedback

* Fix path

* Bump version

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-06-12 09:56:31 +02:00
Tommy Verrall 0c52ee89c8 Merge pull request #5821 from nymtech/dependabot/npm_and_yarn/sdk/typescript/tests/integration-tests/mix-fetch/tar-fs-3.0.9
build(deps): bump tar-fs from 3.0.8 to 3.0.9 in /sdk/typescript/tests/integration-tests/mix-fetch
2025-06-12 08:43:47 +01:00
Tommy Verrall f324d45721 Merge pull request #5449 from indmind/patch-1
chore: fixed typo in API endpoint parameter
2025-06-12 08:42:50 +01:00
Tommy Verrall 470e88f46c Merge pull request #5843 from nymtech/dependabot/npm_and_yarn/wasm/mix-fetch/internal-dev/webpack-dev-server-5.2.1
build(deps-dev): bump webpack-dev-server from 4.13.2 to 5.2.1 in /wasm/mix-fetch/internal-dev
2025-06-12 08:41:20 +01:00
Tommy Verrall 42a5016822 Merge pull request #5845 from nymtech/remove/old-mock-nym-api-client
remove not used old mock-api
2025-06-12 08:40:35 +01:00
Tommy Verrall 579cff358d Merge pull request #5849 from nymtech/feature/remove-browser-extension
Updated browser extension piece removal
2025-06-12 08:38:38 +01:00
benedetta davico f95dda0f2f Merge pull request #5844 from nymtech/feature/remove-bity
remove bity dir
2025-06-12 09:37:19 +02:00
benedetta davico fc666fb984 Merge pull request #5848 from nymtech/remove/old-env-references
Remove/old env references
2025-06-12 09:37:08 +02:00
benedetta davico 1264fd9bfb Update ci-build.yml 2025-06-11 17:48:24 +02:00
Tommy Verrall 3e8451f292 updated browser extension piece
- keep all the internal-dev wasm pieces as future examples
- everything previously was going to be removed
- shows functioning wasm interaction with the js
2025-06-11 17:15:20 +02:00
benedetta davico 53f4582202 Merge pull request #5835 from nymtech/benny/node-version-test
Update publish-nym-binaries.yml
2025-06-11 16:39:18 +02:00
benedettadavico c7c6dcab65 remove old env references 2025-06-11 16:13:59 +02:00
benedettadavico 3422c49e85 remove qa env 2025-06-11 16:07:32 +02:00
benedettadavico deee0b8e14 remove bity integration from cargo toml 2025-06-11 16:05:03 +02:00
Tommy Verrall 7243cb57b5 remove not used old mock-api 2025-06-11 15:58:01 +02:00
Tommy Verrall 0276bd7b0b Merge pull request #5840 from nymtech/remove-testnet-faucet
Removing test-net faucet
2025-06-11 14:47:08 +01:00
Tommy Verrall 457759bb57 Merge pull request #5841 from nymtech/feature/add-buy-locations
Amended the buy section
2025-06-11 14:20:59 +01:00
dependabot[bot] de0f8ee2d3 build(deps): bump next from 14.2.15 to 14.2.26 in /documentation/docs (#5772)
Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.26.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.26)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.26
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:36:30 +01:00
dependabot[bot] ebf97ece9b build(deps-dev): bump webpack-dev-server in /wasm/mix-fetch/internal-dev
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.13.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.13.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 12:23:47 +00:00
dependabot[bot] 50a55f4bfb build(deps-dev): bump webpack-dev-server (#5826)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:22:07 +01:00
Tommy Verrall 4ee5c6457b remove images dir 2025-06-11 13:18:01 +02:00
Tommy Verrall d7b5fce7aa amended the buy section
- change the wallet to include the buy options for nym
- remove legacy code
2025-06-11 13:15:37 +02:00
benedetta davico c3d9c1131b Merge pull request #5838 from nymtech/release/2025.11-cheddar
merge release/2025.11-cheddar to develop
2025-06-11 13:09:57 +02:00
Tommy Verrall 5bdfb1ba5c removing test-net faucet 2025-06-11 12:00:44 +02:00
benedettadavico 94e51f0047 remove bity dir 2025-06-11 10:28:44 +02:00
benedetta davico f313e95e2f Merge pull request #5837 from nymtech/yana/replace-mintscan
Replace mintscan with ping.pub
2025-06-11 10:14:36 +02:00
Yana 2b13ac99b4 Replace mintscan with ping.pub 2025-06-10 19:34:19 +03:00
benedetta davico ef220882d4 update the workflow file again with a temp fix
reference: https://github.com/softprops/action-gh-release/issues/628
2025-06-10 11:39:20 +02:00
benedetta davico 59e26178ee Update publish-nym-binaries.yml 2025-06-10 11:20:19 +02:00
benedetta davico 0d420fb0a5 remove explorer-api in workflow 2025-06-10 11:01:24 +02:00
benedettadavico fce195fdba update changelog 2025-06-10 10:28:47 +02:00
Jędrzej Stuczyński 554b1eb022 bugfix: fix swapped total and circulating supplies (#5822) 2025-06-09 08:41:21 +01:00
Andrej Mihajlov e52bd918fb Hide tokio behind feature 2025-06-06 15:00:40 +02:00
Andrej Mihajlov 9d82d6d111 Hide tokio and sqlx behind not(wasm32) 2025-06-06 13:34:56 +02:00
Andrej Mihajlov 3593631e4a Exclude sqlx-pool-guard from wasm builds 2025-06-06 13:24:04 +02:00
import this 5b67403fb9 [DOCs/operators]: Add auto scraping of staking_supply_scale_factor & update api outputs (#5832) 2025-06-06 09:57:48 +00:00
Bogdan-Ștefan Neacşu 3a528d3b89 No autoremoval of peers (#5831)
* No autoremoval

* Remove startup_timestamp
2025-06-06 12:48:34 +03:00
Bogdan-Ștefan Neacşu 466bb97bc7 Use the same client bandwidth for top up (#5813)
* Use the same client bandwidth for top up

* Fix clippy
2025-06-06 10:12:50 +03:00
benedettadavico e9bb9792ab bump binaries 2025-06-04 14:42:04 +02:00
Andrej Mihajlov f5846d5bc2 Log all tracing output just in case 2025-06-04 11:40:56 +02:00
Bogdan-Ștefan Neacşu 88d4a9b111 Set cached storage counters to 0 (#5812)
* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too
2025-06-04 12:11:46 +03:00
Andrej Mihajlov d7779df1b7 Include proc_pidinfo on iOS 2025-06-04 11:00:15 +02:00
Andrej Mihajlov 7fcc188041 Switch to tracing 2025-06-03 17:19:42 +02:00
Andrej Mihajlov b8c8d33c94 Use log here 2025-06-03 15:13:21 +02:00
Andrej Mihajlov 02909c03dd Expose database path 2025-06-03 14:49:49 +02:00
Andrej Mihajlov 11262836d2 Clean up 2025-06-03 09:43:36 +02:00
Andrej Mihajlov f26fd5384d Improve windows 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 085103b333 Cleanup 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 574f7f1abd Revert 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 31e161604a Use sqlite pool guard 2025-06-03 09:43:36 +02:00
Andrej Mihajlov e4e349bea8 Remove logs 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 6391b7ed3a Document 2025-06-03 09:43:36 +02:00
Andrej Mihajlov c225511f95 Add Windows impl 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 4eedbb235a Add Windows implementation 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 548b8717b2 Update Linux impl 2025-06-03 09:43:36 +02:00
Andrej Mihajlov a215b3d0bf Open file watch 2025-06-03 09:43:36 +02:00
Andrej Mihajlov 03d5a133eb Close sqlite pool before erroring 2025-06-03 09:43:36 +02:00
dependabot[bot] b323c62a6e build(deps): bump tar-fs
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.8 to 3.0.9.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 06:50:43 +00:00
indmind d511aac301 chore: fixed typo in API endpoint parameter 2025-02-11 05:39:00 -06:00
289 changed files with 9295 additions and 11140 deletions
+1 -2
View File
@@ -5,7 +5,6 @@ on:
paths:
- 'clients/**'
- 'common/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
@@ -39,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -44,8 +44,10 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -53,5 +55,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
exit 2
fi
@@ -44,8 +44,10 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -53,5 +55,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
exit 2
fi
+2 -4
View File
@@ -66,7 +66,6 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -75,14 +74,13 @@ jobs:
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
-1
View File
@@ -40,7 +40,6 @@ validator-config
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
.parcel-cache
**/.DS_Store
cpu-cycles/libcpucycles/build
+24
View File
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
Generated
+1019 -266
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -66,8 +66,10 @@ members = [
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymkkt",
"common/nymnoise",
"common/nymnoise/keys",
"common/nympsq",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -101,7 +103,6 @@ members = [
"common/wireguard-types",
"documentation/autodoc",
"gateway",
"integrations/bity",
"nym-api",
"nym-api/nym-api-requests",
"nym-browser-extension/storage",
@@ -127,6 +128,7 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
@@ -288,6 +290,7 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.56"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.56"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -19,7 +18,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -32,7 +30,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -1,20 +1,18 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
config,
config::Config,
error::ClientCoreError,
};
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::nyxd;
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use std::{io, path::Path};
use time::OffsetDateTime;
use url::Url;
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> {
info!("creating fresh surb database");
info!("Creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}");
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold,
);
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err),
})?;
Ok(storage_backend)
match storage_backend.init_fresh(&mem_store).await {
Ok(()) => Ok(storage_backend),
Err(err) => {
storage_backend.shutdown().await;
Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
})
}
}
}
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// )
// }
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref();
debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted");
let new_extension =
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
} else {
suffix
};
let renamed = db_path.with_extension(new_extension);
let mut renamed = db_path.to_owned();
renamed.set_extension(new_extension);
fs::rename(db_path, renamed)
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
info!("Loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path)?;
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
setup_fresh_backend(db_path, surb_config).await
}
}
+12 -1
View File
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
[features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -1,8 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -30,7 +29,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -43,7 +41,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -15,9 +15,11 @@ use sqlx::{
};
use std::path::Path;
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)]
pub struct StorageManager {
pub connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
// all SQL goes here
@@ -37,7 +39,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path)
.filename(&database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -49,11 +51,15 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&connection_pool)
.run(&*connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
@@ -61,38 +67,43 @@ impl StorageManager {
Ok(StorageManager { connection_pool })
}
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
.map(|r| r.is_some())
}
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
@@ -100,14 +111,14 @@ impl StorageManager {
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
@@ -115,21 +126,21 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -141,21 +152,21 @@ impl StorageManager {
stored_tag.recipient,
stored_tag.tag
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -171,14 +182,14 @@ impl StorageManager {
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -193,7 +204,7 @@ impl StorageManager {
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
@@ -211,17 +222,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(())
@@ -239,7 +250,7 @@ impl StorageManager {
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -253,7 +264,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
}
@@ -267,7 +278,7 @@ impl StorageManager {
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
}
@@ -1,18 +1,21 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
backend::fs_backend::{
manager::StorageManager,
models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
@@ -41,15 +44,17 @@ impl Backend {
}
let manager = StorageManager::init(database_path, true).await?;
manager.create_status_table().await?;
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
};
Ok(backend)
match manager.create_status_table().await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(err) => {
manager.close_pool().await;
Err(err.into())
}
}
}
pub async fn try_load<P: AsRef<Path>>(
@@ -64,7 +69,28 @@ impl Backend {
}
let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager, fresh_sender_tags).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(
manager: &StorageManager,
fresh_sender_tags: bool,
) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
@@ -126,20 +152,11 @@ impl Backend {
manager.delete_all_tags().await?;
}
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
Ok(())
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.close_pool().await;
self.manager.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
@@ -152,7 +169,8 @@ impl Backend {
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
fs::rename(&self.database_path, &temp_old)
tokio::fs::rename(&self.database_path, &temp_old)
.await
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
@@ -161,9 +179,10 @@ impl Backend {
Ok(())
}
fn remove_old(&mut self) -> Result<(), StorageError> {
async fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
fs::remove_file(old_path)
tokio::fs::remove_file(old_path)
.await
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old()?;
self.remove_old().await?;
self.end_storage_flush().await
}
@@ -33,7 +33,6 @@ 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,
@@ -50,7 +49,6 @@ 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 {
@@ -133,7 +133,7 @@ impl ManagedConnection {
debug!("Managed to establish connection to {}", self.address);
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
match upgrade_noise_initiator(stream, &self.noise_config, None).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
error!("Failed to perform Noise handshake with {address} - {err}");
@@ -16,8 +16,8 @@ async fn main() {
let prefix = "n";
let denom: Denom = "unym".parse().unwrap();
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
let validator = "https://qwerty-validator.qa.nymte.ch";
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
let validator = "https://rpc.sandbox.nymtech.net";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
}
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
&res.transaction_hash
);
println!(
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
"Mintscan: https://ping.pub/nyx/tx/{}",
&res.transaction_hash
);
println!("Transaction hash: {}", &res.transaction_hash);
+9 -2
View File
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../sqlx-pool-guard"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
persistent-storage = ["bincode", "serde"]
@@ -7,10 +7,11 @@ use crate::models::{
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
impl SqliteEcashTicketbookManager {
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
SqliteEcashTicketbookManager { connection_pool }
}
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
data,
expiration_date,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
epoch_id,
total_tickets,
used_tickets,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
"#,
)
.bind(data)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await?
.is_some();
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
FROM ecash_ticketbook
"#,
)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
ticketbook_id,
expected_current_total_spent
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
&self,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
"#,
expiration_date
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
expiration_date
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -37,6 +37,7 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use sqlx_pool_guard::SqlitePoolGuard;
use std::path::Path;
use zeroize::Zeroizing;
@@ -54,15 +55,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().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
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();
@@ -74,13 +75,17 @@ impl PersistentStorage {
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
})
}
}
@@ -40,6 +40,10 @@ impl BandwidthStorageManager {
}
}
pub fn client_bandwidth(&self) -> ClientBandwidth {
self.client_bandwidth.clone()
}
pub async fn available_bandwidth(&self) -> i64 {
self.client_bandwidth.available().await
}
@@ -73,7 +73,7 @@ impl ClientBandwidth {
false
}
pub(crate) async fn available(&self) -> i64 {
pub async fn available(&self) -> i64 {
self.inner.read().await.bandwidth.bytes
}
+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().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
// 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().as_os_str()
"Attempting to connect to database {}",
database_path.as_ref().display()
);
// TODO: we can inject here more stuff based on our gateway global config
+28 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "network")]
use crate::{DenomDetails, ValidatorDetails};
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
pub const NETWORK_NAME: &str = "mainnet";
@@ -29,10 +29,36 @@ 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,6 +37,37 @@ 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
@@ -67,6 +98,8 @@ 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(),
}
}
@@ -154,6 +187,8 @@ 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,
}
}
+47
View File
@@ -0,0 +1,47 @@
[package]
name = "nym-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 }
blake3 = { workspace = true }
aead = { 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"] }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"]}
rand = "0.9.2"
curve25519-dalek = {version = "4.1.3", features = ["rand_core", "serde"] }
zeroize = { workspace = true, features = ["zeroize_derive"] }
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f","zeroize"]}
[dev-dependencies]
criterion = {workspace = true}
[[bench]]
name = "benches"
harness = false
[lints]
workspace = true
+518
View File
@@ -0,0 +1,518 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use criterion::{criterion_group, criterion_main, Criterion};
use nym_crypto::asymmetric::ed25519;
use nym_kkt::{
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, SignatureScheme, KEM},
context::KKTMode,
frame::KKTFrame,
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
},
};
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) {
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);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::SHAKE128,
HashFunction::SHAKE256,
] {
let ciphersuite = Ciphersuite::resolve_ciphersuite(
kem,
hash_function,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// generate kem public keys
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => (
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::X25519 => (
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::McEliece => (
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
),
};
let i_kem_key_bytes = initiator_kem_public_key.encode();
let r_kem_key_bytes = responder_kem_public_key.encode();
let i_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&i_kem_key_bytes,
);
let r_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&r_kem_key_bytes,
);
// Anonymous Initiator, OneWay
{
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Generate Request",
kem, hash_function
),
|b| {
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
},
);
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Encode Frame - Request",
kem, hash_function
),
|b| b.iter(|| i_frame.to_bytes()),
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Decode Frame - Request",
kem, hash_function
),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Ingest Frame",
kem, hash_function
),
|b| {
b.iter(|| {
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
});
},
);
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Generate Response",
kem, hash_function
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Responder Encode Frame",
kem, hash_function
),
|b| b.iter(|| r_frame.to_bytes()),
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Anonymous Initiator: Initiator Ingest Response",
kem, hash_function
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
},
);
let obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Generate Request",
kem, hash_function
),
|b| {
b.iter(|| {
initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap()
});
},
);
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Encode Frame - Request",
kem, hash_function
),
|b| b.iter(|| i_frame.to_bytes()),
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Decode Frame - Request",
kem, hash_function
),
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Ingest Frame",
kem, hash_function
),
|b| {
b.iter(|| {
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Generate Response",
kem, hash_function
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Responder Encode Frame",
kem, hash_function
),
|b| {
b.iter(|| r_frame.to_bytes());
},
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator OneWay: Initiator Ingest Response",
kem, hash_function
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
},
);
let i_obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Generate Request",
kem, hash_function
),
|b| {
b.iter(|| {
initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap()
});
},
);
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Encode Frame - Request",
kem, hash_function
),
|b| {
b.iter(|| i_frame.to_bytes());
},
);
let i_frame_bytes = i_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Decode Frame - Request",
kem, hash_function
),
|b| {
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
},
);
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Ingest Frame",
kem, hash_function
),
|b| {
b.iter(|| {
responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap()
});
},
);
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Generate Response",
kem, hash_function
),
|b| {
b.iter(|| {
responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap()
});
},
);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Responder Encode Frame",
kem, hash_function
),
|b| {
b.iter(|| {
r_frame.to_bytes();
});
},
);
let r_bytes = r_frame.to_bytes();
c.bench_function(
&format!(
"{}, {} | Initiator Mutual: Initiator Ingest Response",
kem, hash_function
),
|b| {
b.iter(|| {
initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap()
});
},
);
let obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
}
}
}
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
kkt_benchmark
);
criterion_main!(benches);
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use libcrux_kem::{Algorithm, MlKem768PublicKey};
use nym_crypto::asymmetric::ed25519;
use crate::error::KKTError;
pub const HASH_LEN_256: u8 = 32;
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
pub const CURVE25519_KEY_LEN: usize = 32;
#[derive(Clone, Copy, Debug)]
pub enum HashFunction {
Blake3,
SHAKE128,
SHAKE256,
SHA256,
}
impl Display for HashFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
HashFunction::Blake3 => "Blake3",
HashFunction::SHAKE128 => "SHAKE128",
HashFunction::SHAKE256 => "SHAKE256",
HashFunction::SHA256 => "SHA256",
})
}
}
pub enum EncapsulationKey<'a> {
MlKem768(libcrux_kem::PublicKey),
XWing(libcrux_kem::PublicKey),
X25519(libcrux_kem::PublicKey),
McEliece(classic_mceliece_rust::PublicKey<'a>),
}
pub enum DecapsulationKey<'a> {
MlKem768(libcrux_kem::PrivateKey),
XWing(libcrux_kem::PrivateKey),
X25519(libcrux_kem::PrivateKey),
McEliece(classic_mceliece_rust::SecretKey<'a>),
}
impl<'a> EncapsulationKey<'a> {
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
match kem {
KEM::McEliece => {
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
Err(KKTError::KEMError {
info: "Received McEliece Encapsulation Key with Invalid Length",
})
} else {
let mut public_key_bytes =
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
public_key_bytes.clone_from_slice(bytes);
Ok(EncapsulationKey::McEliece(
classic_mceliece_rust::PublicKey::from(public_key_bytes),
))
}
}
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem),
bytes,
)?)),
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem),
bytes,
)?)),
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
map_kem_to_libcrux_kem(kem),
bytes,
)?)),
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
EncapsulationKey::XWing(public_key)
| EncapsulationKey::MlKem768(public_key)
| EncapsulationKey::X25519(public_key) => public_key.encode(),
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum SignatureScheme {
Ed25519,
}
impl Display for SignatureScheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SignatureScheme::Ed25519 => "Ed25519",
})
}
}
#[derive(Clone, Copy, Debug)]
pub enum KEM {
MlKem768,
XWing,
X25519,
McEliece,
}
impl Display for KEM {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
KEM::MlKem768 => "MlKem768",
KEM::XWing => "XWing",
KEM::X25519 => "x25519",
KEM::McEliece => "McEliece",
})
}
}
#[derive(Clone, Copy, Debug)]
pub struct Ciphersuite {
hash_function: HashFunction,
signature_scheme: SignatureScheme,
kem: KEM,
hash_length: u8,
encapsulation_key_length: usize,
signing_key_length: usize,
verification_key_length: usize,
signature_length: usize,
}
impl Ciphersuite {
pub fn kem_key_len(&self) -> usize {
self.encapsulation_key_length
}
pub fn signature_len(&self) -> usize {
self.signature_length
}
pub fn signing_key_len(&self) -> usize {
self.signing_key_length
}
pub fn verification_key_len(&self) -> usize {
self.verification_key_length
}
pub fn hash_function(&self) -> HashFunction {
self.hash_function
}
pub fn kem(&self) -> KEM {
self.kem
}
pub fn signature_scheme(&self) -> SignatureScheme {
self.signature_scheme
}
pub fn hash_len(&self) -> usize {
self.hash_length as usize
}
pub fn default() -> Self {
Self::resolve_ciphersuite(
KEM::XWing,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap()
}
pub fn resolve_ciphersuite(
kem: KEM,
hash_function: HashFunction,
signature_scheme: SignatureScheme,
// This should be None 99.9999% of the time
custom_hash_length: Option<u8>,
) -> Result<Self, KKTError> {
let hash_len = match custom_hash_length {
Some(l) => {
if l < 16 {
return Err(KKTError::InsecureHashLen);
} else {
l
}
}
None => HASH_LEN_256,
};
Ok(Self {
hash_function: hash_function,
signature_scheme: signature_scheme,
kem: kem,
hash_length: hash_len,
encapsulation_key_length: match kem {
// 1184 bytes
KEM::MlKem768 => MlKem768PublicKey::len(),
// 1216 bytes = 1184 + 32
KEM::XWing => MlKem768PublicKey::len() + CURVE25519_KEY_LEN,
// 32 bytes
KEM::X25519 => CURVE25519_KEY_LEN,
// 524160 bytes
KEM::McEliece => classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES,
},
signing_key_length: match signature_scheme {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH,
},
verification_key_length: match signature_scheme {
// 32 bytes
SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH,
},
signature_length: match signature_scheme {
// 64 bytes
SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH,
},
})
}
pub fn encode(&self) -> [u8; 4] {
// [kem, hash, hashlen, sig]
[
match self.kem {
KEM::XWing => 0,
KEM::MlKem768 => 1,
KEM::McEliece => 2,
KEM::X25519 => 255,
},
match self.hash_function {
HashFunction::Blake3 => 0,
HashFunction::SHAKE256 => 1,
HashFunction::SHAKE128 => 2,
HashFunction::SHA256 => 3,
},
match self.hash_length {
HASH_LEN_256 => 0,
_ => self.hash_length as u8,
},
match self.signature_scheme {
SignatureScheme::Ed25519 => 0,
},
]
}
pub fn decode(encoding: &[u8]) -> Result<Self, KKTError> {
if encoding.len() == 4 {
let kem = match encoding[0] {
0 => KEM::XWing,
1 => KEM::MlKem768,
2 => KEM::McEliece,
255 => KEM::X25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined KEM: {}", encoding[0]),
})
}
};
let hash_function = match encoding[1] {
0 => HashFunction::Blake3,
1 => HashFunction::SHAKE256,
2 => HashFunction::SHAKE128,
3 => HashFunction::SHA256,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Hash Function: {}", encoding[1]),
})
}
};
let custom_hash_length = match encoding[2] {
0 => None,
_ => Some(encoding[2]),
};
let signature_scheme = match encoding[3] {
0 => SignatureScheme::Ed25519,
_ => {
return Err(KKTError::CiphersuiteDecodingError {
info: format!("Undefined Signature Scheme: {}", encoding[3]),
})
}
};
Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length)
} else {
return Err(KKTError::CiphersuiteDecodingError {
info: format!(
"Incorrect Encoding Length: actual: {} != expected: {}",
encoding.len(),
CIPHERSUITE_ENCODING_LEN
),
});
}
}
}
impl Display for Ciphersuite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&format!(
"{}_{}({})_{}",
self.kem, self.hash_function, self.hash_length, self.signature_scheme
)
.to_ascii_lowercase(),
)
}
}
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Algorithm {
match kem {
KEM::MlKem768 => Algorithm::MlKem768,
KEM::XWing => Algorithm::XWingKemDraft06,
KEM::X25519 => Algorithm::X25519,
_ => unreachable!(),
}
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use crate::{ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN, KKT_VERSION};
pub const KKT_CONTEXT_LEN: usize = 7;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum KKTStatus {
Ok,
InvalidRequestFormat,
InvalidResponseFormat,
InvalidSignature,
UnsupportedCiphersuite,
UnsupportedKKTVersion,
InvalidKey,
Timeout,
}
impl Display for KKTStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
KKTStatus::Ok => "Ok",
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
KKTStatus::InvalidSignature => "Invalid Signature",
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
KKTStatus::InvalidKey => "Invalid Key",
KKTStatus::Timeout => "Timeout",
})
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum KKTRole {
Initiator,
AnonymousInitiator,
Responder,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum KKTMode {
OneWay,
Mutual,
}
#[derive(Copy, Clone, Debug)]
pub struct KKTContext {
version: u8,
message_sequence: u8,
status: KKTStatus,
mode: KKTMode,
role: KKTRole,
ciphersuite: Ciphersuite,
}
impl KKTContext {
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
return Err(KKTError::IncompatibilityError {
info: "Anonymous Initiator can only use OneWay mode",
});
}
Ok(Self {
version: KKT_VERSION,
message_sequence: 0,
status: KKTStatus::Ok,
mode: mode,
role: role,
ciphersuite: ciphersuite,
})
}
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
let mut responder_header = self.clone();
responder_header.increment_message_sequence_count()?;
responder_header.role = KKTRole::Responder;
Ok(responder_header)
}
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> {
if self.message_sequence + 1 < (1 << 4) {
self.message_sequence += 1;
Ok(())
} else {
Err(KKTError::MessageCountLimitReached)
}
}
pub fn update_status(&mut self, status: KKTStatus) {
self.status = status;
}
pub fn version(&self) -> u8 {
self.version
}
pub fn status(&self) -> KKTStatus {
self.status
}
pub fn ciphersuite(&self) -> Ciphersuite {
self.ciphersuite
}
pub fn role(&self) -> KKTRole {
self.role
}
pub fn mode(&self) -> KKTMode {
self.mode
}
pub fn body_len(&self) -> usize {
if self.status != KKTStatus::Ok
|| (self.mode == KKTMode::OneWay
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
{
0
} else {
self.ciphersuite.kem_key_len()
}
}
pub fn signature_len(&self) -> usize {
match self.role {
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
KKTRole::AnonymousInitiator => 0,
}
}
pub fn header_len(&self) -> usize {
KKT_CONTEXT_LEN
}
pub fn session_id_len(&self) -> usize {
// match self.role {
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
// It doesn't make sense to send a session_id if we send messages in the clear
// KKTRole::AnonymousInitiator => 0,
// }
KKT_SESSION_ID_LEN
}
pub fn full_message_len(&self) -> usize {
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
}
pub fn encode(&self) -> Result<Vec<u8>, KKTError> {
let mut header_bytes: Vec<u8> = Vec::with_capacity(KKT_CONTEXT_LEN);
if self.message_sequence >= 1 << 4 {
return Err(KKTError::MessageCountLimitReached);
}
header_bytes.push((KKT_VERSION << 4) + self.message_sequence);
header_bytes.push(
match self.status {
KKTStatus::Ok => 0,
KKTStatus::InvalidRequestFormat => 0b001_000_00,
KKTStatus::InvalidResponseFormat => 0b010_000_00,
KKTStatus::InvalidSignature => 0b011_000_00,
KKTStatus::UnsupportedCiphersuite => 0b100_000_00,
KKTStatus::UnsupportedKKTVersion => 0b101_000_00,
KKTStatus::InvalidKey => 0b110_000_00,
KKTStatus::Timeout => 0b111_000_00,
} + match self.mode {
KKTMode::OneWay => 0,
KKTMode::Mutual => 0b000_001_00,
} + match self.role {
KKTRole::Initiator => 0,
KKTRole::Responder => 1,
KKTRole::AnonymousInitiator => 2,
},
);
header_bytes.extend_from_slice(&self.ciphersuite.encode());
header_bytes.push(0);
Ok(header_bytes)
}
pub fn try_decode(header_bytes: &[u8]) -> Result<Self, KKTError> {
if header_bytes.len() == KKT_CONTEXT_LEN {
let kkt_version = header_bytes[0] & 0b1111_0000;
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
// We only check if stuff is valid here, not necessarily if it's compatible
if (kkt_version >> 4) > KKT_VERSION {
return Err(KKTError::FrameDecodingError {
info: format!("Header - Invalid KKT Version: {}", kkt_version >> 4),
});
}
let status = match header_bytes[1] & 0b111_000_00 {
0 => KKTStatus::Ok,
0b001_000_00 => KKTStatus::InvalidRequestFormat,
0b010_000_00 => KKTStatus::InvalidResponseFormat,
0b011_000_00 => KKTStatus::InvalidSignature,
0b100_000_00 => KKTStatus::UnsupportedCiphersuite,
0b101_000_00 => KKTStatus::UnsupportedKKTVersion,
0b110_000_00 => KKTStatus::InvalidKey,
0b111_000_00 => KKTStatus::Timeout,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Status: {}",
header_bytes[1] & 0b111_000_00
),
})
}
};
let role = match header_bytes[1] & 0b000_000_11 {
0 => KKTRole::Initiator,
1 => KKTRole::Responder,
2 => KKTRole::AnonymousInitiator,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Role: {}",
header_bytes[1] & 0b000_000_11
),
})
}
};
let mode = match (header_bytes[1] & 0b000_111_00) >> 2 {
0 => KKTMode::OneWay,
1 => KKTMode::Mutual,
_ => {
return Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid KKT Mode: {}",
(header_bytes[1] & 0b000_111_00) >> 2
),
})
}
};
Ok(KKTContext {
version: kkt_version,
status: status,
mode: mode,
role: role,
ciphersuite: Ciphersuite::decode(&header_bytes[2..6])?,
message_sequence: message_sequence_counter,
})
} else {
Err(KKTError::FrameDecodingError {
info: format!(
"Header - Invalid Header Length: actual: {} != expected: {}",
header_bytes.len(),
KKT_CONTEXT_LEN
),
})
}
}
}
+95
View File
@@ -0,0 +1,95 @@
use core::hash;
use blake3::{Hash, Hasher};
use curve25519_dalek::digest::DynDigest;
use libcrux_psq::traits::Ciphertext;
use nym_crypto::symmetric::aead::{AeadKey, Nonce};
use nym_crypto::{
aes::Aes256,
asymmetric::x25519::{self, PrivateKey, PublicKey},
generic_array::GenericArray,
Aes256GcmSiv,
};
// use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use crate::error::KKTError;
fn generate_round_trip_symmetric_key<R>(
rng: &mut R,
remote_public_key: &PublicKey,
) -> ([u8; 64], [u8; 32])
where
R: CryptoRng + RngCore,
{
let mut s = x25519::PrivateKey::new(rng);
let gs = s.public_key();
let mut gbs = s.diffie_hellman(remote_public_key);
s.zeroize();
let mut message: [u8; 64] = [0u8; 64];
message[0..32].clone_from_slice(gs.as_bytes());
let mut hasher = Hasher::new();
hasher.update(&gbs);
gbs.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
hasher.update(remote_public_key.as_bytes());
hasher.update(gs.as_bytes());
hasher.finalize_into_reset(&mut message[32..64]);
(message, key)
}
fn extract_shared_secret(b: &PrivateKey, message: &[u8; 64]) -> Result<[u8; 32], KKTError> {
let gs = PublicKey::from_bytes(&message[0..32])?;
let mut gsb = b.diffie_hellman(&gs);
let mut hasher = Hasher::new();
hasher.update(&gsb);
gsb.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
hasher.update(b.public_key().as_bytes());
hasher.update(gs.as_bytes());
// This runs in constant time
if hasher.finalize() == message[32..64] {
Ok(key)
} else {
Err(KKTError::X25519Error {
info: format!("Symmetric Key Hash Validation Error"),
})
}
}
fn encrypt(mut key: [u8; 32], message: &[u8]) -> Result<Vec<u8>, KKTError> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256GcmSiv>::from_slice(&[]);
let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
key.zeroize();
Ok(ciphertext)
}
fn decrypt(key: [u8; 32], ciphertext: Vec<u8>) -> Vec<u8> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256>::from_slice(&[]);
let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
key.zeroize();
Ok(ciphertext)
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
use crate::context::KKTStatus;
#[derive(Error, Debug)]
pub enum KKTError {
#[error("Signature constructor error")]
SigConstructorError,
#[error("Signature verification error")]
SigVerifError,
#[error("Ciphersuite Decoding Error: {}", info)]
CiphersuiteDecodingError { info: String },
#[error("Insecure Encapsulation Key Hash Length")]
InsecureHashLen,
#[error("KKT Frame Decoding Error: {}", info)]
FrameDecodingError { info: String },
#[error("KKT Frame Encoding Error: {}", info)]
FrameEncodingError { info: String },
#[error("KKT Incompatibility Error: {}", info)]
IncompatibilityError { info: &'static str },
#[error("KKT Responder Flagged Error: {}", status)]
ResponderFlaggedError { status: KKTStatus },
#[error("KKT Message Count Limit Reached")]
MessageCountLimitReached,
#[error("PSQ KEM Error: {}", info)]
KEMError { info: &'static str },
#[error("Local Function Input Error: {}", info)]
FunctionInputError { info: &'static str },
#[error("{}", info)]
X25519Error { info: &'static str },
// #[error("Protocol did not complete")]
// ProtocolError,
// #[error("encountered an IO error: {0}")]
// IoError(#[from] io::Error),s
// #[error("Handshake timeout")]
// HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl From<libcrux_kem::Error> for KKTError {
fn from(err: libcrux_kem::Error) -> Self {
match err {
libcrux_kem::Error::EcDhError(_) => KKTError::KEMError { info: "ECDH Error" },
libcrux_kem::Error::KeyGen => KKTError::KEMError {
info: "Key Generation Error",
},
libcrux_kem::Error::Encapsulate => KKTError::KEMError {
info: "Encapsulation Error",
},
libcrux_kem::Error::Decapsulate => KKTError::KEMError {
info: "Decapsulation Error",
},
libcrux_kem::Error::UnsupportedAlgorithm => KKTError::KEMError {
info: "libcrux Unsupported Algorithm",
},
libcrux_kem::Error::InvalidPrivateKey => KKTError::KEMError {
info: "Invalid Private Key",
},
libcrux_kem::Error::InvalidPublicKey => KKTError::KEMError {
info: "Invalid Public Key",
},
libcrux_kem::Error::InvalidCiphertext => KKTError::KEMError {
info: "Invalid Ciphertext",
},
}
}
}
impl From<libcrux_ecdh::Error> for KKTError {
fn from(err: libcrux_ecdh::Error) -> Self {
match err {
libcrux_ecdh::Error::InvalidPoint => KKTError::KEMError {
info: "Invalid Remote Public Key",
},
_ => todo!(),
}
}
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// | 0 | 1 | 2, 3, 4, 5 | 6 | 7
// [0] => KKT version (4 bits) + Message Sequence Count (4 bits)
// [1] => Status (3 bits) + Mode (3 bits) + Role (2 bits)
// [2..=5] => Ciphersuite
// [6] => Reserved
use nym_crypto::asymmetric::x25519::PublicKey;
use crate::{
context::{KKTContext, KKT_CONTEXT_LEN},
error::KKTError,
};
pub const KKT_SESSION_ID_LEN: usize = 16;
pub struct KKTFrame {
context: Vec<u8>,
session_id: Vec<u8>,
body: Vec<u8>,
signature: Vec<u8>,
}
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
// if message coming from anonymous initiator => body is empty, there is no signature.
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
impl KKTFrame {
pub fn new(context: &[u8], body: &[u8], session_id: &[u8], signature: &[u8]) -> Self {
Self {
context: Vec::from(context),
body: Vec::from(body),
session_id: Vec::from(session_id),
signature: Vec::from(signature),
}
}
pub fn context_ref<'a>(&'a self) -> &'a [u8] {
&self.context
}
pub fn signature_ref<'a>(&'a self) -> &'a [u8] {
&self.signature
}
pub fn body_ref<'a>(&'a self) -> &'a [u8] {
&self.body
}
pub fn session_id_ref<'a>(&'a self) -> &'a [u8] {
&self.session_id
}
pub fn signature_mut<'a>(&'a mut self) -> &'a mut [u8] {
&mut self.signature
}
pub fn body_mut<'a>(&'a mut self) -> &'a mut [u8] {
&mut self.body
}
pub fn session_id_mut<'a>(&'a mut self) -> &'a mut [u8] {
&mut self.session_id
}
pub fn frame_length(&self) -> usize {
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.frame_length());
bytes.extend_from_slice(&self.context);
bytes.extend_from_slice(&self.body);
bytes.extend_from_slice(&self.session_id);
bytes.extend_from_slice(&self.signature);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> {
if bytes.len() < KKT_CONTEXT_LEN {
return Err(KKTError::FrameDecodingError {
info: format!(
"Frame is shorter than expected context length: actual {} != expected {}",
bytes.len(),
KKT_CONTEXT_LEN
),
});
} else {
let context_bytes = Vec::from(&bytes[0..KKT_CONTEXT_LEN]);
let context = KKTContext::try_decode(&context_bytes)?;
let (mut session_id, mut body, mut signature): (Vec<u8>, Vec<u8>, Vec<u8>) =
(vec![], vec![], vec![]);
if bytes.len() == context.full_message_len() {
if context.body_len() > 0 {
body.extend_from_slice(
&bytes[KKT_CONTEXT_LEN..KKT_CONTEXT_LEN + context.body_len()],
);
}
if context.session_id_len() > 0 {
session_id.extend_from_slice(
&bytes[KKT_CONTEXT_LEN + context.body_len()
..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
);
}
if context.signature_len() > 0 {
signature.extend_from_slice(
&bytes[KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()
..KKT_CONTEXT_LEN
+ context.body_len()
+ context.session_id_len()
+ context.signature_len()],
);
}
Ok((
KKTFrame::new(&context_bytes, &body, &session_id, &signature),
context,
))
} else {
return Err(KKTError::FrameDecodingError {
info: format!(
"Frame is shorter than expected: actual {} != expected {}",
bytes.len(),
context.full_message_len()
),
});
}
}
}
}
pub struct EncryptedKKTRequest {
ephermeral_key: PublicKey,
body: Vec<u8>,
}
impl EncryptedKKTRequest {
pub fn to_bytes(mut self) -> Vec<u8> {
self.body.extend(self.ephermeral_key.to_bytes());
self.body
}
}
pub struct EncryptedKKTResponse {
body: Vec<u8>,
}
+107
View File
@@ -0,0 +1,107 @@
use crate::{
ciphersuite::{HashFunction, KEM},
error::KKTError,
};
use classic_mceliece_rust::keypair_boxed;
use libcrux_kem::{key_gen, Algorithm};
use libcrux_sha3;
use rand::{CryptoRng, RngCore};
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_libcrux<R>(
rng: &mut R,
kem: KEM,
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), KKTError>
where
R: RngCore + CryptoRng,
{
match kem {
KEM::MlKem768 => Ok(key_gen(Algorithm::MlKem768, rng)?),
KEM::XWing => Ok(key_gen(Algorithm::XWingKemDraft06, rng)?),
KEM::X25519 => Ok(key_gen(Algorithm::X25519, rng)?),
_ => Err(KKTError::KEMError {
info: "Key Generation Error: Unsupported Libcrux Algorithm",
}),
}
}
// (decapsulation_key, encapsulation_key)
pub fn generate_keypair_mceliece<'a, R>(
rng: &mut R,
) -> (
classic_mceliece_rust::SecretKey<'a>,
classic_mceliece_rust::PublicKey<'a>,
)
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
(decapsulation_key, encapsulation_key)
}
pub fn hash_key_bytes(
hash_function: &HashFunction,
hash_length: usize,
key_bytes: &[u8],
) -> Vec<u8> {
let mut hashed_key: Vec<u8> = vec![0u8; hash_length];
match hash_function {
HashFunction::Blake3 => {
let mut hasher = blake3::Hasher::new();
hasher.update(key_bytes);
hasher.finalize_xof().fill(&mut hashed_key);
hasher.reset();
}
HashFunction::SHAKE256 => {
libcrux_sha3::shake256_ema(&mut hashed_key, &key_bytes);
}
HashFunction::SHAKE128 => {
libcrux_sha3::shake128_ema(&mut hashed_key, &key_bytes);
}
HashFunction::SHA256 => {
libcrux_sha3::sha256_ema(&mut hashed_key, &key_bytes);
}
}
hashed_key
}
/// This does NOT run in constant time.
// It's fine for KKT since we are comparing hashes.
fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
a == b
}
pub fn validate_encapsulation_key(
hash_function: &HashFunction,
hash_length: usize,
encapsulation_key: &[u8],
expected_hash_bytes: &[u8],
) -> bool {
compare_hashes(
&hash_encapsulation_key(hash_function, hash_length, encapsulation_key),
expected_hash_bytes,
)
}
pub fn validate_key_bytes(
hash_function: &HashFunction,
hash_length: usize,
key_bytes: &[u8],
expected_hash_bytes: &[u8],
) -> bool {
compare_hashes(
&hash_key_bytes(hash_function, hash_length, key_bytes),
expected_hash_bytes,
)
}
pub fn hash_encapsulation_key(
hash_function: &HashFunction,
hash_length: usize,
encapsulation_key: &[u8],
) -> Vec<u8> {
hash_key_bytes(hash_function, hash_length, &encapsulation_key)
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod ciphersuite;
pub mod context;
// pub mod encryption;
pub mod error;
pub mod frame;
pub mod key_utils;
pub mod session;
// pub mod psq;
// This must be less than 4 bits
pub const KKT_VERSION: u8 = 1;
const _: () = assert!(KKT_VERSION < 1 << 4);
#[cfg(test)]
mod test {
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
frame::KKTFrame,
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
session::{
anonymous_initiator_process, initiator_ingest_response, initiator_process,
responder_ingest_message, responder_process,
},
};
#[test]
fn test_kkt_psq_e2e_clear() {
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);
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
for hash_function in [
HashFunction::Blake3,
HashFunction::SHA256,
HashFunction::SHAKE128,
HashFunction::SHAKE256,
] {
let ciphersuite = Ciphersuite::resolve_ciphersuite(
kem,
hash_function,
crate::ciphersuite::SignatureScheme::Ed25519,
None,
)
.unwrap();
// generate kem public keys
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
KEM::MlKem768 => (
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
EncapsulationKey::MlKem768(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
),
KEM::XWing => (
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
),
KEM::X25519 => (
EncapsulationKey::X25519(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
EncapsulationKey::X25519(
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
),
),
KEM::McEliece => (
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
),
};
let i_kem_key_bytes = initiator_kem_public_key.encode();
let r_kem_key_bytes = responder_kem_public_key.encode();
let i_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&i_kem_key_bytes,
);
let r_dir_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&r_kem_key_bytes,
);
// Anonymous Initiator, OneWay
{
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, OneWay
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
initiator_ed25519_keypair.private_key(),
None,
)
.unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
&i_frame_r,
)
.unwrap();
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let i_obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
}
// Initiator, Mutual
{
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
initiator_ed25519_keypair.private_key(),
Some(&initiator_kem_public_key),
)
.unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
&i_frame_r,
)
.unwrap();
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&mut r_context,
i_frame_r.session_id_ref(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
.unwrap();
let r_bytes = r_frame.to_bytes();
let obtained_key = initiator_ingest_response(
&mut i_context,
responder_ed25519_keypair.public_key(),
&r_dir_hash,
&r_bytes,
)
.unwrap();
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
}
}
}
}
}
+228
View File
@@ -0,0 +1,228 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand::{CryptoRng, RngCore};
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
error::KKTError,
frame::{KKTFrame, KKT_SESSION_ID_LEN},
key_utils::validate_encapsulation_key,
};
pub fn initiator_process<'a, R>(
rng: &mut R,
mode: KKTMode,
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
) -> Result<(KKTContext, KKTFrame), KKTError>
where
R: CryptoRng + RngCore,
{
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
let context_bytes = context.encode()?;
let mut session_id = [0; KKT_SESSION_ID_LEN];
// Generate Session ID
rng.fill_bytes(&mut session_id);
let body: &[u8] = match mode {
KKTMode::OneWay => &[],
KKTMode::Mutual => match own_encapsulation_key {
Some(encaps_key) => &encaps_key.encode(),
// Missing key
None => {
return Err(KKTError::FunctionInputError {
info: "KEM Key Not Provided",
})
}
},
};
let mut bytes_to_sign =
Vec::with_capacity(context.full_message_len() - context.signature_len());
bytes_to_sign.extend_from_slice(&context_bytes);
bytes_to_sign.extend_from_slice(&body);
bytes_to_sign.extend_from_slice(&session_id);
let signature = signing_key.sign(bytes_to_sign).to_bytes();
Ok((
context,
KKTFrame::new(&context_bytes, &body, &session_id, &signature),
))
}
pub fn anonymous_initiator_process<R>(
rng: &mut R,
ciphersuite: Ciphersuite,
) -> Result<(KKTContext, KKTFrame), KKTError>
where
R: CryptoRng + RngCore,
{
let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?;
let context_bytes = context.encode()?;
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
rng.fill_bytes(&mut session_id);
Ok((
context,
KKTFrame::new(&context_bytes, &[], &session_id, &[]),
))
}
pub fn initiator_ingest_response<'a>(
own_context: &mut KKTContext,
remote_verification_key: &ed25519::PublicKey,
expected_hash: &[u8],
message_bytes: &[u8],
) -> Result<EncapsulationKey<'a>, KKTError> {
// sizes have to be correct
let (frame, remote_context) = KKTFrame::from_bytes(message_bytes)?;
check_compatibility(&own_context, &remote_context)?;
match remote_context.status() {
KKTStatus::Ok => {
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
remote_context.full_message_len() - remote_context.signature_len(),
);
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
bytes_to_verify.extend_from_slice(frame.body_ref());
bytes_to_verify.extend_from_slice(frame.session_id_ref());
match Signature::from_bytes(frame.signature_ref()) {
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
Ok(()) => {
let received_encapsulation_key = EncapsulationKey::decode(
own_context.ciphersuite().kem(),
frame.body_ref(),
)?;
match validate_encapsulation_key(
&own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
&frame.body_ref(),
expected_hash,
) {
true => Ok(received_encapsulation_key),
// The key does not match the hash obtained from the directory
false => Err(KKTError::KEMError { info: "Hash of received encapsulation key does not match the value stored on the directory." }),
}
}
Err(_) => Err(KKTError::SigVerifError),
},
Err(_) => Err(KKTError::SigConstructorError),
}
}
_ => Err(KKTError::ResponderFlaggedError {
status: remote_context.status(),
}),
}
}
// todo: figure out how to handle errors using status codes
pub fn responder_ingest_message<'a>(
remote_context: &KKTContext,
remote_verification_key: Option<&ed25519::PublicKey>,
expected_hash: Option<&[u8]>,
remote_frame: &KKTFrame,
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> {
let own_context = remote_context.derive_responder_header()?;
match remote_context.role() {
KKTRole::AnonymousInitiator => Ok((own_context, None)),
KKTRole::Initiator => {
match remote_verification_key {
Some(remote_verif_key) => {
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
own_context.full_message_len() - own_context.signature_len(),
);
bytes_to_verify.extend_from_slice(remote_frame.context_ref());
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
match Signature::from_bytes(remote_frame.signature_ref()) {
Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) {
Ok(()) => {
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
match own_context.mode() {
KKTMode::OneWay => return Ok((own_context, None)),
KKTMode::Mutual => {
match expected_hash {
Some(expected_hash) =>{
let received_encapsulation_key =
EncapsulationKey::decode(own_context.ciphersuite().kem(), remote_frame.body_ref())?;
if
validate_encapsulation_key(
&own_context.ciphersuite().hash_function(),
own_context.ciphersuite().hash_len(),
&remote_frame.body_ref(),
expected_hash,
){
Ok((own_context, Some(received_encapsulation_key)))
}
// The key does not match the hash obtained from the directory
else {
Err(KKTError::KEMError { info: "Hash of received encapsulation key does not match the value stored on the directory." })
}
}
None => Err(KKTError::FunctionInputError { info: "Expected hash of the remote encapsulation key is not provided." }),
}
}
}
}
Err(_) => Err(KKTError::SigVerifError),
},
Err(_) => Err(KKTError::SigConstructorError),
}
}
None => Err(KKTError::FunctionInputError {
info: "Remote Signature Verification Key Not Provided",
}),
}
}
KKTRole::Responder => Err(KKTError::IncompatibilityError {
info: "Responder received a request from another responder.",
}),
}
}
pub fn responder_process<'a>(
own_context: &mut KKTContext,
session_id: &[u8],
signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>,
) -> Result<KKTFrame, KKTError> {
let body = encapsulation_key.encode();
let context_bytes = own_context.encode()?;
let mut bytes_to_sign =
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
bytes_to_sign.extend_from_slice(&own_context.encode()?);
bytes_to_sign.extend_from_slice(&body);
bytes_to_sign.extend_from_slice(&session_id);
let signature = signing_key.sign(bytes_to_sign).to_bytes();
Ok(KKTFrame::new(
&context_bytes,
&body,
&session_id,
&signature,
))
}
fn check_compatibility(
_own_context: &KKTContext,
_remote_context: &KKTContext,
) -> Result<(), KKTError> {
// todo: check ciphersuite/context compatibility
Ok(())
}
+6
View File
@@ -22,6 +22,12 @@ tokio-util = { workspace = true, features = ["codec"] }
nym-crypto = { path = "../crypto" }
nym-noise-keys = { path = "keys" }
nympsq = {path = "../nympsq"}
libcrux-ed25519 = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
rand = "0.9.2"
[dev-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
+3
View File
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
#[serde(from = "u8", into = "u8")]
pub enum NoiseVersion {
V1,
V2,
Unknown(u8), //Implies a newer version we don't know
}
@@ -16,6 +17,7 @@ impl From<u8> for NoiseVersion {
fn from(value: u8) -> Self {
match value {
1 => NoiseVersion::V1,
2 => NoiseVersion::V2,
other => NoiseVersion::Unknown(other),
}
}
@@ -25,6 +27,7 @@ impl From<NoiseVersion> for u8 {
fn from(version: NoiseVersion) -> Self {
match version {
NoiseVersion::V1 => 1,
NoiseVersion::V2 => 2,
NoiseVersion::Unknown(other) => other,
}
}
+19
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::NoiseVersion;
use nympsq::error::PSQError;
use snow::Error;
use std::io;
use thiserror::Error;
@@ -70,6 +71,12 @@ pub enum NoiseError {
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
#[error("Missing Field")]
MissingField { info: &'static str },
#[error("PSQ Error")]
PSQError(PSQError),
}
impl NoiseError {
@@ -81,6 +88,18 @@ impl NoiseError {
}
}
impl From<PSQError> for NoiseError {
fn from(err: PSQError) -> Self {
NoiseError::PSQError(err)
}
}
impl From<libcrux_kem::Error> for NoiseError {
fn from(err: libcrux_kem::Error) -> Self {
NoiseError::PSQError(err.into())
}
}
impl From<Error> for NoiseError {
fn from(err: Error) -> Self {
match err {
+136 -28
View File
@@ -1,9 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::ed25519;
use nym_noise_keys::NoiseVersion;
use snow::error::Prerequisite;
use snow::Error;
use nympsq::psq::CONTEXT_LEN;
use snow::{error::Prerequisite, Error};
use tokio::net::TcpStream;
use tracing::{error, warn};
@@ -12,48 +14,138 @@ 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;
use crate::{
config::NoiseConfig, connection::Connection, error::NoiseError, stream::NoiseStreamBuilder,
};
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
pub(crate) const NOISE_PSQ_DEFAULT_CONTEXT: &[u8; CONTEXT_LEN] = b"Exsl88AD2ccS99kk";
pub(crate) const NOISE_PSQ_DEFAULT_DURATION_SECS: u64 = 1000;
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V2;
// 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 std::time::Duration;
use crate::{error::NoiseError, stream::Psk, NOISE_PSK_PREFIX};
use libcrux_ed25519::VerificationKey;
use libcrux_kem::{PrivateKey, PublicKey};
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
use nym_crypto::asymmetric::ed25519;
use nympsq::{
error::PSQError,
psq::{PSQInitiator, PSQResponder, CONTEXT_LEN, PSK_HANDLE_LEN},
};
use sha2::{Digest, Sha256};
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 })
}
}
}
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
pub fn generate_psk_v1(responder_pub_key: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.update(responder_pub_key);
hasher.finalize().into()
}
pub(crate) fn psq_initiate_x25519(
initiator: &mut PSQInitiator<X25519>,
responder_pub_key: impl AsRef<[u8]>,
context: &[u8; CONTEXT_LEN],
psq_ttl: Duration,
) -> Result<Vec<u8>, NoiseError> {
let pub_key =
PublicKey::decode(libcrux_kem::Algorithm::X25519, responder_pub_key.as_ref())?;
let message =
initiator.compute_initiator_message(&mut rand::rng(), &pub_key, context, psq_ttl)?;
Ok(message)
}
pub(crate) fn psq_respond_x25519(
responder_private_key: impl AsRef<[u8]>,
responder_public_key: impl AsRef<[u8]>,
initiator_verification_key: &ed25519::PublicKey,
initiator_message: &mut [u8],
context: &[u8; CONTEXT_LEN],
psq_ttl: Duration,
psk_handle: &[u8; PSK_HANDLE_LEN],
) -> Result<(Psk, Vec<u8>), PSQError> {
let kem_private_key = PrivateKey::decode(
libcrux_kem::Algorithm::X25519,
responder_private_key.as_ref(),
)?;
let kem_public_key = PublicKey::decode(
libcrux_kem::Algorithm::X25519,
responder_public_key.as_ref(),
)?;
let responder: PSQResponder<X25519> = PSQResponder::init(&kem_private_key, &kem_public_key);
let res = responder.compute_responder_message(
&VerificationKey::from_bytes(initiator_verification_key.to_bytes()),
initiator_message,
context,
psq_ttl,
psk_handle,
)?;
Ok(res)
}
pub(crate) fn psq_initiate_xwing(
initiator: &mut PSQInitiator<XWingKemDraft06>,
responder_pub_key: impl AsRef<[u8]>,
context: &[u8; CONTEXT_LEN],
psq_ttl: Duration,
) -> Result<Vec<u8>, NoiseError> {
let pub_key = PublicKey::decode(
libcrux_kem::Algorithm::XWingKemDraft06,
responder_pub_key.as_ref(),
)?;
let message =
initiator.compute_initiator_message(&mut rand::rng(), &pub_key, context, psq_ttl)?;
Ok(message)
}
pub(crate) fn psq_respond_xwing(
responder_private_key: impl AsRef<[u8]>,
responder_public_key: impl AsRef<[u8]>,
initiator_verification_key: &ed25519::PublicKey,
initiator_message: &mut [u8],
context: &[u8; CONTEXT_LEN],
psq_ttl: Duration,
psk_handle: &[u8; PSK_HANDLE_LEN],
) -> Result<(Psk, Vec<u8>), PSQError> {
let kem_private_key = PrivateKey::decode(
libcrux_kem::Algorithm::XWingKemDraft06,
responder_private_key.as_ref(),
)?;
let kem_public_key = PublicKey::decode(
libcrux_kem::Algorithm::XWingKemDraft06,
responder_public_key.as_ref(),
)?;
let responder: PSQResponder<XWingKemDraft06> =
PSQResponder::init(&kem_private_key, &kem_public_key);
let res = responder.compute_responder_message(
&VerificationKey::from_bytes(initiator_verification_key.to_bytes()),
initiator_message,
context,
psq_ttl,
psk_handle,
)?;
Ok(res)
}
}
pub async fn upgrade_noise_initiator(
conn: TcpStream,
config: &NoiseConfig,
initiator_identity_keypair: Option<ed25519::KeyPair>,
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
@@ -73,6 +165,7 @@ pub async fn upgrade_noise_initiator(
let handshake_version = match key.supported_version {
NoiseVersion::V1 => NoiseVersion::V1,
NoiseVersion::V2 => NoiseVersion::V2,
// 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.
@@ -82,14 +175,29 @@ pub async fn upgrade_noise_initiator(
}
};
let (signing_key, verification_key): (Option<[u8; 32]>, Option<[u8; 32]>) =
match initiator_identity_keypair {
Some(keypair) => (
Some(keypair.private_key().to_bytes()),
Some(keypair.public_key().to_bytes()),
),
None => (None, None),
};
NoiseStreamBuilder::new(conn)
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
.perform_initiator_handshake(
config,
handshake_version,
key.x25519_pubkey,
signing_key,
verification_key,
)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
pub async fn upgrade_noise_responder(
conn: TcpStream,
config: &NoiseConfig,
initiator_verification_key: Option<ed25519::PublicKey>,
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
@@ -112,7 +220,7 @@ pub async fn upgrade_noise_responder(
};
NoiseStreamBuilder::new(conn)
.perform_responder_handshake(config)
.perform_responder_handshake(config, initiator_verification_key)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
+193 -34
View File
@@ -1,20 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::{NoiseConfig, NoisePattern};
use crate::error::NoiseError;
use crate::psk_gen::generate_psk;
use crate::stream::codec::NymNoiseCodec;
use crate::stream::framing::NymNoiseFrame;
use crate::{
config::{NoiseConfig, NoisePattern},
error::NoiseError,
psk_gen::{generate_psk_v1, psq_initiate_x25519, psq_respond_x25519},
stream::{codec::NymNoiseCodec, framing::NymNoiseFrame},
NOISE_PSQ_DEFAULT_CONTEXT, NOISE_PSQ_DEFAULT_DURATION_SECS,
};
use bytes::{Bytes, BytesMut};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::x25519;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_noise_keys::NoiseVersion;
use nympsq::psq::{PSQInitiator, PSK_HANDLE_LEN};
use snow::{Builder, HandshakeState, TransportState};
use std::io;
use std::pin::Pin;
use std::task::Poll;
use std::{cmp::min, task::ready};
use std::{cmp::min, io, pin::Pin, task::ready, task::Poll, time::Duration};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_util::codec::Framed;
@@ -22,6 +23,7 @@ mod codec;
mod framing;
const TAGLEN: usize = 16;
// This needs to be increased when using psq
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
pub(crate) type Psk = [u8; 32];
@@ -45,19 +47,65 @@ impl<C> NoiseStreamBuilder<C> {
pattern: NoisePattern,
local_private_key: impl AsRef<[u8]>,
remote_pub_key: impl AsRef<[u8]>,
psk: Psk,
local_signing_key: Option<impl AsRef<[u8]>>,
local_verification_key: Option<impl AsRef<[u8]>>,
version: NoiseVersion,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let handshake = Builder::new(pattern.as_noise_params())
let mut handshake = Builder::new(pattern.as_noise_params())
.local_private_key(local_private_key.as_ref())
.remote_public_key(remote_pub_key.as_ref())
.psk(pattern.psk_position(), &psk)
.build_initiator()?;
self.perform_handshake(handshake, version, pattern).await
let payload: Vec<u8> = match version {
NoiseVersion::V1 => {
handshake.set_psk(
pattern.psk_position() as usize,
&generate_psk_v1(remote_pub_key.as_ref()),
)?;
vec![]
}
NoiseVersion::V2 => match (local_signing_key, local_verification_key) {
(Some(signing_key), Some(verification_key)) => {
let mut psq_initiator = PSQInitiator::init(signing_key, verification_key);
let payload = psq_initiate_x25519(
&mut psq_initiator,
&remote_pub_key,
NOISE_PSQ_DEFAULT_CONTEXT,
Duration::from_secs(NOISE_PSQ_DEFAULT_DURATION_SECS),
)?;
handshake.set_psk(
pattern.psk_position() as usize,
//
&psq_initiator.get_psk().unwrap(),
)?;
payload
}
(None, None) => {
return Err(NoiseError::MissingField {
info: "Missing Local Signing Key and Local Verification Key",
})
}
(None, _) => {
return Err(NoiseError::MissingField {
info: "Local Signing Key",
})
}
(_, None) => {
return Err(NoiseError::MissingField {
info: "Local Verification Key",
})
}
},
NoiseVersion::Unknown(version) => {
return Err(NoiseError::UnknownVersion { encoded: version })
}
};
self.perform_handshake(handshake, payload, version, pattern)
.await
}
pub(crate) async fn perform_initiator_handshake(
@@ -65,12 +113,12 @@ impl<C> NoiseStreamBuilder<C> {
config: &NoiseConfig,
version: NoiseVersion,
remote_pub_key: x25519::PublicKey,
local_signing_key: Option<impl AsRef<[u8]>>,
local_verification_key: Option<impl AsRef<[u8]>>,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let psk = generate_psk(remote_pub_key, version)?;
let timeout = config.timeout;
tokio::time::timeout(
timeout,
@@ -78,7 +126,8 @@ impl<C> NoiseStreamBuilder<C> {
config.pattern,
config.local_key.private_key(),
remote_pub_key,
psk,
local_signing_key,
local_verification_key,
version,
),
)
@@ -89,7 +138,8 @@ impl<C> NoiseStreamBuilder<C> {
mut self,
noise_pattern: NoisePattern,
local_private_key: impl AsRef<[u8]>,
local_pub_key: x25519::PublicKey,
local_public_key: impl AsRef<[u8]>,
initiator_verification_key: Option<ed25519::PublicKey>,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
@@ -117,26 +167,58 @@ impl<C> NoiseStreamBuilder<C> {
});
}
// 2. generate psk and handshake state
let psk = generate_psk(local_pub_key, initial_frame.header.version)?;
let mut handshake = Builder::new(pattern.as_noise_params())
.local_private_key(local_private_key.as_ref())
.psk(pattern.psk_position(), &psk)
.build_responder()?;
// update handshake state with initial frame
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
handshake.read_message(&initial_frame.data, &mut buf)?;
let len = handshake.read_message(&initial_frame.data, &mut buf)?;
buf.truncate(len);
let payload: Vec<u8> = match initial_frame.version() {
NoiseVersion::V1 => {
handshake.set_psk(
pattern.psk_position() as usize,
&generate_psk_v1(local_public_key.as_ref()),
)?;
vec![]
}
NoiseVersion::V2 => match initiator_verification_key {
Some(verif_key) => {
let (psk, message) = psq_respond_x25519(
&local_private_key,
&local_public_key,
&verif_key,
&mut buf,
NOISE_PSQ_DEFAULT_CONTEXT,
Duration::from_secs(NOISE_PSQ_DEFAULT_DURATION_SECS),
&[1; PSK_HANDLE_LEN],
)?;
handshake.set_psk(pattern.psk_position() as usize, psk.as_slice())?;
message
}
None => {
return Err(NoiseError::MissingField {
info: "Initiator Verification Key",
})
}
},
NoiseVersion::Unknown(version) => {
return Err(NoiseError::UnknownVersion { encoded: version })
}
};
// 3. run handshake to completion
self.perform_handshake(handshake, initial_frame.version(), pattern)
self.perform_handshake(handshake, payload, initial_frame.version(), pattern)
.await
}
pub(crate) async fn perform_responder_handshake(
self,
config: &NoiseConfig,
initiator_verification_key: Option<ed25519::PublicKey>,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
@@ -147,7 +229,8 @@ impl<C> NoiseStreamBuilder<C> {
self.perform_responder_handshake_inner(
config.pattern,
config.local_key.private_key(),
*config.local_key.public_key(),
config.local_key.public_key(),
initiator_verification_key,
),
)
.await?
@@ -156,6 +239,7 @@ impl<C> NoiseStreamBuilder<C> {
async fn send_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
payload: impl AsRef<[u8]>,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<(), NoiseError>
@@ -163,7 +247,7 @@ impl<C> NoiseStreamBuilder<C> {
C: AsyncRead + AsyncWrite + Unpin,
{
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)?;
let len = handshake.write_message(payload.as_ref(), &mut buf)?;
buf.truncate(len);
let frame = NymNoiseFrame::new_handshake_frame(buf.freeze(), version, pattern)?;
@@ -176,7 +260,7 @@ impl<C> NoiseStreamBuilder<C> {
handshake: &mut HandshakeState,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<(), NoiseError>
) -> Result<Bytes, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
@@ -199,9 +283,12 @@ impl<C> NoiseStreamBuilder<C> {
});
}
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
// The buffer below is the payload...
// we're in the handshake, we can afford a smaller buffer
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
handshake.read_message(&frame.data, &mut buf)?;
Ok(())
Ok(buf.into())
}
Some(Err(err)) => Err(err),
None => Err(NoiseError::HandshakeError),
@@ -211,6 +298,7 @@ impl<C> NoiseStreamBuilder<C> {
async fn perform_handshake(
mut self,
mut handshake_state: HandshakeState,
payload: impl AsRef<[u8]>,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<NoiseStream<C>, NoiseError>
@@ -219,7 +307,7 @@ impl<C> NoiseStreamBuilder<C> {
{
while !handshake_state.is_handshake_finished() {
if handshake_state.is_my_turn() {
self.send_handshake_msg(&mut handshake_state, version, pattern)
self.send_handshake_msg(&mut handshake_state, &payload, version, pattern)
.await?;
} else {
self.recv_handshake_msg(&mut handshake_state, version, pattern)
@@ -519,24 +607,26 @@ mod tests {
}
#[tokio::test]
async fn noise_handshake() -> anyhow::Result<()> {
async fn noise_handshake_v1() -> anyhow::Result<()> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let no_key: Option<[u8; 0]> = None;
let (initiator_stream, responder_stream) = mock_streams();
let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
let pattern = NoisePattern::default();
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
.perform_initiator_handshake_inner(
pattern,
initiator_keys.private_key().to_bytes(),
responder_keys.public_key().to_bytes(),
psk,
*responder_keys.public_key(),
no_key,
no_key,
NoiseVersion::V1,
);
@@ -545,6 +635,75 @@ mod tests {
pattern,
responder_keys.private_key().to_bytes(),
*responder_keys.public_key(),
None,
);
let initiator_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_initiator).await },
);
let responder_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_responder).await },
);
let (initiator, responder) = join!(initiator_fut, responder_fut);
let mut initiator = initiator???;
let mut responder = responder???;
let msg = b"hello there";
// if noise was successful we should be able to write a proper message across
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;
initiator.inner_stream.flush().await?;
let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();
let mut buf = [0u8; 11];
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
assert_eq!(&buf[..], msg);
// the inner content is different from the actual msg since it was encrypted
assert_ne!(inner_buf, buf);
assert_ne!(inner_buf.len(), msg.len());
Ok(())
}
#[tokio::test]
async fn noise_handshake_v2() -> anyhow::Result<()> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let initator_identity_keys = Arc::new(ed25519::KeyPair::new(&mut rng));
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let init_verif_key = initator_identity_keys.public_key().clone();
let (initiator_stream, responder_stream) = mock_streams();
let pattern = NoisePattern::default();
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
.perform_initiator_handshake_inner(
pattern,
initiator_keys.private_key().to_bytes(),
responder_keys.public_key().to_bytes(),
Some(initator_identity_keys.private_key().to_bytes()),
Some(initator_identity_keys.public_key().to_bytes()),
NoiseVersion::V2,
);
let stream_responder = NoiseStreamBuilder::new(responder_stream)
.perform_responder_handshake_inner(
pattern,
responder_keys.private_key().to_bytes(),
*responder_keys.public_key(),
Some(init_verif_key),
);
let initiator_fut =
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "nympsq"
version = "0.1.0"
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
edition = "2021"
license.workspace = true
[dependencies]
thiserror = { workspace = true }
# 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"] }
libcrux-ed25519 = { git = "https://github.com/cryspen/libcrux" }
rand = "0.9.2"
tls_codec = { version = "0.4.2", features = ["derive"] }
[dev-dependencies]
criterion = {workspace = true}
[[bench]]
name = "benches"
harness = false
[lints]
workspace = true
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use criterion::{criterion_group, criterion_main, Criterion};
use nym_crypto::asymmetric::ed25519;
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 gen_x25519_keypair(c: &mut Criterion) {
c.bench_function("Generate DHKem Keypair", |b| {
b.iter(|| libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap());
});
}
pub fn gen_xwing_keypair(c: &mut Criterion) {
c.bench_function("Generate XWingKem Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng()).unwrap()
});
});
}
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
gen_x25519_keypair,
gen_xwing_keypair
);
criterion_main!(benches);
+58
View File
@@ -0,0 +1,58 @@
// 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 a Serialization/Deserialization error")]
SerializationError,
#[error("encountered an IO error: {0}")]
IoError(#[from] io::Error),
#[error("Incorrect state")]
IncorrectStateError,
#[error("Handshake did not complete")]
HandshakeError,
#[error("Unknown PSQ version")]
UnknownVersion,
}
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,
}
}
}
impl From<tls_codec::Error> for PSQError {
fn from(err: tls_codec::Error) -> Self {
match err {
_err => PSQError::SerializationError,
}
}
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod psq;
+270
View File
@@ -0,0 +1,270 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{io::Cursor, marker::PhantomData, time::Duration};
use libcrux_ed25519::VerificationKey;
use libcrux_psq::{
cred::Ed25519,
psk_registration::{Initiator, InitiatorMsg, Responder, ResponderMsg},
traits::PSQ,
};
use tls_codec::{Deserialize, Serialize};
use libcrux_traits::kem::KEM;
use rand::{CryptoRng, RngCore};
use crate::error::PSQError;
pub const PSK_LENGTH: usize = 32;
pub const CONTEXT_LEN: usize = 16;
pub const PSK_HANDLE_LEN: usize = 16;
pub struct PSQInitiator<T: PSQ> {
signing_key: [u8; 32],
verification_key: VerificationKey,
state: Option<Initiator>,
_t: PhantomData<T>,
}
impl<T: PSQ> PSQInitiator<T> {
pub fn init(signing_key: impl AsRef<[u8]>, verification_key: impl AsRef<[u8]>) -> Self {
let mut sig_key: [u8; 32] = [0u8; 32];
sig_key.clone_from_slice(signing_key.as_ref());
let mut verif_key: [u8; 32] = [0u8; 32];
verif_key.clone_from_slice(verification_key.as_ref());
Self {
signing_key: sig_key,
verification_key: VerificationKey::from_bytes(verif_key),
state: None,
_t: PhantomData,
}
}
pub fn compute_initiator_message<R>(
&mut self,
rng: &mut R,
responder_kem_public_key: &<T::InnerKEM as KEM>::EncapsulationKey,
context: &[u8; CONTEXT_LEN],
psk_ttl: Duration,
) -> Result<Vec<u8>, PSQError>
where
<T as PSQ>::InnerKEM: KEM,
R: RngCore + CryptoRng,
{
let (state, message) = Initiator::send_initial_message::<Ed25519, T>(
context,
psk_ttl,
responder_kem_public_key,
&self.signing_key,
&self.verification_key,
rng,
)?;
self.state = Some(state);
Ok(message.tls_serialize_detached()?)
}
// The initator generates the PSK themselves while computing the first message.
// It's possible to get_psk() this after sending the first message.
// The key will only be valid if we're sure that the responder has received it.
//
// If we use noise right after PSQ, we can just extract the PSK and expect the responder
// to just send the first Noise message instead of responding to with a PSQ message.
// In other words, instead of using the responder's PSQ message for key confirmation,
// we could use a Noise handshake for key confirmation.
pub fn get_psk(&self) -> Option<[u8; 32]> {
match &self.state {
Some(initiator_state) => Some(*initiator_state.unregistered_psk()),
None => None,
}
}
pub fn finalize(&self, responder_message: &[u8]) -> Result<[u8; PSK_LENGTH], PSQError> {
match &self.state {
Some(state) => match ResponderMsg::tls_deserialize_exact(responder_message) {
Ok(deserialized_responder_message) => {
match state.complete_handshake(&deserialized_responder_message) {
Ok(psk) => Ok(psk.psk),
Err(err) => Err(err.into()),
}
}
Err(err) => Err(err.into()),
},
None => Err(PSQError::IncorrectStateError),
}
}
}
pub struct PSQResponder<'a, T: PSQ>
where
<T as PSQ>::InnerKEM: KEM,
{
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>
where
<T as PSQ>::InnerKEM: KEM,
{
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 compute_responder_message(
&self,
initiator_verification_key: &VerificationKey,
initiator_message: &mut [u8],
context: &[u8; CONTEXT_LEN],
psk_ttl: Duration,
psk_handle: &[u8; PSK_HANDLE_LEN],
) -> Result<([u8; PSK_LENGTH], Vec<u8>), PSQError> {
let mut cursor = Cursor::new(initiator_message);
let deserialized_initiator_message = InitiatorMsg::tls_deserialize(&mut cursor)?;
// the problem is here the deserialized message is the same on the initiator's side
let (registered_psk, responder_msg) = Responder::send::<Ed25519, T>(
psk_handle,
psk_ttl,
context,
self.kem_public_key,
self.kem_private_key,
initiator_verification_key,
&deserialized_initiator_message,
)?;
let responder_bytes = responder_msg.tls_serialize_detached()?;
Ok((registered_psk.psk, responder_bytes))
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use libcrux_ed25519::VerificationKey;
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 crate::psq::{CONTEXT_LEN, PSK_HANDLE_LEN};
use super::{PSQInitiator, PSQResponder};
fn test_helper<T: PSQ, R>(
rng: &mut R,
responder_kem_private_key: <T::InnerKEM as KEM>::DecapsulationKey,
responder_kem_public_key: <T::InnerKEM as KEM>::EncapsulationKey,
) where
R: CryptoRng + rand::RngCore,
{
// set PSQ TTL
let psk_ttl: Duration = Duration::from_secs(3600);
// generate random context string
let mut context: [u8; CONTEXT_LEN] = [0u8; CONTEXT_LEN];
rng.fill_bytes(&mut context);
// generate random psk handle
let mut psk_handle: [u8; PSK_HANDLE_LEN] = [0u8; PSK_HANDLE_LEN];
rng.fill_bytes(&mut psk_handle);
// 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.private_key().to_bytes(),
initiator_ed25519_keypair.public_key().to_bytes(),
);
let responder: PSQResponder<T> =
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
let mut initiator_msg = initiator
.compute_initiator_message(rng, &responder_kem_public_key, &context, psk_ttl)
.unwrap();
let pre_response_psk = initiator.get_psk().unwrap();
let initiator_public_key =
VerificationKey::from_bytes(initiator_ed25519_keypair.public_key().to_bytes());
let (responder_psk, responder_msg) = responder
.compute_responder_message(
&initiator_public_key,
&mut initiator_msg,
&context,
psk_ttl,
&psk_handle,
)
.unwrap();
let initiator_psk = initiator.finalize(&responder_msg).unwrap();
assert_eq!(initiator_psk, pre_response_psk);
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 xwing 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 dhkem 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,
);
}
}
+20
View File
@@ -45,6 +45,10 @@ pub enum PeerControlRequest {
key: Key,
response_tx: oneshot::Sender<QueryBandwidthControlResponse>,
},
GetClientBandwidth {
key: Key,
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
},
}
pub struct AddPeerControlResponse {
@@ -65,6 +69,10 @@ pub struct QueryBandwidthControlResponse {
pub bandwidth_data: Option<RemainingBandwidthData>,
}
pub struct GetClientBandwidthControlResponse {
pub client_bandwidth: Option<ClientBandwidth>,
}
pub struct PeerController {
storage: GatewayStorage,
@@ -267,6 +275,14 @@ impl PeerController {
}))
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
if let Some(Some(bandwidth_storage_manager)) = self.bw_storage_managers.get(key) {
Some(bandwidth_storage_manager.read().await.client_bandwidth())
} else {
None
}
}
async fn update_metrics(&self, new_host: &Host) {
let now = SystemTime::now();
const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(180);
@@ -388,6 +404,10 @@ impl PeerController {
response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok();
}
}
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
}
None => {
log::trace!("PeerController [main loop]: stopping since channel closed");
break;
+46 -40
View File
@@ -13,12 +13,10 @@ use nym_gateway_storage::models::WireguardPeer;
use nym_task::TaskClient;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
pub struct PeerHandle {
public_key: Key,
@@ -28,7 +26,6 @@ pub struct PeerHandle {
request_tx: mpsc::Sender<PeerControlRequest>,
timeout_check_interval: IntervalStream,
task_client: TaskClient,
startup_timestamp: SystemTime,
}
impl PeerHandle {
@@ -53,7 +50,6 @@ impl PeerHandle {
request_tx,
timeout_check_interval,
task_client,
startup_timestamp: SystemTime::now(),
}
}
@@ -73,26 +69,49 @@ impl PeerHandle {
Ok(success)
}
async fn active_peer(
&mut self,
storage_peer: &WireguardPeer,
kernel_peer: &Peer,
) -> Result<bool, Error> {
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
if kernel_peer.last_handshake.is_none()
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
{
let success = self.remove_peer().await?;
self.peer_storage_manager.remove_peer();
tracing::debug!(
"Peer {} has not been active for more then {} seconds, removing it",
kernel_peer.public_key.to_string(),
AUTO_REMOVE_AFTER.as_secs()
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
.ok()?;
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
.ok()?;
let kernel_total = kernel_peer
.rx_bytes
.checked_add(kernel_peer.tx_bytes)
.or_else(|| {
tracing::error!(
"Overflow on kernel adding bytes: {} + {}",
kernel_peer.rx_bytes,
kernel_peer.tx_bytes
);
return Ok(!success);
}
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
None
})?;
let storage_total = storage_peer_rx_bytes
.checked_add(storage_peer_tx_bytes)
.or_else(|| {
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
None
})?;
kernel_total.checked_sub(storage_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
None
})
}
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
.unwrap_or_else(|| {
// if gateway restarted, the kernel values restart from 0
// and we should restart from 0 in storage as well
@@ -100,8 +119,10 @@ impl PeerHandle {
self.peer_storage_manager.peer_information.as_mut()
{
peer_information.force_sync = true;
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
}
kernel_peer.rx_bytes + kernel_peer.tx_bytes
0
})
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
@@ -124,14 +145,6 @@ impl PeerHandle {
}
}
} else {
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
log::debug!(
"Peer {} has been present for 30 days, removing it",
self.public_key
);
let success = self.remove_peer().await?;
return Ok(!success);
}
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
log::debug!(
@@ -158,14 +171,7 @@ impl PeerHandle {
// the host information hasn't beed updated yet
return Ok(true);
};
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if !self.active_peer(&storage_peer, &kernel_peer).await? {
if !self.active_peer(&kernel_peer).await? {
log::debug!(
"Peer {:?} is not active anymore, shutting down handle",
self.public_key
-136
View File
@@ -1,136 +0,0 @@
version: "3.7"
x-network: &NETWORK
BECH32_PREFIX: nymt
DENOM: nymt
STAKE_DENOM: nyxt
WASMD_VERSION: v0.27.0
services:
genesis_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "26657:26657"
- "1317:1317"
container_name: genesis_validator
volumes:
- "genesis_volume:/genesis_volume"
- "genesis_nyxd:/root/.nyxd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.2
command: ["genesis"]
secondary_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "36657:26657"
- "2317:1317"
volumes:
- "genesis_volume:/genesis_volume"
- "secondary_nyxd:/root/.nyxd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.3
depends_on:
- "genesis_validator"
command: ["secondary"]
# mixnet_contract:
# build: docker/mixnet_contract
# image: contract:latest
# volumes:
# - ".:/nym"
# vesting_contract:
# build: docker/vesting_contract
# image: vesting_contract:latest
# volumes:
# - ".:/nym"
# contract_uploader:
# build: docker/typescript_client
# image: contract_uploader:typescript
# volumes:
# - "genesis_volume:/genesis_volume:ro"
# - "contract_volume:/contract_volume"
# - ".:/nym"
# depends_on:
# - "genesis_validator"
# - "secondary_validator"
# - "mixnet_contract"
# environment:
# BECH32_PREFIX: *BECH32_PREFIX
mnemonic_echo:
build: docker/mnemonic_echo
image: mnemonic_echo:latest
volumes:
- "genesis_volume:/genesis_volume:ro"
depends_on:
- "genesis_validator"
- "secondary_validator"
# mongo:
# image: mongo:latest
# command:
# - --storageEngine=wiredTiger
# volumes:
# - mongo_data:/data/db
# block_explorer:
# build:
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
# image: block_explorer:v0.41.x-7
# ports:
# - "3080:3000"
# depends_on:
# - "mongo"
# environment:
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
# MONGO_URL: mongodb://mongo:27017/meteor
# PORT: 3000
# METEOR_SETTINGS: ${METEOR_SETTINGS}
# explorer:
# build:
# context: docker/explorer
# image: explorer:latest
# ports:
# - "3040:3000"
# depends_on:
# - "genesis_validator"
# - "block_explorer"
# service to update geoip binary database, for explorer-api
geoipupdate:
container_name: geoipupdate
image: maxmindinc/geoipupdate
restart: unless-stopped
environment:
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
networks:
- geoipupdate
volumes:
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
volumes:
genesis_volume:
genesis_nyxd:
secondary_nyxd:
# contract_volume:
# mongo_data:
networks:
geoipupdate:
localnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.168.10.0/25
@@ -13,10 +13,6 @@ Stake saturation is a node reputation done in a form of self bond or stakers del
> **rewarded_set_size = active_set_size + standby_set_size**
</Callout>
{/*
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
*/}
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
Node stake saturation is a value between `0` and `1` following this logic.
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "188691142067690"
"amount": "187227500568584"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "811308857932310"
"amount": "812772499431416"
}
}
@@ -1 +1 @@
811_308_857
812_772_499
@@ -1 +1 @@
1_034_081
1_037_131
@@ -1 +1 @@
405_654_428
248_911_577
@@ -1 +1 @@
248_179_643
248_911_577
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 188_691_142 |
| Mixmining Reserve | Tokens releasing for operators rewards | 187_227_500 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 811_308_857 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_034_081 |
| Circulating Supply | Amount of unlocked tokens | 812_772_499 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_037_131 |
@@ -1,10 +1,10 @@
{
"interval": {
"reward_pool": "188691142067690.265566584946684804",
"staking_supply": "248179643769563.241524591796100959",
"staking_supply_scale_factor": "0.5",
"epoch_reward_budget": "5241420612.991396265738470741",
"stake_saturation_point": "1034081849039.84683968579915042",
"reward_pool": "187227500568584.066152911712126276",
"staking_supply": "248911577950871.129740670788161327",
"staking_supply_scale_factor": "0.30625",
"epoch_reward_budget": "5200763904.682890726469769781",
"stake_saturation_point": "1037131574795.296373919461617338",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
@@ -1 +1 @@
Wednesday, May 28th 2025, 11:39:51 UTC
Thursday, June 12th 2025, 11:03:30 UTC
+1 -1
View File
@@ -45,7 +45,7 @@
"chain-registry": "^1.19.0",
"cosmjs-types": "^0.9.0",
"lucide-react": "^0.438.0",
"next": "^14.2.15",
"next": "^15.2.4",
"nextra": "2",
"nextra-theme-docs": "2",
"react": "^18.2.0",
@@ -47,6 +47,53 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.11-cheddar`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar)
- [`nym-node`](nodes/nym-node.mdx) version `1.13.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-06-10T09:41:31.291089877Z
Build Version: 1.13.0
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
Commit Date: 2025-06-10T11:39:20.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance
- **NIP1** will outline the governance process: Its draft will be published soon for discussion
- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable
- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz)
- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!)
### Features
- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783)
- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there
- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787)
- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility.
- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800):
- Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API`
- Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code).
- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802)
- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804)
- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812)
- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831)
## `v2025.10-brie`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie)
@@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
## Current version
```sh
nym-node
ym-node
Binary Name: nym-node
Build Timestamp: 2025-05-27T10:13:18.511075453Z
Build Version: 1.12.0
Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0
Commit Date: 2025-05-27T12:11:13.000000000+02:00
Build Timestamp: 2025-06-10T09:41:31.291089877Z
Build Version: 1.13.0
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
Commit Date: 2025-06-10T11:39:20.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
@@ -104,6 +104,7 @@ To get a full comprehension of [node operators rewards](tokenomics/mixnet-reward
```
#### Supply
<br />
<b>Circulating supply is <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM.</b>
@@ -118,15 +119,10 @@ A number of aimed NYM tokens to be staked in the network. The staking target a i
> **staking_target = staking_supply_scale_factor \* circulating_supply**
</Callout>
{/*
Staking supply scale factor is currently it's set to be <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
Staking supply scale factor is currently at <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
The value of this variable can is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>, <b>the staking target is <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM.</b>
*/}
Staking supply scale factor is currently at 30.5%.
The value of this variable is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor 30.5%, <b>the staking target is <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM.</b>
#### Stake saturation
+1631 -500
View File
File diff suppressed because it is too large Load Diff
@@ -140,6 +140,7 @@ def get_nested_value(response, args):
def _return_percent_annotation(value):
value = float(value) * 100
value = round(value, 2)
value = f"{value}%"
return value
@@ -269,7 +270,7 @@ def parser_main():
action="store_true",
help="A multiplier of staking supply scale factor and circulating supply"
)
parser_calculate.add_argument(
"-s", "--separator",
type=str,
-25
View File
@@ -1,25 +0,0 @@
CONFIGURED=true
RUST_LOG=info
RUST_BACKTRACE=1
NETWORK_NAME=qa
BECH32_PREFIX=n
MIX_DENOM=unym
MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
NYXD=https://rpc.qa-validator.qa.nymte.ch
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/
-21
View File
@@ -1,21 +0,0 @@
[package]
name = "nym-bity-integration"
version = "0.1.0"
edition = "2021"
rust-version = "1.56"
license.workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
eyre = { workspace = true }
cosmrs = { workspace = true }
nym-cli-commands = { path = "../../common/commands" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
[dev-dependencies]
anyhow = { workspace = true }
-53
View File
@@ -1,53 +0,0 @@
# Buy NYM with Bity
This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases.
## Signing
The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields:
```
account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf
message: "This is the order message from Bity"
order signature:
{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
```
The `signature` field of the order contains a JSON representation of:
- the Cosmos address of the signer (`account_id`)
- the Cosmos public key
- a hex string digest of the Bity order message signed by the user
Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered.
## Verification
Verification has been wrapped up into taking a single struct that can be parsed from JSON:
```
{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": << ORDER SIGNATURE JSON GOES HERE >>
}
```
The following will be checked:
- the `account_id` supplied matches:
- the account id derived from the public key
- the account id field in the order signature JSON
- the account id is for Nym mainnet
- the signature is for the message
- all data structures parse correctly
- nested structs
- account ids
- Cosmos public keys
-220
View File
@@ -1,220 +0,0 @@
pub mod order;
pub mod sign;
pub mod verify;
#[cfg(test)]
mod tests {
use crate::order::{Order, OrderSignature};
use crate::{sign, verify};
use cosmrs::AccountId;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use std::str::FromStr;
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
let accounts = wallet.try_derive_accounts()?;
let message = "This is the order message from Bity";
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
Ok((signature, message.to_string()))
}
#[test]
fn integration_happy_path() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap();
assert_eq!(account_id.to_string(), signature.account_id.to_string());
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
Ok(verify::verify_order(Order {
account_id,
signature,
message,
})?)
}
#[test]
fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> {
let (signature, message) = get_order("nymt")?;
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
let res = verify::verify_order(Order {
account_id: signature.clone().account_id,
signature,
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> {
let (signature, message) = get_order("atom")?;
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
let res = verify::verify_order(Order {
account_id: signature.clone().account_id,
signature,
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_change_account_id() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let OrderSignature {
signature_as_hex,
public_key,
account_id: _,
} = signature;
// use a different account id to the one that signed the order
let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap();
let res = verify::verify_order(Order {
account_id: account_id.clone(),
signature: OrderSignature {
account_id,
signature_as_hex,
public_key,
},
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_change_signature() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let OrderSignature {
signature_as_hex: _,
public_key,
account_id,
} = signature;
let res = verify::verify_order(Order {
account_id: account_id.clone(),
signature: OrderSignature {
account_id,
signature_as_hex: "this is not the signature you were looking for".to_string(),
public_key,
},
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_happy_path() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
Ok(verify::verify_order(order)?)
}
#[test]
fn integration_with_json_bad_signature() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "A different message to the one signed",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_bad_account_id() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
}
-17
View File
@@ -1,17 +0,0 @@
use cosmrs::crypto::PublicKey;
use cosmrs::AccountId;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderSignature {
pub public_key: PublicKey,
pub account_id: AccountId,
pub signature_as_hex: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Order {
pub account_id: AccountId,
pub message: String,
pub signature: OrderSignature,
}
-20
View File
@@ -1,20 +0,0 @@
use crate::order::OrderSignature;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::signer::OfflineSigner;
use nym_validator_client::signing::AccountData;
/// Signs an order message to purchase Nym with Bity
pub fn sign_order(
wallet: &DirectSecp256k1HdWallet,
signer: &AccountData,
message: String,
) -> Result<OrderSignature, NyxdError> {
Ok(OrderSignature {
account_id: signer.address().clone(),
public_key: signer.public_key(),
signature_as_hex: wallet
.sign_raw_with_account(signer, message.into_bytes())?
.to_string(),
})
}
-59
View File
@@ -1,59 +0,0 @@
use thiserror::Error;
use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key;
use crate::order::Order;
#[derive(Error, Debug)]
pub enum VerifyOrderError {
#[error("{source}")]
K256Error {
#[from]
source: k256::ecdsa::Error,
},
#[error("{source}")]
ErrorReport {
#[from]
source: eyre::Report,
},
#[error("Account id does not match public key")]
AccountIdDoesNotMatchPubKey,
#[error("Unsupported key type. Only secp256k1 is currently supported.")]
UnsupportedKeyType,
#[error("Signature error - {0}")]
SignatureError(k256::ecdsa::signature::Error),
#[error("Account id is not a Nyx mainnet account")]
AccountIdPrefixIncorrect,
}
/// Verifies an order
pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> {
let account_id = order.signature.public_key.account_id("n")?;
if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" {
return Err(VerifyOrderError::AccountIdPrefixIncorrect);
}
// the account id in the order must match the account id derived from the public key
if account_id != order.signature.account_id {
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
}
// the user provided account id in the order must match the derived account id
if account_id != order.account_id || account_id != order.signature.account_id {
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
}
if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL {
return Err(VerifyOrderError::UnsupportedKeyType);
}
match secp256k1_verify_with_public_key(
&order.signature.public_key.to_bytes(),
order.signature.signature_as_hex,
order.message,
) {
Ok(()) => Ok(()),
Err(e) => Err(VerifyOrderError::SignatureError(e)),
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.59"
version = "1.1.60"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -13,13 +13,10 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> {
Router::new()
.route("/", axum::routing::get(get_full_circulating_supply))
.route(
"/total-supply-value",
"/circulating-supply-value",
axum::routing::get(get_circulating_supply),
)
.route(
"/circulating-supply-value",
axum::routing::get(get_total_supply),
)
.route("/total-supply-value", axum::routing::get(get_total_supply))
}
#[utoipa::path(
-8
View File
@@ -1,8 +0,0 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
+14 -5
View File
@@ -1,5 +1,14 @@
# environment
.env.dev
# error logs
yarn-error.log
dist/
build/
**/*.wasm
**/*.js.map
.env*
!.env.example
.vscode/
.idea/
*.swp
*.swo
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-1
View File
@@ -1 +0,0 @@
16
-6
View File
@@ -1,6 +0,0 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
-7
View File
@@ -1,7 +0,0 @@
RPC_URL=
VALIDATOR_URL=
PREFIX=
MIXNET_CONTRACT_ADDRESS=
VESTING_CONTRACT_ADDRESS=
DENOM=
BLOCK_EXPLORER_URL=
View File
+108 -23
View File
@@ -1,40 +1,125 @@
# Nym Browser Extension
# Nym Browser Extension Storage
The Nym browser extension lets you access your Nym wallet via the browser.
A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption.
## Getting started
## Overview
You will need:
This storage component is built in Rust and compiled to WebAssembly, providing:
- NodeJS (use `nvm install` to automatically install the correct version)
- `npm`
- `yarn`
- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics
- **IndexedDB integration**: Browser-native persistent storage
- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names
- **Type-safe API**: Promise-based JavaScript API with proper error handling
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
## Getting Started
From the [root of the repository](../README.md) run the following to build shared packages:
### Prerequisites
```
yarn
yarn build
- Rust (latest stable)
- `wasm-pack` tool for building WebAssembly
- Node.js (for the demo server)
### Building
```bash
cd storage
make wasm-pack
```
From the `nym-browser-extension` directory of the `nym` monorepo, run:
This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings.
`yarn dev` to run the extension in dev mode.
### Example Usage
You can then open a browser to http://localhost:9000 and start development.
See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples.
OR
Basic usage:
`yarn build` to build the extension.
```javascript
import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage"
The extension will build to the `nym-browser-extension/dist` directory.
// Initialize the WASM module first
await init();
## Load extension
// Set up better error handling
set_panic_hook();
To load the extension into a Chrome browser
// Create storage instance with password
const storage = await new ExtensionStorage("your-secure-password");
- Go to `settings > extensions > manage extensions`
- Select `Load unpacked`
- Select the `nym-browser-extension/dist`
// Store a mnemonic
const mnemonic = "your twenty four word mnemonic phrase goes here...";
await storage.store_mnemonic("my-wallet", mnemonic);
// Read a mnemonic
const retrievedMnemonic = await storage.read_mnemonic("my-wallet");
// Check if a mnemonic exists
const exists = await storage.has_mnemonic("my-wallet");
// Get all stored mnemonic keys
const allKeys = await storage.get_all_mnemonic_keys();
// Remove a mnemonic
await storage.remove_mnemonic("my-wallet");
```
## Development
To run the internal development example:
```bash
cd storage
make demo
# Option 2: Manual server setup
cd internal-dev && node serve.js
# Then open http://localhost:8000 in your browser
```
**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably.
## API Reference
### Initialization
- `init()` - **Required**: Initialize the WASM module before using other functions
### Constructor
- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password
### Static Methods
- `ExtensionStorage.exists()` - Check if storage database exists
### Instance Methods
- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name
- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found)
- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists
- `get_all_mnemonic_keys()` - Get all stored mnemonic names
- `remove_mnemonic(name: string)` - Remove a mnemonic by name
### Error Handling
- `set_panic_hook()` - Set up better stack traces for Rust panics in development
## Security Features
- **Password-based encryption**: All data is encrypted using the provided password
- **BIP39 validation**: Mnemonics are validated before storage
- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed
- **Browser sandbox**: Runs within the browser's security model
## Architecture
The storage component consists of:
- **Rust core** (`src/storage.rs`): Main storage implementation with encryption
- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer
- **Error handling** (`src/error.rs`): Comprehensive error types
- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management
## Important Notes
1. **WASM Initialization**: Always call `await init()` before using any other functions
2. **MIME Types**: The demo requires a server that properly serves WASM files
3. **Browser Compatibility**: Requires modern browsers with WebAssembly support
4. **Module Loading**: Uses ES modules - ensure your build system supports them
-86
View File
@@ -1,86 +0,0 @@
{
"name": "@nym/browser-extension",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "yarn webpack serve --config webpack.dev.js",
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:ts": "tsc --noEmit",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
},
"dependencies": {
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.7.0",
"@hookform/resolvers": "^3.1.0",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.15",
"@mui/system": "^5.11.15",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@nymproject/extension-storage": ">=1.2.0-rc.0 || 1",
"@storybook/react": "^6.5.16",
"big.js": "^6.2.1",
"crypto-js": "^4.1.1",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.3",
"react-hook-form": "^7.43.9",
"react-router-dom": "^6.9.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
"@types/big.js": "^6.1.6",
"@types/crypto-js": "4.1.1",
"@types/jest": "^27.0.1",
"@types/node": "^18.16.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"copy-webpack-plugin": "^11.0.0",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.12",
"favicons-webpack-plugin": "^5.0.2",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "^2.8.7",
"react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.8",
"style-loader": "^3.3.1",
"ts-jest": "^27.0.5",
"ts-loader": "^9.4.2",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"util": "^0.12.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
}
}
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
import { AppRoutes } from './routes';
import { AppLayout } from './layouts/AppLayout';
import { AppProvider } from './context';
export const App = () => (
<NymBrowserExtThemeWithMode mode="light">
<AppProvider>
<AppLayout>
<AppRoutes />
</AppLayout>
</AppProvider>
</NymBrowserExtThemeWithMode>
);
@@ -1,47 +0,0 @@
import React from 'react';
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useAppContext } from 'src/context';
import { AccountActions } from './Actions';
const AccountItem = ({
accountName,
disabled,
onSelect,
}: {
accountName: string;
disabled: boolean;
onSelect: () => void;
}) => (
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
<ListItemButton onClick={onSelect} disabled={disabled}>
<ListItemAvatar>
<Avatar>{accountName[0]}</Avatar>
</ListItemAvatar>
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
</ListItemButton>
</ListItem>
);
export const AccountList = () => {
const navigate = useNavigate();
const { accounts, selectAccount, selectedAccount } = useAppContext();
const handleSelectAccount = async (accountName: string) => {
await selectAccount(accountName);
navigate('/user/balance');
};
return (
<>
{accounts.map((accountName) => (
<AccountItem
disabled={selectedAccount === accountName}
accountName={accountName}
key={accountName}
onSelect={() => handleSelectAccount(accountName)}
/>
))}
</>
);
};
@@ -1,55 +0,0 @@
import React from 'react';
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
import { useAppContext } from 'src/context';
type ActionType = {
title: string;
Icon: React.ReactNode;
onSelect: () => void;
};
const ActionItem = ({ action }: { action: ActionType }) => (
<MenuItem dense onClick={action.onSelect}>
<ListItemIcon>{action.Icon}</ListItemIcon>
<ListItemText>{action.title}</ListItemText>
</MenuItem>
);
export const AccountActions = ({ accountName }: { accountName: string }) => {
const { setShowSeedForAccount } = useAppContext();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const actions: Array<ActionType> = [
{
title: 'View seed phrase',
Icon: <VisibilityOutlined />,
onSelect: () => {
setShowSeedForAccount(accountName);
},
},
];
return (
<>
<IconButton onClick={handleClick}>
<MoreVert />
</IconButton>
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
{actions.map((action) => (
<ActionItem action={action} key={action.title} />
))}
</Menu>
</>
);
};
@@ -1,68 +0,0 @@
import React, { useState } from 'react';
import { Box, Card, CardContent, Typography } from '@mui/material';
import { PasswordInput } from '@nymproject/react/textfields/Password';
import { ExtensionStorage } from '@nymproject/extension-storage';
import { Button, ConfirmationModal } from 'src/components/ui';
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
Show seed phrase
</Button>
);
const DoneButton = ({ onDone }: { onDone: () => void }) => (
<Button fullWidth variant="contained" onClick={onDone}>
Done
</Button>
);
const Seed = ({ seed }: { seed: string }) => (
<Card>
<CardContent>
<Typography>{seed}</Typography>
</CardContent>
</Card>
);
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
const [seed, setSeed] = useState<string>();
const [password, setPassword] = useState('');
const [error, setError] = useState<string>();
const handleShowSeedPhrase = async () => {
try {
const storage = await new ExtensionStorage(password);
const accountSeed = await storage.read_mnemonic(accountName);
setSeed(accountSeed);
} catch (e) {
setError('Could not retrieve seed phrase. Please check your password');
}
};
return (
<ConfirmationModal
open
onClose={onDone}
title={seed ? 'Account seed phrase' : 'Password'}
subtitle={seed ? '' : 'Enter your account password'}
ConfirmButton={
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
}
>
{seed ? (
<Seed seed={seed} />
) : (
<Box sx={{ mt: 2 }}>
<PasswordInput
label="Password"
error={error}
password={password}
onUpdatePassword={(pw: string) => {
setPassword(pw);
}}
/>
</Box>
)}
</ConfirmationModal>
);
};
@@ -1,3 +0,0 @@
export * from './Accounts';
export * from './Actions';
export * from './ViewSeedPhrase';
@@ -1,16 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
import { Stack } from '@mui/system';
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
import { useAppContext } from 'src/context';
export const Address = () => {
const { client } = useAppContext();
return (
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontWeight={700}>Address</Typography>
<ClientAddress withCopy address={client?.address || ''} />
</Stack>
);
};
@@ -1,23 +0,0 @@
import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { useAppContext } from 'src/context';
export const Balance = () => {
const { balance, fiatBalance, currency, getBalance } = useAppContext();
useEffect(() => {
getBalance();
}, []);
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
return (
<Stack alignItems="center" gap={1}>
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
<Typography variant="h4" textAlign="center">
{balance} NYM
</Typography>
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
</Stack>
);
};
@@ -1,6 +0,0 @@
export * from './accounts';
export * from './address';
export * from './balance';
export * from './receive';
export * from './send';
export * from './ui';
@@ -1,35 +0,0 @@
import React from 'react';
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { QRCodeSVG } from 'qrcode.react';
import { useAppContext } from 'src/context';
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
import { Close } from '@mui/icons-material';
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
const { client } = useAppContext();
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={700}>Receive</Typography>
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
<Close fontSize="small" />
</IconButton>
</Stack>
</DialogTitle>
<DialogContent>
<Stack gap={1} alignItems="center">
<Card elevation={3} sx={{ my: 2, width: 200 }}>
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<QRCodeSVG value={client?.address || ''} />
</CardContent>
</Card>
<Typography variant="body2" fontWeight={700}>
Your Nym address
</Typography>
<ClientAddress address={client?.address || ''} withCopy smallIcons />
</Stack>
</DialogContent>
</Dialog>
);
};
@@ -1 +0,0 @@
export * from './ReceiveModal';
@@ -1,30 +0,0 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal, Button } from 'src/components/ui';
export const SendConfirmationModal = ({
amount,
txUrl,
onConfirm,
}: {
amount: string;
txUrl: string;
onConfirm: () => void;
}) => (
<ConfirmationModal
open
fullWidth
title="You sent"
ConfirmButton={
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
Done
</Button>
}
>
<Box>
<Typography variant="h6">{amount}</Typography>
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
</Box>
</ConfirmationModal>
);
@@ -1 +0,0 @@
export * from './SendConfirmationModal';

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