Merge pull request #6481 from nymtech/release/2026.4-quark
Quark to master
This commit is contained in:
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--allow-branch ${{ github.ref_name }} \
|
||||
--no-git-commit \
|
||||
|
||||
# Dry run may show cascading dependency errors because packages aren't
|
||||
# actually uploaded - these are expected and ignored. We check for real
|
||||
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
|
||||
- name: Publish (dry run)
|
||||
run: |
|
||||
output=$(cargo workspaces publish --dry-run --allow-dirty 2>&1) || true
|
||||
echo "$output"
|
||||
|
||||
# Check for real errors (not cascading dependency errors)
|
||||
# Cascading errors mention "crates.io index", real errors mention "Cargo.toml"
|
||||
echo "$output" | grep -i "Cargo.toml" && exit 1 || true
|
||||
|
||||
# Show the list of packages published
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,59 @@
|
||||
# This is in case, for whatever reason, a publication run fails, and we need to restart halfway down the list, of unbumped/unpublished crates.
|
||||
name: Resume crates.io publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
CRATES=$(cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2)
|
||||
|
||||
if [ -z "$CRATES" ]; then
|
||||
echo "Error: No crates found after '${{ inputs.resume_after }}'"
|
||||
echo "Check the crate name matches exactly from 'cargo workspaces plan'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Will publish the following crates:"
|
||||
echo "$CRATES"
|
||||
echo ""
|
||||
|
||||
echo "$CRATES" | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting ${{ inputs.publish_interval }}s before next publish..."
|
||||
sleep ${{ inputs.publish_interval }}
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Publish crates to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes (600 for first publish, 60 after)"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
backup_author:
|
||||
description: "Second team member added as owner of the crate"
|
||||
required: false
|
||||
default: "jstuczyn"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
cargo workspaces publish \
|
||||
--publish-as-is \
|
||||
--publish-interval ${{ inputs.publish_interval }}
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
|
||||
- name: Add team as crate owners
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM="github:nymtech:core"
|
||||
echo "Checking and adding $TEAM as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM"; then
|
||||
echo " $TEAM already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM as owner of $crate..."
|
||||
cargo owner --add "$TEAM" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
|
||||
- name: Add secondary member as crate owner
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM_MEMBER="${{ inputs.backup_author }}"
|
||||
echo "Checking and adding $TEAM_MEMBER as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM_MEMBER"; then
|
||||
echo " $TEAM_MEMBER already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM_MEMBER as owner of $crate..."
|
||||
cargo owner --add "$TEAM_MEMBER" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Bump crate versions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to set (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
version-bump:
|
||||
runs-on: arc-linux-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--yes
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git add -A
|
||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
||||
git push
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,21 @@
|
||||
name: ci-docs-linkcheck
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "documentation/docs/**"
|
||||
- ".github/workflows/ci-docs-linkcheck.yml"
|
||||
- "lychee.toml"
|
||||
|
||||
jobs:
|
||||
linkcheck:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check links
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: ${{ github.workspace }}/documentation/docs/ --config ${{ github.workspace }}/lychee.toml --root-dir ${{ github.workspace }}/documentation/docs/pages/
|
||||
fail: true
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--no-git-tag \
|
||||
--no-git-push \
|
||||
--yes
|
||||
|
||||
# Note: Dry run may show cascading dependency errors because packages
|
||||
# aren't actually uploaded. Check if the missing dependency has an
|
||||
# "aborting upload due to dry run" message earlier in the output - if so,
|
||||
# it would succeed in a real publish since cargo-workspaces publishes in
|
||||
# dependency order. cargo-workspaces doesn't fail on err, so there isn't
|
||||
# a good way to check this at the moment.
|
||||
- name: Publish (dry run)
|
||||
run: cargo workspaces publish --from-git --dry-run --allow-dirty
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# - name: Configure git
|
||||
# run: |
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-push \
|
||||
--no-git-tag \
|
||||
--yes
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo workspaces publish --from-git --no-git-commit
|
||||
|
||||
# - name: Push version commit
|
||||
# run: |
|
||||
# git push origin HEAD
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.50.1
|
||||
uses: mikefarah/yq@v4.52.2
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Resume publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2 | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting 600s before next publish..."
|
||||
sleep 600
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -67,7 +67,6 @@ nym-api/redocly/formatted-openapi.json
|
||||
*.profraw
|
||||
.beads
|
||||
CLAUDE.md
|
||||
docs
|
||||
.claude
|
||||
.superego
|
||||
|
||||
|
||||
@@ -4,6 +4,82 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.4-quark] (2026-02-24)
|
||||
|
||||
- Enhance CI workflow with feature inputs ([#6462])
|
||||
- Chore/revert 6433 ([#6445])
|
||||
- Lp/stateless handshake ([#6437])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/client/internal-dev ([#6435])
|
||||
- build(deps-dev): bump webpack from 5.102.1 to 5.104.1 ([#6432])
|
||||
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/mix-fetch/internal-dev ([#6431])
|
||||
- build(deps-dev): bump webpack from 5.94.0 to 5.104.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6430])
|
||||
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/zknym-lib/internal-dev ([#6429])
|
||||
- build(deps-dev): bump webpack from 5.76.0 to 5.105.0 in /clients/native/examples/js-examples/websocket ([#6428])
|
||||
- HTTP & DNS Improvements ([#6423])
|
||||
- Endpoint for exit GW IPs ([#6418])
|
||||
- build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts ([#6416])
|
||||
- build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([#6415])
|
||||
- build(deps): bump bytes from 1.11.0 to 1.11.1 ([#6414])
|
||||
- build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 ([#6407])
|
||||
- build(deps-dev): bump eslint from 8.57.1 to 9.26.0 ([#6405])
|
||||
- Update reqwest to v0.13.1 ([#6401])
|
||||
- build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs ([#6387])
|
||||
- build(deps): bump next from 15.4.10 to 16.1.5 in /nym-node-status-api/nym-node-status-ui ([#6385])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 ([#6369])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 ([#6360])
|
||||
- build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/codegen/contract-clients ([#6359])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/packages/nodejs-client ([#6354])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs ([#6353])
|
||||
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /clients/native/examples/js-examples/websocket ([#6351])
|
||||
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 in /documentation/docs ([#6350])
|
||||
- build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs ([#6345])
|
||||
- Max/crates publishing tweaks ([#6343])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 ([#6339])
|
||||
- build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs ([#6332])
|
||||
- build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs ([#6325])
|
||||
- build(deps): bump rsa from 0.9.8 to 0.9.10 ([#6311])
|
||||
- build(deps): bump qs and express in /wasm/mix-fetch/internal-dev ([#6308])
|
||||
- build(deps): bump qs and express in /clients/native/examples/js-examples/websocket ([#6307])
|
||||
- feat: introduce on-disk cache persistance for major nym-api caches ([#6302])
|
||||
- Fix migrations in the Data Observatory ([#6271])
|
||||
|
||||
[#6462]: https://github.com/nymtech/nym/pull/6462
|
||||
[#6445]: https://github.com/nymtech/nym/pull/6445
|
||||
[#6437]: https://github.com/nymtech/nym/pull/6437
|
||||
[#6435]: https://github.com/nymtech/nym/pull/6435
|
||||
[#6432]: https://github.com/nymtech/nym/pull/6432
|
||||
[#6431]: https://github.com/nymtech/nym/pull/6431
|
||||
[#6430]: https://github.com/nymtech/nym/pull/6430
|
||||
[#6429]: https://github.com/nymtech/nym/pull/6429
|
||||
[#6428]: https://github.com/nymtech/nym/pull/6428
|
||||
[#6423]: https://github.com/nymtech/nym/pull/6423
|
||||
[#6418]: https://github.com/nymtech/nym/pull/6418
|
||||
[#6416]: https://github.com/nymtech/nym/pull/6416
|
||||
[#6415]: https://github.com/nymtech/nym/pull/6415
|
||||
[#6414]: https://github.com/nymtech/nym/pull/6414
|
||||
[#6407]: https://github.com/nymtech/nym/pull/6407
|
||||
[#6405]: https://github.com/nymtech/nym/pull/6405
|
||||
[#6401]: https://github.com/nymtech/nym/pull/6401
|
||||
[#6387]: https://github.com/nymtech/nym/pull/6387
|
||||
[#6385]: https://github.com/nymtech/nym/pull/6385
|
||||
[#6369]: https://github.com/nymtech/nym/pull/6369
|
||||
[#6360]: https://github.com/nymtech/nym/pull/6360
|
||||
[#6359]: https://github.com/nymtech/nym/pull/6359
|
||||
[#6354]: https://github.com/nymtech/nym/pull/6354
|
||||
[#6353]: https://github.com/nymtech/nym/pull/6353
|
||||
[#6351]: https://github.com/nymtech/nym/pull/6351
|
||||
[#6350]: https://github.com/nymtech/nym/pull/6350
|
||||
[#6345]: https://github.com/nymtech/nym/pull/6345
|
||||
[#6343]: https://github.com/nymtech/nym/pull/6343
|
||||
[#6339]: https://github.com/nymtech/nym/pull/6339
|
||||
[#6332]: https://github.com/nymtech/nym/pull/6332
|
||||
[#6325]: https://github.com/nymtech/nym/pull/6325
|
||||
[#6311]: https://github.com/nymtech/nym/pull/6311
|
||||
[#6308]: https://github.com/nymtech/nym/pull/6308
|
||||
[#6307]: https://github.com/nymtech/nym/pull/6307
|
||||
[#6302]: https://github.com/nymtech/nym/pull/6302
|
||||
[#6271]: https://github.com/nymtech/nym/pull/6271
|
||||
|
||||
## [2026.3-parmigiano] (2026-02-10)
|
||||
|
||||
- chore: disable LP on parmigiano branch ([#6422])
|
||||
|
||||
Generated
+338
-178
File diff suppressed because it is too large
Load Diff
+103
-102
@@ -205,7 +205,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.85"
|
||||
readme = "README.md"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -232,7 +232,7 @@ blake3 = "1.7.0"
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
bytes = "1.11.1"
|
||||
cargo_metadata = "0.19.2"
|
||||
celes = "2.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
@@ -320,12 +320,13 @@ publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
@@ -381,7 +382,7 @@ url = "2.5"
|
||||
utoipa = "5.2"
|
||||
utoipa-swagger-ui = "8.1"
|
||||
utoipauto = "0.2"
|
||||
uuid = "*"
|
||||
uuid = "1.19.0"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
@@ -391,106 +392,106 @@ zeroize = "1.7.0"
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.1", path = "common/nym-id" }
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.4", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
|
||||
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.1", path = "common/task" }
|
||||
nym-tun = { version = "1.20.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.1", path = "common/topology" }
|
||||
nym-types = { version = "1.20.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.4", path = "common/task" }
|
||||
nym-tun = { version = "1.20.4", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.4", path = "common/topology" }
|
||||
nym-types = { version = "1.20.4", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.20.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.71"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
+1004
-974
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack": "^5.105.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.70"
|
||||
version = "1.1.71"
|
||||
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"
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use opentelemetry;
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub use tracing_subscriber;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_tree;
|
||||
|
||||
@@ -76,7 +76,7 @@ features = ["json"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
|
||||
workspace = true
|
||||
features = ["json", "rustls-tls"]
|
||||
features = ["json", "rustls"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { workspace = true }
|
||||
nym-sphinx-types = { workspace = true, optional = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -51,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ description = "Functions to interact with zknym signers, checking their status a
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub use error::SignerCheckError;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
||||
use nym_validator_client::models::{
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
||||
@@ -18,6 +13,12 @@ use nym_validator_client::models::{
|
||||
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||
ContractVKShare, DealerDetails, Epoch,
|
||||
};
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub use error::SignerCheckError;
|
||||
|
||||
mod client_check;
|
||||
pub mod error;
|
||||
@@ -31,6 +32,7 @@ pub type TypedSignerResult = SignerResult<
|
||||
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
||||
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SignersTestResult {
|
||||
pub threshold: Option<u64>,
|
||||
pub results: Vec<TypedSignerResult>,
|
||||
|
||||
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
|
||||
async-trait = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
cfg-if = { workspace = true}
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -46,7 +46,10 @@ use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
sync::{
|
||||
Arc, LazyLock,
|
||||
atomic::{AtomicBool, Ordering::Relaxed},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -70,14 +73,23 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
|
||||
self = self.non_shared();
|
||||
// because of the call to non-shared this conditional should always run.
|
||||
if let Some(rb) = self.reqwest_client_builder {
|
||||
self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
|
||||
}
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
/// Override the DNS resolver implementation used by the underlying http client. If
|
||||
/// [`Self::dns_resolver`] is called directly that will take priority over this, there is no
|
||||
/// need to call both.
|
||||
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||
pub fn no_hickory_dns(mut self) -> Self {
|
||||
self = self.non_shared();
|
||||
self.use_secure_dns = false;
|
||||
self
|
||||
}
|
||||
@@ -129,7 +141,8 @@ pub struct HickoryDnsResolver {
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
use_system: Arc<AtomicBool>,
|
||||
system_resolver: Arc<OnceCell<TokioResolver>>,
|
||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
use_shared: bool,
|
||||
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
||||
@@ -141,7 +154,8 @@ impl Default for HickoryDnsResolver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
fallback: Default::default(),
|
||||
use_system: Arc::new(AtomicBool::new(false)),
|
||||
system_resolver: Default::default(),
|
||||
static_base: Some(Default::default()),
|
||||
use_shared: true,
|
||||
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
||||
@@ -151,16 +165,28 @@ impl Default for HickoryDnsResolver {
|
||||
|
||||
impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let resolver = self.state.clone();
|
||||
let maybe_fallback = self.fallback.clone();
|
||||
let maybe_static = self.static_base.clone();
|
||||
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let use_shared = self.use_shared;
|
||||
let resolver = if use_system {
|
||||
match self
|
||||
.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
|
||||
{
|
||||
Ok(r) => r.clone(),
|
||||
Err(e) => return Box::pin(return_err(e)),
|
||||
}
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
.clone()
|
||||
};
|
||||
|
||||
let maybe_static = self.static_base.clone();
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
resolve(
|
||||
name,
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
use_shared,
|
||||
overall_dns_timeout,
|
||||
@@ -171,16 +197,17 @@ impl Resolve for HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
name: Name,
|
||||
resolver: Arc<OnceCell<TokioResolver>>,
|
||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
resolver: TokioResolver,
|
||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
independent: bool,
|
||||
overall_dns_timeout: Duration,
|
||||
) -> Result<Addrs, ResolveError> {
|
||||
let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent));
|
||||
|
||||
// try checking the static table to see if any of the addresses in the table have been
|
||||
// looked up previously within the timeout to where we are not yet ready to try the
|
||||
// default resolver yet again.
|
||||
@@ -214,22 +241,6 @@ async fn resolve(
|
||||
}
|
||||
};
|
||||
|
||||
// If the primary resolver encountered an error, attempt a lookup using the fallback
|
||||
// resolver if one is configured.
|
||||
if let Some(ref fallback) = maybe_fallback {
|
||||
let resolver =
|
||||
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
|
||||
|
||||
let resolve_fut =
|
||||
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
|
||||
if let Ok(Ok(lookup)) = resolve_fut.await {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
|
||||
// If no record has been found and a static map of fallback addresses is configured
|
||||
// check the table for our entry
|
||||
if let Some(ref static_resolver) = maybe_static {
|
||||
@@ -258,6 +269,11 @@ impl Iterator for SocketAddrs {
|
||||
}
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Returns an instance of the shared resolver.
|
||||
pub fn shared() -> Self {
|
||||
SHARED_RESOLVER.clone()
|
||||
}
|
||||
|
||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||
pub async fn resolve_str(
|
||||
&self,
|
||||
@@ -265,10 +281,20 @@ impl HickoryDnsResolver {
|
||||
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
||||
let n =
|
||||
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
||||
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let resolver = if use_system {
|
||||
self.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
|
||||
.clone()
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
|
||||
.clone()
|
||||
};
|
||||
|
||||
resolve(
|
||||
n,
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
resolver,
|
||||
self.static_base.clone(),
|
||||
self.use_shared,
|
||||
self.overall_dns_timeout,
|
||||
@@ -298,13 +324,11 @@ impl HickoryDnsResolver {
|
||||
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
if !use_shared {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.system_resolver
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
@@ -320,45 +344,80 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
|
||||
self.fallback = Some(Default::default());
|
||||
let _ = self
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?;
|
||||
/// Swap the primary internal resolver to the system resolver rather than the
|
||||
/// configured custom resolver.
|
||||
pub fn use_system_resolver(&self) {
|
||||
self.use_system.store(true, Relaxed);
|
||||
|
||||
// IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.enable_system_fallback()?;
|
||||
// }
|
||||
Ok(())
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_system_resolver();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable fallback resolution. If the primary resolver fails the error is
|
||||
/// returned immediately
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
/// Swap the primary internal resolver to the configured custom resolver rather than the
|
||||
/// system resolver.
|
||||
pub fn use_configured_resolver(&self) {
|
||||
self.use_system.store(false, Relaxed);
|
||||
|
||||
// // IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.fallback = None;
|
||||
// }
|
||||
if self.use_shared {
|
||||
SHARED_RESOLVER.use_configured_resolver();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
|
||||
pub fn clear_preresolve(&self) {
|
||||
debug!("clearing pre-resolve table");
|
||||
if let Some(cell) = &self.static_base
|
||||
&& let Some(static_base) = cell.get()
|
||||
{
|
||||
static_base.clear_preresolve()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current map of hostnames to addresses used in the fallback static lookup stage if one
|
||||
/// exists.
|
||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_addrs())
|
||||
Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
|
||||
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
let cell = OnceCell::new();
|
||||
cell.set(StaticResolver::new(addrs))
|
||||
.expect("infallible assign");
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
debug!("setting fallback entries for {:?}", addrs.keys());
|
||||
if self.static_base.is_none() {
|
||||
let cell = OnceCell::new();
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
self.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||
.set_fallback(addrs);
|
||||
}
|
||||
|
||||
/// Get the current map of hostnames to addresses used in the preresolve static lookup stage
|
||||
/// if one exists.
|
||||
pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of addresses used in the preresolve static hostname lookup
|
||||
pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
debug!("setting pre-resolve entries for {:?}", addrs.keys());
|
||||
if self.static_base.is_none() {
|
||||
let cell = OnceCell::new();
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
self.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||
.set_preresolve(addrs);
|
||||
}
|
||||
|
||||
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
||||
@@ -495,7 +554,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
StaticResolver::new().with_fallback(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
@@ -532,10 +591,7 @@ mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
@@ -552,7 +608,7 @@ mod test {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver.into())
|
||||
.dns_resolver(resolver)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
@@ -597,7 +653,7 @@ mod test {
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
|
||||
|
||||
resolver.set_static_fallbacks(addr_map);
|
||||
resolver.set_fallback_addrs(addr_map);
|
||||
|
||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||
assert!(addrs.contains(&example_ip4));
|
||||
@@ -738,18 +794,19 @@ mod test {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the default
|
||||
// resolvers. Otherwise the default resolvers will succeed without using the static fallback,
|
||||
// making the test pointless
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
// interference with other tests.
|
||||
//
|
||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the
|
||||
// default resolvers. Otherwise the default resolvers will succeed without using the static
|
||||
// fallback, making the test pointless
|
||||
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
||||
let time_start = Instant::now();
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
let resolver1 = HickoryDnsResolver::shared();
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let time_start = std::time::Instant::now();
|
||||
// create a new resolver that uses the shared resolver
|
||||
let resolver = HickoryDnsResolver::shared();
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "rpc.nymtech.net";
|
||||
@@ -758,9 +815,27 @@ mod test {
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
println!(
|
||||
"{}ms resolved {domain}",
|
||||
(Instant::now() - time_start).as_millis()
|
||||
let lookup_dur = Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur > resolver.overall_dns_timeout,
|
||||
"expected lookup timeout - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
let time_start = std::time::Instant::now();
|
||||
// successful lookup using pre-resolve entry promoted from fallback
|
||||
let domain = "rpc.nymtech.net";
|
||||
let _ = resolver1
|
||||
.resolve_str(domain)
|
||||
.await
|
||||
.expect("domain expected to be in pre-resolve");
|
||||
|
||||
// this lookup should basically be instant as we are using pre-resolve
|
||||
let lookup_dur = std::time::Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur < Duration::from_millis(10),
|
||||
"expected instant - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
@@ -771,5 +846,62 @@ mod test {
|
||||
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
// interference with other tests.
|
||||
async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
|
||||
let resolver1 = HickoryDnsResolver::shared();
|
||||
|
||||
// create a new resolver that uses the shared resolver
|
||||
let mut resolver = HickoryDnsResolver::shared();
|
||||
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
resolver.set_static_preresolve(addr_map1);
|
||||
|
||||
let time_start = std::time::Instant::now();
|
||||
// successful lookup using pre-resolve entry promoted from fallback
|
||||
let _ = resolver1
|
||||
.resolve_str(&example_domains[0])
|
||||
.await
|
||||
.expect("domain expected to be in pre-resolve");
|
||||
|
||||
// this lookup should basically be instant as we are using pre-resolve
|
||||
let lookup_dur = std::time::Instant::now() - time_start;
|
||||
assert!(
|
||||
lookup_dur < Duration::from_millis(10),
|
||||
"expected instant - took {}ms",
|
||||
(lookup_dur).as_millis()
|
||||
);
|
||||
|
||||
// After clearing the pre-resolve in one instance of the shared resolver ...
|
||||
resolver.clear_preresolve();
|
||||
|
||||
// ... other instances have their pre-resolve entries cleared.
|
||||
let prereslve_lookup = resolver1
|
||||
.static_base
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get()
|
||||
.unwrap()
|
||||
.pre_resolve(&example_domains[0]);
|
||||
assert!(prereslve_lookup.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,12 @@ pub const NYM_RPC_IPS: &[IpAddr] = &[
|
||||
)),
|
||||
];
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn empty_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
|
||||
|
||||
@@ -14,42 +14,78 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StaticResolver {
|
||||
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
pre_resolve_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
enum PreResolveStatus {
|
||||
#[default]
|
||||
Valid,
|
||||
ValidUntil(Instant),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Entry {
|
||||
valid_for_pre_resolve_until: Option<Instant>,
|
||||
status: PreResolveStatus,
|
||||
addrs: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new(addrs: Vec<IpAddr>) -> Self {
|
||||
Self {
|
||||
valid_for_pre_resolve_until: None,
|
||||
status: PreResolveStatus::Valid,
|
||||
addrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_timeout(addrs: Vec<IpAddr>, timeout: Duration) -> Self {
|
||||
Self {
|
||||
status: PreResolveStatus::ValidUntil(Instant::now() + timeout),
|
||||
addrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
match self.status {
|
||||
PreResolveStatus::Valid => true,
|
||||
PreResolveStatus::ValidUntil(t) => t > Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticResolver {
|
||||
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
|
||||
debug!("building static resolver");
|
||||
let static_entries = static_entries
|
||||
.into_iter()
|
||||
.map(|(name, ips)| (name, Entry::new(ips)))
|
||||
.collect();
|
||||
pub fn new() -> StaticResolver {
|
||||
Self {
|
||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
||||
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the full set of domain names and associated addresses stored in this static lookup table
|
||||
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
/// Initialize the contents of the pre-resolve table for this instance of the static resolver
|
||||
#[allow(unused)]
|
||||
pub fn with_preresolve(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|(name, ips)| (name, Entry::new(ips)))
|
||||
.collect();
|
||||
self.preresolve_addr_map = Arc::new(Mutex::new(entries));
|
||||
self
|
||||
}
|
||||
|
||||
/// Initialize the contenes of the fallback table for this instance of the static resolver
|
||||
pub fn with_fallback(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||
self.fallback_addr_map = Arc::new(Mutex::new(entries));
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the set of domain names and associated addresses stored in the pre-resolve static
|
||||
/// lookup table
|
||||
pub fn get_preresolve_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
let mut out = HashMap::new();
|
||||
self.static_addr_map
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
@@ -59,6 +95,38 @@ impl StaticResolver {
|
||||
out
|
||||
}
|
||||
|
||||
/// Return the set of domain names and associated addresses stored in the fallback static lookup
|
||||
/// table
|
||||
pub fn get_fallback_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
self.fallback_addr_map.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of static addresses to be returned only after attempting a lookup
|
||||
/// over the network resolver.
|
||||
pub fn set_fallback(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
self.fallback_addr_map.lock().unwrap().extend(addrs);
|
||||
}
|
||||
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::preresolve_to_addrs`]) will be removed.
|
||||
pub fn clear_preresolve(&self) {
|
||||
*self.preresolve_addr_map.lock().unwrap() = HashMap::new();
|
||||
}
|
||||
|
||||
/// Set (or overwrite) the map of static addresses and mark these domains to be returned
|
||||
/// WITHOUT attempting a lookup over the network resolver.
|
||||
pub fn set_preresolve(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||
let mut current_map = self.preresolve_addr_map.lock().unwrap();
|
||||
for (domain, ips) in addrs.into_iter() {
|
||||
_ = current_map.insert(domain, Entry::new(ips))
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the timeout for which domains can be pre-resolved after they are looked up in the
|
||||
/// static lookup table.
|
||||
#[allow(unused)]
|
||||
@@ -71,44 +139,58 @@ impl StaticResolver {
|
||||
/// recently (within the configured timeout) looked it up previously in this static table using
|
||||
/// a regular resolve.
|
||||
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||
debug!("found {name:?} in pre-resolve static table resolver");
|
||||
|
||||
self.pre_resolve_timeout?;
|
||||
|
||||
self.static_addr_map
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.filter(|e| {
|
||||
e.valid_for_pre_resolve_until
|
||||
.is_some_and(|t| t > Instant::now())
|
||||
.filter(|entry| entry.is_valid())
|
||||
.map(|entry| {
|
||||
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
|
||||
entry.addrs.clone()
|
||||
})
|
||||
.map(|e| e.addrs.clone())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||
Self::resolve_inner(
|
||||
self.static_addr_map.lock().unwrap(),
|
||||
self.fallback_addr_map.lock().unwrap(),
|
||||
self.preresolve_addr_map.lock().unwrap(),
|
||||
name,
|
||||
self.pre_resolve_timeout,
|
||||
)
|
||||
.map(|e| e.addrs)
|
||||
}
|
||||
|
||||
fn resolve_inner(
|
||||
mut table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
|
||||
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||
name: &str,
|
||||
timeout: Option<Duration>,
|
||||
) -> Option<Entry> {
|
||||
let resolved = table.get_mut(name)?;
|
||||
pre_resolve_cache_timeout: Option<Duration>,
|
||||
) -> Option<Vec<IpAddr>> {
|
||||
let resolved = fallback_table.get(name)?;
|
||||
|
||||
debug!("found {name:?} in static table resolver");
|
||||
debug!("lookup hit for \"{name:?}\" in static table resolver");
|
||||
|
||||
if let Some(pre_resolve_timeout) = timeout {
|
||||
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
||||
// trigger in pre-resolve lookups for the next _timeout_ window.
|
||||
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout);
|
||||
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
||||
// trigger in pre-resolve lookups for the next _timeout_ window if it wasn't already
|
||||
// triggering.
|
||||
if let Some(pre_resolve_timeout) = pre_resolve_cache_timeout {
|
||||
match preresolve_table.get_mut(name) {
|
||||
None => {
|
||||
_ = preresolve_table.insert(
|
||||
name.to_string(),
|
||||
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||
);
|
||||
}
|
||||
// Not sure how we would get cases where this is Some( ) -- it requires having a
|
||||
// Valid entry in the preresolve table and still doing a lookup against fallback.
|
||||
Some(entry) if matches!(entry.status, PreResolveStatus::ValidUntil(_)) => {
|
||||
_ = preresolve_table.insert(
|
||||
name.to_string(),
|
||||
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(resolved.clone())
|
||||
}
|
||||
@@ -117,13 +199,23 @@ impl StaticResolver {
|
||||
impl Resolve for StaticResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
debug!("looking up {name:?} in static resolver");
|
||||
let addr_map = self.static_addr_map.clone();
|
||||
// these should clone arcs, not the actual tables
|
||||
let fallback_addr_map = self.fallback_addr_map.clone();
|
||||
let presesolve_addr_map = self.preresolve_addr_map.clone();
|
||||
let timeout = self.pre_resolve_timeout;
|
||||
// Also the returned future doesn't try to take the lock on the tables until the
|
||||
// future is awaited, so no blocking issues.
|
||||
Box::pin(async move {
|
||||
let addr_map = addr_map.lock().unwrap();
|
||||
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
|
||||
let fallback_addr_map = fallback_addr_map.lock().unwrap();
|
||||
let presesolve_addr_map = presesolve_addr_map.lock().unwrap();
|
||||
let lookup = match Self::resolve_inner(
|
||||
fallback_addr_map,
|
||||
presesolve_addr_map,
|
||||
name.as_str(),
|
||||
timeout,
|
||||
) {
|
||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||
Some(entry) => entry.addrs,
|
||||
Some(addrs) => addrs,
|
||||
};
|
||||
let addrs: Addrs = Box::new(
|
||||
lookup
|
||||
@@ -142,6 +234,7 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -149,7 +242,7 @@ mod test {
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
|
||||
// lookup for domain for which there is no entry
|
||||
let resolver = StaticResolver::new(HashMap::new());
|
||||
let resolver = StaticResolver::new();
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let result = resolver.resolve(url).await;
|
||||
@@ -166,7 +259,7 @@ mod test {
|
||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let resolver = StaticResolver::new(addr_map);
|
||||
let resolver = StaticResolver::new().with_fallback(addr_map);
|
||||
let mut addrs = resolver.resolve(url).await?;
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
||||
@@ -175,7 +268,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_lookup_pre_resolve() {
|
||||
fn elevate_fallback_to_pre_resolve() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
let mut addr_map = HashMap::new();
|
||||
@@ -183,24 +276,23 @@ mod test {
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||
|
||||
let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration);
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// ensure that attempting to pre-resolve without first resolving returns none
|
||||
let result = resolver.pre_resolve(&example_domain);
|
||||
assert!(result.is_none());
|
||||
|
||||
// resolving should now update the pre-resolve validity timeout for the entry
|
||||
let entry = StaticResolver::resolve_inner(
|
||||
resolver.static_addr_map.lock().unwrap(),
|
||||
&example_domain,
|
||||
Some(example_duration),
|
||||
)
|
||||
.expect("missing entry???!!!!");
|
||||
assert!(
|
||||
entry
|
||||
.valid_for_pre_resolve_until
|
||||
.is_some_and(|t| t < Instant::now() + example_duration)
|
||||
);
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domain)
|
||||
.expect("entry should exist");
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domain),
|
||||
Some(PreResolveStatus::ValidUntil(t))
|
||||
if t < Instant::now() + example_duration
|
||||
));
|
||||
|
||||
// check that pre-resolve now returns the expected record
|
||||
let addrs = resolver
|
||||
@@ -214,4 +306,139 @@ mod test {
|
||||
let result = resolver.pre_resolve(&example_domain);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_use_preresolve() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
String::from("preresolve.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
let mut addr_map2 = HashMap::new();
|
||||
addr_map2.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
addr_map2.insert(
|
||||
example_domains[2].clone(),
|
||||
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||
);
|
||||
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map1)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// Attempting to pre-resolve without setting the table returns none
|
||||
let result = resolver.pre_resolve(&example_domains[0]);
|
||||
assert!(result.is_none());
|
||||
|
||||
resolver.set_preresolve(addr_map2);
|
||||
|
||||
// After setting the pre-resolve, addresses in the the table are returned
|
||||
let result = resolver.pre_resolve(&example_domains[1]);
|
||||
assert!(result.is_some());
|
||||
|
||||
// If the domain wasn't in the pre-resolve table it returns none.
|
||||
let result = resolver.pre_resolve(&example_domains[0]);
|
||||
assert!(result.is_none());
|
||||
|
||||
resolver.clear_preresolve();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preresolve_with_fallback() {
|
||||
let example_duration = Duration::from_secs(3);
|
||||
let example_domains = [
|
||||
String::from("static1.nymvpn.com"),
|
||||
String::from("static2.nymvpn.com"),
|
||||
String::from("preresolve.nymvpn.com"),
|
||||
];
|
||||
let mut addr_map1 = HashMap::new();
|
||||
addr_map1.insert(
|
||||
example_domains[0].clone(),
|
||||
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||
);
|
||||
addr_map1.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
|
||||
let mut addr_map2 = HashMap::new();
|
||||
addr_map2.insert(
|
||||
example_domains[1].clone(),
|
||||
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||
);
|
||||
addr_map2.insert(
|
||||
example_domains[2].clone(),
|
||||
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||
);
|
||||
|
||||
let resolver = StaticResolver::new()
|
||||
.with_fallback(addr_map1)
|
||||
.with_preresolve(addr_map2)
|
||||
.with_pre_resolve_timeout(example_duration);
|
||||
|
||||
// when using both pre-resolve and fallback elevating entries from fallback to pre-resolve
|
||||
// leaves the entries as `Valid`.
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[1]),
|
||||
Some(PreResolveStatus::Valid)
|
||||
));
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domains[1])
|
||||
.expect("entry should exist");
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[1]),
|
||||
Some(PreResolveStatus::Valid)
|
||||
));
|
||||
|
||||
// entries not already in pre-resolve get elevated with a timeout.
|
||||
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||
let _addrs = resolver
|
||||
.resolve_str(&example_domains[0])
|
||||
.expect("entry should exist");
|
||||
assert!(resolver.preresolve_contains(&example_domains[0]));
|
||||
assert!(matches!(
|
||||
resolver.preresolve_status(&example_domains[0]),
|
||||
Some(PreResolveStatus::ValidUntil(_))
|
||||
));
|
||||
|
||||
// clearing the pre-resolve table doesn't impact the fallback table.
|
||||
resolver.clear_preresolve();
|
||||
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||
assert!(!resolver.preresolve_contains(&example_domains[1]));
|
||||
assert!(!resolver.preresolve_contains(&example_domains[2]));
|
||||
assert!(!resolver.fallback_contains(&example_domains[0]));
|
||||
assert!(!resolver.fallback_contains(&example_domains[1]));
|
||||
}
|
||||
|
||||
/// convenience functions for testing
|
||||
impl StaticResolver {
|
||||
fn preresolve_status(&self, name: &str) -> Option<PreResolveStatus> {
|
||||
self.preresolve_addr_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.map(|e| e.status.clone())
|
||||
}
|
||||
|
||||
fn preresolve_contains(&self, name: &str) -> bool {
|
||||
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||
}
|
||||
|
||||
fn fallback_contains(&self, name: &str) -> bool {
|
||||
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
|
||||
//! Utilities for and implementation of request tunneling
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{
|
||||
Arc, LazyLock, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ClientBuilder;
|
||||
use crate::{Client, ClientBuilder};
|
||||
|
||||
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
|
||||
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
|
||||
|
||||
// #[cfg(feature = "tunneling")]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Front {
|
||||
pub(crate) policy: FrontPolicy,
|
||||
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
|
||||
enabled: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -19,7 +25,7 @@ impl Clone for Front {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
policy: self.policy.clone(),
|
||||
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
|
||||
enabled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,13 +33,30 @@ impl Clone for Front {
|
||||
impl Front {
|
||||
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
||||
Self {
|
||||
enabled: AtomicBool::new(policy == FrontPolicy::Always),
|
||||
enabled: AtomicBool::new(false),
|
||||
policy: Arc::new(RwLock::new(policy)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn off() -> Self {
|
||||
Self::new(FrontPolicy::Off)
|
||||
}
|
||||
|
||||
pub(crate) fn shared() -> Self {
|
||||
let policy = SHARED_FRONTING_POLICY.clone();
|
||||
Self {
|
||||
enabled: AtomicBool::new(false),
|
||||
policy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_policy(&self, policy: FrontPolicy) {
|
||||
*self.policy.write().unwrap() = policy;
|
||||
self.enabled.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool {
|
||||
match self.policy {
|
||||
match *self.policy.read().unwrap() {
|
||||
FrontPolicy::Off => false,
|
||||
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
||||
FrontPolicy::Always => true,
|
||||
@@ -46,14 +69,13 @@ impl Front {
|
||||
if self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if matches!(self.policy, FrontPolicy::OnRetry) {
|
||||
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
|
||||
self.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Clone)]
|
||||
#[cfg(feature = "tunneling")]
|
||||
/// Policy for when to use domain fronting for HTTP requests.
|
||||
pub enum FrontPolicy {
|
||||
/// Always use domain fronting for all requests.
|
||||
@@ -66,29 +88,208 @@ pub enum FrontPolicy {
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Enable and configure request tunneling for API requests.
|
||||
#[cfg(feature = "tunneling")]
|
||||
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
|
||||
let front = Front::new(policy);
|
||||
/// Enable and configure request tunneling for API requests. If no front policy is
|
||||
/// provided the shared fronting policy will be used.
|
||||
pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
|
||||
let front = if let Some(p) = policy {
|
||||
Front::new(p)
|
||||
} else {
|
||||
Front::shared()
|
||||
};
|
||||
|
||||
// Check if any of the supplied urls even support fronting
|
||||
if !self.urls.iter().any(|url| url.has_front()) {
|
||||
warn!(
|
||||
"fronting is enabled, but none of the supplied urls have configured fronting domains"
|
||||
"fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
|
||||
self.urls
|
||||
);
|
||||
}
|
||||
|
||||
self.front = Some(front);
|
||||
self.front = front;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Set the policy for enabling fronting. If fronting was previously unset this will set it, and
|
||||
/// make it possible to enable (i.e [`FrontPolicy::Off`] will not enable it).
|
||||
///
|
||||
/// Calling this function sets a custom policy for this client, disconnecting it from the shared
|
||||
/// fronting policy -- i.e. changes applied through [`Client::set_shared_front_policy`] will not
|
||||
/// be impact this client.
|
||||
pub fn set_front_policy(&mut self, policy: FrontPolicy) {
|
||||
self.front.set_policy(policy)
|
||||
}
|
||||
|
||||
/// Set the fronting policy for this client to follow the shared policy.
|
||||
pub fn use_shared_front_policy(&mut self) {
|
||||
self.front = Front::shared();
|
||||
}
|
||||
|
||||
/// Set the fronting policy for all clients using the shared policy.
|
||||
//
|
||||
// NOTE: this does not reset the per-instance enabled flag like it will when using
|
||||
// [`Front::set_front_policy`]. So if a client is using shared policy with the `OnRetry` policy
|
||||
// and this function is used to swap that policy away from and then back to `OnRetry` the
|
||||
// fronting will still be enabled. Noting this here just in case this triggers any corner cases
|
||||
// down the road.
|
||||
pub fn set_shared_front_policy(policy: FrontPolicy) {
|
||||
*SHARED_FRONTING_POLICY.write().unwrap() = policy;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ApiClientCore, NO_PARAMS, Url};
|
||||
|
||||
impl Front {
|
||||
pub(crate) fn policy(&self) -> FrontPolicy {
|
||||
self.policy.read().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy can be set for an independent client and the update is applied properly
|
||||
#[test]
|
||||
fn set_policy_independent_client() {
|
||||
let url1 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut client1 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::Off))
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||
|
||||
let client2 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::OnRetry))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Ensure that setting the policy for a client it gets properly applied.
|
||||
client1.set_front_policy(FrontPolicy::Always);
|
||||
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// ensure that setting the policy in a client NOT using the shared policy does NOT update
|
||||
// the policy used by another client.
|
||||
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||
|
||||
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||
// requests
|
||||
let req = client1
|
||||
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let expected_host = url1.host_str().unwrap();
|
||||
assert!(
|
||||
req.headers()
|
||||
.get(reqwest::header::HOST)
|
||||
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||
"{:?} != {:?}",
|
||||
expected_host,
|
||||
req,
|
||||
);
|
||||
|
||||
let expected_front = url1.front_str().unwrap();
|
||||
assert!(
|
||||
req.url()
|
||||
.host()
|
||||
.is_some_and(|url| url.to_string() == expected_front),
|
||||
"{:?} != {:?}",
|
||||
expected_front,
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
/// Policy can be set for the shared client and the update is applied properly
|
||||
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH THE SHARED POLICY AND AS SUCH CAN HAVE
|
||||
// AN IMPACT ON OTHER TESTS
|
||||
#[test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
fn set_policy_shared_client() {
|
||||
let url1 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Client::set_shared_front_policy(FrontPolicy::Off);
|
||||
assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
|
||||
|
||||
let client1 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(None)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||
|
||||
let mut client2 = ClientBuilder::new(url1.clone())
|
||||
.unwrap()
|
||||
.with_fronting(Some(FrontPolicy::Off))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Ensure that setting the shared policy gets properly applied
|
||||
Client::set_shared_front_policy(FrontPolicy::Always);
|
||||
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// Setting the shared policy should NOT update clients NOT using the shared policy.
|
||||
assert!(client2.front.policy() == FrontPolicy::Off);
|
||||
|
||||
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||
// requests
|
||||
let req = client1
|
||||
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let expected_host = url1.host_str().unwrap();
|
||||
assert!(
|
||||
req.headers()
|
||||
.get(reqwest::header::HOST)
|
||||
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||
"{:?} != {:?}",
|
||||
expected_host,
|
||||
req,
|
||||
);
|
||||
|
||||
let expected_front = url1.front_str().unwrap();
|
||||
assert!(
|
||||
req.url()
|
||||
.host()
|
||||
.is_some_and(|url| url.to_string() == expected_front),
|
||||
"{:?} != {:?}",
|
||||
expected_front,
|
||||
req,
|
||||
);
|
||||
|
||||
// ensure that setting to the shared policy works
|
||||
client2.use_shared_front_policy();
|
||||
assert!(client2.front.policy() == FrontPolicy::Always);
|
||||
|
||||
// ensure that if the policy is OnRetry then the `enabled` fields are still independent,
|
||||
// despite the policy being shared.
|
||||
Client::set_shared_front_policy(FrontPolicy::OnRetry);
|
||||
assert!(client1.front.policy() == FrontPolicy::OnRetry);
|
||||
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||
|
||||
assert!(!client1.front.is_enabled());
|
||||
assert!(!client2.front.is_enabled());
|
||||
|
||||
client1.front.retry_enable();
|
||||
assert!(client1.front.is_enabled());
|
||||
assert!(!client2.front.is_enabled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nym_api_works() {
|
||||
let url1 = Url::new(
|
||||
@@ -104,7 +305,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new(url1)
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
@@ -140,7 +341,7 @@ mod tests {
|
||||
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use http::header::USER_AGENT;
|
||||
pub use inventory;
|
||||
pub use reqwest;
|
||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||
@@ -147,6 +148,7 @@ pub mod registry;
|
||||
use crate::path::RequestPath;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use cfg_if::cfg_if;
|
||||
use http::HeaderMap;
|
||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||
use itertools::Itertools;
|
||||
@@ -161,9 +163,7 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
mod fronted;
|
||||
@@ -195,6 +195,8 @@ use nym_http_api_client_macro::client_defaults;
|
||||
/// high and chatty protocols take a while to complete.
|
||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
client_defaults!(
|
||||
priority = -100;
|
||||
@@ -206,6 +208,24 @@ client_defaults!(
|
||||
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
|
||||
);
|
||||
|
||||
static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
tracing::info!("Initializing shared HTTP client");
|
||||
cfg_if! {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
reqwest::ClientBuilder::new().build()
|
||||
.expect("failed to initialize shared http client")
|
||||
} else {
|
||||
let mut builder = default_builder();
|
||||
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
|
||||
builder
|
||||
.build()
|
||||
.expect("failed to initialize shared http client")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// Collection of URL Path Segments
|
||||
pub type PathSegments<'a> = &'a [&'a str];
|
||||
/// Collection of HTTP Request Parameters
|
||||
@@ -327,6 +347,12 @@ pub enum HttpClientError {
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse header value: {source}")]
|
||||
InvalidHeaderValue {
|
||||
#[source]
|
||||
source: http::Error,
|
||||
},
|
||||
|
||||
#[error("failed to send request for {url}: {source}")]
|
||||
RequestSendFailure {
|
||||
url: Box<reqwest::Url>,
|
||||
@@ -554,6 +580,19 @@ pub trait ApiClientCore {
|
||||
let req = self.create_request(method, path, params, json_body)?;
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
|
||||
///
|
||||
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
|
||||
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
|
||||
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
|
||||
/// multiple times.
|
||||
fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
|
||||
|
||||
/// If the fronting policy for the client is set to `OnRetry` this function will enable the
|
||||
/// fronting if not already enabled.
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
|
||||
}
|
||||
|
||||
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
|
||||
@@ -562,16 +601,18 @@ pub struct ClientBuilder {
|
||||
urls: Vec<Url>,
|
||||
|
||||
timeout: Option<Duration>,
|
||||
custom_user_agent: bool,
|
||||
reqwest_client_builder: reqwest::ClientBuilder,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
reqwest_client_builder: Option<reqwest::ClientBuilder>,
|
||||
#[allow(dead_code)] // not dead code, just unused in wasm
|
||||
use_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
front: fronted::Front,
|
||||
|
||||
retry_limit: usize,
|
||||
serialization: SerializationFormat,
|
||||
|
||||
error: Option<HttpClientError>,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
@@ -642,10 +683,10 @@ impl ClientBuilder {
|
||||
|
||||
let mut builder = Self::new_with_urls(urls)?;
|
||||
|
||||
// Enable domain fronting by default (on retry)
|
||||
// Enable domain fronting using the shared fronting policy
|
||||
#[cfg(feature = "tunneling")]
|
||||
{
|
||||
builder = builder.with_fronting(FrontPolicy::OnRetry);
|
||||
builder = builder.with_fronting(None);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
@@ -659,26 +700,31 @@ impl ClientBuilder {
|
||||
|
||||
let urls = Self::check_urls(urls);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reqwest_client_builder = reqwest::ClientBuilder::new();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let reqwest_client_builder = default_builder();
|
||||
|
||||
Ok(ClientBuilder {
|
||||
urls,
|
||||
timeout: None,
|
||||
custom_user_agent: false,
|
||||
reqwest_client_builder,
|
||||
custom_user_agent: None,
|
||||
reqwest_client_builder: None,
|
||||
use_secure_dns: true,
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: None,
|
||||
front: fronted::Front::off(),
|
||||
|
||||
retry_limit: 0,
|
||||
serialization: SerializationFormat::Json,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure use of an independent HTTP request executor. This prevents use of beneficial
|
||||
/// features like connection pooling under the hood.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn non_shared(mut self) -> Self {
|
||||
if self.reqwest_client_builder.is_none() {
|
||||
self.reqwest_client_builder = Some(default_builder());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an additional URL to the set usable by this constructed `Client`
|
||||
pub fn add_url(mut self, url: Url) -> Self {
|
||||
self.urls.push(url);
|
||||
@@ -723,7 +769,7 @@ impl ClientBuilder {
|
||||
|
||||
/// Provide a pre-configured [`reqwest::ClientBuilder`]
|
||||
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
|
||||
self.reqwest_client_builder = reqwest_builder;
|
||||
self.reqwest_client_builder = Some(reqwest_builder);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -733,18 +779,12 @@ impl ClientBuilder {
|
||||
V: TryInto<HeaderValue>,
|
||||
V::Error: Into<http::Error>,
|
||||
{
|
||||
self.custom_user_agent = true;
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Override DNS resolution for specific domains to particular IP addresses.
|
||||
///
|
||||
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
|
||||
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
|
||||
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
|
||||
match value.try_into() {
|
||||
Ok(v) => self.custom_user_agent = Some(v),
|
||||
Err(err) => {
|
||||
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -761,30 +801,33 @@ impl ClientBuilder {
|
||||
|
||||
/// Returns a Client that uses this ClientBuilder configuration.
|
||||
pub fn build(self) -> Result<Client, HttpClientError> {
|
||||
if let Some(err) = self.error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reqwest_client = self.reqwest_client_builder.build()?;
|
||||
let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
|
||||
|
||||
// TODO: we should probably be propagating the error rather than panicking,
|
||||
// but that'd break bunch of things due to type changes
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let reqwest_client = {
|
||||
let mut builder = self.reqwest_client_builder;
|
||||
let reqwest_client = self
|
||||
.reqwest_client_builder
|
||||
.map(|mut builder| {
|
||||
// unless explicitly disabled use the DoT/DoH enabled resolver
|
||||
if self.use_secure_dns {
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
}
|
||||
|
||||
// unless explicitly disabled use the DoT/DoH enabled resolver
|
||||
if self.use_secure_dns {
|
||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)?
|
||||
};
|
||||
builder
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let client = Client {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
using_secure_dns: self.use_secure_dns,
|
||||
custom_user_agent: self.custom_user_agent,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
@@ -804,11 +847,11 @@ impl ClientBuilder {
|
||||
pub struct Client {
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: reqwest::Client,
|
||||
using_secure_dns: bool,
|
||||
reqwest_client: Option<reqwest::Client>,
|
||||
custom_user_agent: Option<HeaderValue>,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
front: fronted::Front,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: Duration,
|
||||
@@ -862,8 +905,8 @@ impl Client {
|
||||
Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
using_secure_dns: self.using_secure_dns,
|
||||
reqwest_client: None,
|
||||
custom_user_agent: None,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front.clone(),
|
||||
@@ -897,9 +940,7 @@ impl Client {
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn matches_current_host(&self, url: &Url) -> bool {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
url.host_str() == self.current_url().front_str()
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
@@ -926,9 +967,7 @@ impl Client {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
// if we are using fronting, try updating to the next front
|
||||
let url = self.current_url();
|
||||
|
||||
@@ -948,9 +987,7 @@ impl Client {
|
||||
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
while next != orig {
|
||||
if self.base_urls[next].has_front() {
|
||||
// we have a front for the next host, so we can use it
|
||||
@@ -981,14 +1018,12 @@ impl Client {
|
||||
/// this method. For example, if the client is configured to rotate hosts after each error, this
|
||||
/// method should be called after the host has been updated -- i.e. as part of the subsequent
|
||||
/// send.
|
||||
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||
let url = self.current_url();
|
||||
r.url_mut().set_host(url.host_str()).unwrap();
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
if self.front.is_enabled() {
|
||||
if let Some(front_host) = url.front_str() {
|
||||
if let Some(actual_host) = url.host_str() {
|
||||
tracing::debug!(
|
||||
@@ -1008,6 +1043,13 @@ impl Client {
|
||||
.headers_mut()
|
||||
.insert(reqwest::header::HOST, actual_host_header);
|
||||
|
||||
// Set a custom header to capture the outer host (used in the SNI) of the request
|
||||
let front_host_header: HeaderValue =
|
||||
front_host.parse().unwrap_or(HeaderValue::from_static(""));
|
||||
_ = r
|
||||
.headers_mut()
|
||||
.insert(NYM_OUTER_SNI_HEADER, front_host_header);
|
||||
|
||||
return (url.as_str(), url.front_str());
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -1048,12 +1090,21 @@ impl ApiClientCore for Client {
|
||||
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
|
||||
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
|
||||
let client = if let Some(client) = &self.reqwest_client {
|
||||
client.clone()
|
||||
} else {
|
||||
SHARED_CLIENT.clone()
|
||||
};
|
||||
let mut rb = RequestBuilder::from_parts(client, req);
|
||||
|
||||
rb = rb
|
||||
.header(ACCEPT, self.serialization.content_type())
|
||||
.header(CONTENT_TYPE, self.serialization.content_type());
|
||||
|
||||
if let Some(user_agent) = &self.custom_user_agent {
|
||||
rb = rb.header(USER_AGENT, user_agent.clone());
|
||||
}
|
||||
|
||||
if let Some(body) = body {
|
||||
match self.serialization {
|
||||
SerializationFormat::Json => {
|
||||
@@ -1096,16 +1147,19 @@ impl ApiClientCore for Client {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
Ok(wasmtimer::tokio::timeout(
|
||||
self.request_timeout,
|
||||
self.reqwest_client.execute(req),
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
Ok(
|
||||
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
|
||||
)
|
||||
.await
|
||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let response = self.reqwest_client.execute(req).await;
|
||||
let response = {
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
client.execute(req).await
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
@@ -1121,20 +1175,10 @@ impl ApiClientCore for Client {
|
||||
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host(Some(url.clone()));
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
}
|
||||
}
|
||||
self.maybe_enable_fronting(("network", url.as_str(), &err));
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
@@ -1158,6 +1202,21 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_rotate_hosts(&self, offending: Option<Url>) {
|
||||
self.update_host(offending);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
|
||||
// If fronting is set to be OnRetry, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = self.front.is_enabled();
|
||||
self.front.retry_enable();
|
||||
if !was_enabled && self.front.is_enabled() {
|
||||
tracing::debug!("Domain fronting activated after failure: {context:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common usage functionality for the http client.
|
||||
@@ -1310,6 +1369,35 @@ pub trait ApiClient: ApiClientCore {
|
||||
self.get_response(path, params).await
|
||||
}
|
||||
|
||||
/// Attempt to parse a response object from an HTTP response
|
||||
async fn parse_response<T>(
|
||||
&self,
|
||||
res: Response,
|
||||
allow_empty: bool,
|
||||
) -> Result<T, HttpClientError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let url = Url::from(res.url());
|
||||
parse_response(res, allow_empty).await.inspect_err(|e| {
|
||||
if matches!(
|
||||
// if we encounter a read error while we attempt to parse it could be caused by censorship and we should
|
||||
// rotate hosts / enable fronting.
|
||||
e,
|
||||
HttpClientError::ResponseReadFailure {
|
||||
url: _,
|
||||
headers: _,
|
||||
status: _,
|
||||
source: _,
|
||||
}
|
||||
) {
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
#[cfg(feature = "tunneling")]
|
||||
self.maybe_enable_fronting(("parse/read", url.as_str(), e));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
|
||||
/// into the provided type `T` based on the content type header
|
||||
@@ -1327,7 +1415,8 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::GET, path, params, None::<&()>)
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1349,7 +1438,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::POST, path, params, Some(json_body))
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
|
||||
@@ -1369,7 +1458,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||
@@ -1391,7 +1480,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let res = self
|
||||
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
|
||||
.await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1403,7 +1492,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1420,7 +1509,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `delete` json data from the provided absolute endpoint, e.g.
|
||||
@@ -1432,7 +1521,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
{
|
||||
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
|
||||
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||
@@ -1450,7 +1539,7 @@ pub trait ApiClient: ApiClientCore {
|
||||
let req =
|
||||
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
|
||||
let res = self.send(req).await?;
|
||||
parse_response(res, false).await
|
||||
self.parse_response(res, false).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ fn sanitizing_urls() {
|
||||
// - on error without retries is where we have multiple urls, is the url updated?
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // test relies on external services being available and behaving in a specific way.
|
||||
#[cfg(any())] // #[ignore] we run ignore assuming it just means slow in Ci/CD -_-
|
||||
// test relies on external services being available and behaving in a specific way.
|
||||
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = ClientBuilder::new_with_urls(vec![
|
||||
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
|
||||
@@ -199,7 +200,7 @@ fn fronted_host_updating() {
|
||||
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
|
||||
let mut client = ClientBuilder::new(url)
|
||||
.unwrap()
|
||||
.with_fronting(crate::fronted::FrontPolicy::Always)
|
||||
.with_fronting(Some(crate::fronted::FrontPolicy::Always))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -123,6 +123,16 @@ impl From<reqwest::Url> for Url {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&reqwest::Url> for Url {
|
||||
fn from(url: &url::Url) -> Self {
|
||||
Self {
|
||||
url: url.clone(),
|
||||
fronts: None,
|
||||
current_front: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<url::Url> for Url {
|
||||
fn as_ref(&self) -> &url::Url {
|
||||
&self.url
|
||||
|
||||
@@ -6,6 +6,9 @@ edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
# of env files and requires workspace context
|
||||
exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
|
||||
@@ -21,7 +21,8 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
rand = "0.9.2"
|
||||
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
|
||||
rand09 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ use nym_kkt::{
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rand09::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);
|
||||
rand09::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
@@ -33,13 +33,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
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()
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
|
||||
use blake3::Hasher;
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
@@ -182,8 +182,7 @@ mod test {
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng, rng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand09::{RngCore, SeedableRng, rng};
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
@@ -227,7 +226,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
||||
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
@@ -61,7 +61,6 @@ pub fn generate_keypair_mceliece<'a, R>(
|
||||
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);
|
||||
|
||||
+14
-15
@@ -9,7 +9,7 @@
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
@@ -33,7 +33,7 @@ use crate::frame::KKTFrame;
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - Random number generator
|
||||
/// * `rng` - random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
@@ -90,7 +90,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
@@ -199,14 +199,14 @@ mod tests {
|
||||
|
||||
fn random_x25519_key() -> x25519::PrivateKey {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
||||
let (session_key, context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
@@ -262,7 +262,7 @@ mod tests {
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
@@ -304,8 +304,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (mut context, request_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
@@ -324,7 +323,7 @@ mod tests {
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
@@ -337,7 +336,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -390,7 +389,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -417,7 +416,7 @@ mod tests {
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, mut context, request_frame) = request_kem_key(
|
||||
let (session_key, context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
@@ -437,7 +436,7 @@ mod tests {
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
|
||||
+26
-26
@@ -38,7 +38,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -106,18 +106,18 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (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, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -129,7 +129,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -141,7 +141,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -154,7 +154,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -165,7 +165,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -177,7 +177,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -190,7 +190,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -203,7 +203,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -214,7 +214,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -226,7 +226,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -241,7 +241,7 @@ mod test {
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -312,7 +312,7 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
@@ -330,11 +330,11 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -352,7 +352,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -364,7 +364,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -388,7 +388,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -399,7 +399,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -417,7 +417,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -430,7 +430,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -454,7 +454,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -465,7 +465,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -483,7 +483,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
@@ -73,7 +73,7 @@ where
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
session_id: KKTSessionId,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing::debug;
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
pub trait LpTransport: Sized {
|
||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
||||
@@ -24,32 +24,7 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
self.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Send the actual packet data
|
||||
self.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Flush to ensure data is sent immediately
|
||||
self.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
|
||||
|
||||
/// Receives an LP packet from a TCP stream with length-prefixed framing.
|
||||
///
|
||||
@@ -57,35 +32,72 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
self.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
async fn send_serialised_packet_async_write<W>(
|
||||
writer: &mut W,
|
||||
packet_data: &[u8],
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
writer
|
||||
.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
// Send the actual packet data
|
||||
writer
|
||||
.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
self.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
// Flush to ensure data is sent immediately
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader
|
||||
.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
reader
|
||||
.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
}
|
||||
|
||||
impl LpTransport for TcpStream {
|
||||
@@ -97,6 +109,14 @@ impl LpTransport for TcpStream {
|
||||
// Set TCP_NODELAY for low latency
|
||||
self.set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "io-mocks")]
|
||||
@@ -108,4 +128,12 @@ impl LpTransport for MockIOStream {
|
||||
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||
rand09 = { package = "rand", version = "0.9.2" }
|
||||
rand09 = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-lp-transport = { path = "../nym-lp-transport" }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||
@@ -34,12 +35,21 @@ num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
# needed for the 'mock 'feature
|
||||
nym-test-utils = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
#rand_chacha = "0.3"
|
||||
mock_instant = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
|
||||
|
||||
[features]
|
||||
mock = ["nym-test-utils", "nym-crypto/rand"]
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use nym_test_utils::helpers::u64_seeded_rng;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let mut rng = u64_seeded_rng(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
|
||||
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
|
||||
// Need payload and trailer to meet min_size requirement
|
||||
let payload_size = 10; // Arbitrary
|
||||
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
||||
@@ -565,7 +565,7 @@ mod tests {
|
||||
let result = parse_lp_packet(&buf, None);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::InvalidMessageType(255)) => {} // Expected error
|
||||
Err(LpError::InvalidMessageType(231)) => {} // Expected error
|
||||
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
@@ -628,7 +628,7 @@ mod tests {
|
||||
receiver_idx: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -681,7 +681,7 @@ mod tests {
|
||||
receiver_idx: 100,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [55; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -1289,4 +1289,37 @@ mod tests {
|
||||
_ => panic!("Expected SubsessionKK1 message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_error() {
|
||||
use crate::message::ErrorPacketData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let error_data = ErrorPacketData {
|
||||
message: "this is an error".to_string(),
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::Error(error_data.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||
|
||||
assert_eq!(decoded.header.receiver_idx, 42);
|
||||
match decoded.message {
|
||||
LpMessage::Error(data) => {
|
||||
assert_eq!(data.message, "this is an error");
|
||||
}
|
||||
_ => panic!("Expected Error message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::MessageType;
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||
use nym_kkt::error::KKTError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -102,4 +104,25 @@ pub enum LpError {
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
},
|
||||
|
||||
#[error("failed to complete KKT/PSQ handshake: {0}")]
|
||||
KKTPSQHandshake(String),
|
||||
|
||||
#[error("failed to complete the KKT exchange: {source}")]
|
||||
KKTFailure {
|
||||
#[from]
|
||||
source: KKTError,
|
||||
},
|
||||
}
|
||||
|
||||
impl LpError {
|
||||
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
|
||||
Self::KKTPSQHandshake(msg.into())
|
||||
}
|
||||
|
||||
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
|
||||
Self::KKTPSQHandshake(format!(
|
||||
"received unexpected response, got: {got:?}, expected: {expected:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+136
-61
@@ -10,6 +10,7 @@ pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod peer;
|
||||
pub mod psk;
|
||||
mod psq;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
@@ -21,62 +22,156 @@ pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::{LpSession, generate_fresh_salt};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub struct SessionsMock {
|
||||
pub initiator: LpSession,
|
||||
pub responder: LpSession,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl SessionsMock {
|
||||
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
let init_remote = init.as_remote();
|
||||
let salt = [42u8; 32];
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
|
||||
// skip KKT by just deriving the kem key locally
|
||||
let kem_keys = resp.kem_psq.as_ref().unwrap();
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
// INIT -> RESP: PSQ MSG1
|
||||
let psq_initiator = crate::psk::psq_initiator_create_message(
|
||||
init.x25519.private_key(),
|
||||
&resp_remote.x25519_public,
|
||||
&encapsulation_key,
|
||||
init.ed25519.private_key(),
|
||||
init.ed25519.public_key(),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
|
||||
|
||||
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(init.x25519().private_key().as_bytes())
|
||||
.remote_public_key(resp_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_initiator()
|
||||
.unwrap();
|
||||
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
|
||||
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
let psq_responder = crate::psk::psq_responder_process_message(
|
||||
resp.x25519.private_key(),
|
||||
&init_remote.x25519_public,
|
||||
(&decapsulation_key, &encapsulation_key),
|
||||
&init_remote.ed25519_public,
|
||||
&psq_payload,
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(resp.x25519().private_key().as_bytes())
|
||||
.remote_public_key(init_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_responder()
|
||||
.unwrap();
|
||||
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
|
||||
noise_protocol_resp.read_message(&noise_msg1).unwrap();
|
||||
|
||||
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
|
||||
noise_protocol_init.read_message(&noise_msg2).unwrap();
|
||||
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
assert!(noise_protocol_init.is_handshake_finished());
|
||||
|
||||
noise_protocol_resp.read_message(&noise_msg3).unwrap();
|
||||
assert!(noise_protocol_resp.is_handshake_finished());
|
||||
|
||||
SessionsMock {
|
||||
initiator: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key.clone(),
|
||||
init,
|
||||
resp_remote,
|
||||
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
noise_protocol_init,
|
||||
),
|
||||
responder: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key,
|
||||
resp,
|
||||
init_remote,
|
||||
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
noise_protocol_resp,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// we just need a dummy 'valid' session for simpler tests
|
||||
pub fn mock_initiator() -> LpSession {
|
||||
Self::mock_post_handshake(1234).initiator
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
let (init, resp) = crate::peer::mock_peers();
|
||||
let sessions = SessionsMock::mock_post_handshake(69);
|
||||
(sessions.initiator, sessions.responder)
|
||||
}
|
||||
|
||||
// Use a fixed receiver_index for deterministic tests
|
||||
let receiver_index: u32 = 12345;
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
let initiator_session = LpSession::new(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
packet::version::CURRENT,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
let responder_session = LpSession::new(
|
||||
receiver_index,
|
||||
false,
|
||||
resp,
|
||||
init.as_remote(),
|
||||
&salt,
|
||||
packet::version::CURRENT,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
(initiator_session, responder_session)
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_session_for_test() -> LpSession {
|
||||
SessionsMock::mock_initiator()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN, version};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{LpError, sessions_for_tests};
|
||||
use crate::{LpError, SessionsMock, mock_session_for_test};
|
||||
use bytes::BytesMut;
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
use crate::peer::mock_peers;
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let session = sessions_for_tests().0;
|
||||
let mut session = mock_session_for_test();
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
@@ -175,40 +270,20 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
// Create session manager
|
||||
let local_manager = SessionManager::new();
|
||||
let remote_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypairs for PSQ authentication
|
||||
let (init, resp) = mock_peers();
|
||||
let mut local_manager = SessionManager::new();
|
||||
let mut remote_manager = SessionManager::new();
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
|
||||
// Test salt
|
||||
let salt = [46u8; 32];
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let local_session = sessions.initiator;
|
||||
let remote_session = sessions.responder;
|
||||
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = local_manager.create_session_state_machine(local_session);
|
||||
let _ = remote_manager.create_session_state_machine(remote_session);
|
||||
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
resp,
|
||||
init.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError};
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
@@ -12,7 +13,7 @@ use std::fmt::{self, Display};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClientHelloData {
|
||||
/// Client-proposed receiver index for session identification (4 bytes)
|
||||
/// Auto-generated randomly by the client
|
||||
@@ -29,6 +30,17 @@ impl ClientHelloData {
|
||||
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
||||
pub const LEN: usize = 100;
|
||||
|
||||
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
|
||||
LpPacket::new(
|
||||
LpHeader::new(
|
||||
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
|
||||
0, // counter starts at 0
|
||||
protocol_version,
|
||||
),
|
||||
LpMessage::ClientHello(self),
|
||||
)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
Self::LEN
|
||||
}
|
||||
@@ -142,6 +154,8 @@ pub enum MessageType {
|
||||
SubsessionReady = 0x000C,
|
||||
/// Subsession abort - race winner tells loser to become responder
|
||||
SubsessionAbort = 0x000D,
|
||||
/// General error
|
||||
Error = 0x00FF,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
@@ -158,6 +172,9 @@ impl MessageType {
|
||||
pub struct HandshakeData(pub Vec<u8>);
|
||||
|
||||
impl HandshakeData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -175,6 +192,11 @@ impl HandshakeData {
|
||||
pub struct EncryptedDataPayload(pub Vec<u8>);
|
||||
|
||||
impl EncryptedDataPayload {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -193,6 +215,10 @@ impl EncryptedDataPayload {
|
||||
pub struct KKTRequestData(pub Vec<u8>);
|
||||
|
||||
impl KKTRequestData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -211,6 +237,10 @@ impl KKTRequestData {
|
||||
pub struct KKTResponseData(pub Vec<u8>);
|
||||
|
||||
impl KKTResponseData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -224,6 +254,52 @@ impl KKTResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
/// General human-readable error message
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorPacketData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorPacketData {
|
||||
pub(crate) fn new(message: impl Into<String>) -> Self {
|
||||
ErrorPacketData {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// length-encoding + message
|
||||
4 + self.message.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.message.len() as u32);
|
||||
dst.put_slice(self.message.as_bytes());
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ErrorPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||
if bytes[4..].len() != message_len {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
|
||||
bytes.len(),
|
||||
4 + message_len
|
||||
)));
|
||||
}
|
||||
|
||||
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
|
||||
|
||||
Ok(ErrorPacketData { message })
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardPacketData {
|
||||
@@ -449,6 +525,62 @@ pub enum LpMessage {
|
||||
SubsessionReady(SubsessionReadyData),
|
||||
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
||||
SubsessionAbort,
|
||||
/// An error has occurred
|
||||
Error(ErrorPacketData),
|
||||
}
|
||||
|
||||
impl From<HandshakeData> for LpMessage {
|
||||
fn from(value: HandshakeData) -> Self {
|
||||
LpMessage::Handshake(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptedDataPayload> for LpMessage {
|
||||
fn from(value: EncryptedDataPayload) -> Self {
|
||||
LpMessage::EncryptedData(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientHelloData> for LpMessage {
|
||||
fn from(value: ClientHelloData) -> Self {
|
||||
LpMessage::ClientHello(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTRequestData> for LpMessage {
|
||||
fn from(value: KKTRequestData) -> Self {
|
||||
LpMessage::KKTRequest(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KKTResponseData> for LpMessage {
|
||||
fn from(value: KKTResponseData) -> Self {
|
||||
LpMessage::KKTResponse(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ForwardPacketData> for LpMessage {
|
||||
fn from(value: ForwardPacketData) -> Self {
|
||||
LpMessage::ForwardPacket(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK1Data> for LpMessage {
|
||||
fn from(value: SubsessionKK1Data) -> Self {
|
||||
LpMessage::SubsessionKK1(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionKK2Data> for LpMessage {
|
||||
fn from(value: SubsessionKK2Data) -> Self {
|
||||
LpMessage::SubsessionKK2(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SubsessionReadyData> for LpMessage {
|
||||
fn from(value: SubsessionReadyData) -> Self {
|
||||
LpMessage::SubsessionReady(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LpMessage {
|
||||
@@ -468,6 +600,7 @@ impl Display for LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||
LpMessage::Error(_) => write!(f, "Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,6 +622,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
||||
LpMessage::SubsessionAbort => &[],
|
||||
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +642,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||
LpMessage::SubsessionAbort => true, // Empty signal
|
||||
LpMessage::Error(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +662,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(payload) => payload.len(),
|
||||
LpMessage::SubsessionReady(payload) => payload.len(),
|
||||
LpMessage::SubsessionAbort => 0,
|
||||
LpMessage::Error(payload) => payload.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +682,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
||||
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
||||
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
||||
LpMessage::Error(_) => MessageType::Error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +702,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
||||
LpMessage::SubsessionReady(data) => data.encode(dst),
|
||||
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
||||
LpMessage::Error(data) => data.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +755,7 @@ impl LpMessage {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::SubsessionAbort)
|
||||
}
|
||||
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,13 @@ pub enum ReadResult {
|
||||
// --- Implementation ---
|
||||
|
||||
impl NoiseProtocol {
|
||||
pub fn params() -> NoiseParams {
|
||||
// SAFETY: the hardcoded pattern must be valid
|
||||
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
|
||||
#[allow(clippy::unwrap_used)]
|
||||
crate::NOISE_PATTERN.parse().unwrap()
|
||||
}
|
||||
|
||||
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
||||
///
|
||||
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
||||
@@ -91,6 +98,46 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_handshake_state<'a>(
|
||||
local_private_key: &'a [u8],
|
||||
remote_public_key: &'a [u8],
|
||||
psk: &'a [u8],
|
||||
) -> snow::Builder<'a> {
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params = NoiseProtocol::params();
|
||||
|
||||
snow::Builder::new(noise_params)
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key)
|
||||
.psk(psk_index, psk)
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_initiator(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_initiator()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
|
||||
/// remote public key and psk
|
||||
pub fn build_new_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, NoiseError> {
|
||||
let handshake_state =
|
||||
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||
.build_responder()?;
|
||||
Ok(Self::new(handshake_state))
|
||||
}
|
||||
|
||||
/// Processes a single, complete incoming Noise message frame.
|
||||
///
|
||||
/// Assumes the caller handles buffering and framing to provide one full message.
|
||||
@@ -288,43 +335,3 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_noise_state(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_initiator()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
pub fn create_noise_state_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_responder()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use crate::message::LpMessage;
|
||||
use crate::message::{LpMessage, MessageType};
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_lp_common::format_debug_bytes;
|
||||
@@ -53,6 +53,10 @@ impl LpPacket {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> MessageType {
|
||||
self.message.typ()
|
||||
}
|
||||
|
||||
/// Compute a hash of the message payload
|
||||
///
|
||||
/// This can be used for message integrity verification or deduplication
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ClientHelloData;
|
||||
use crate::{ClientHelloData, LpError};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -52,6 +52,14 @@ impl LpLocalPeer {
|
||||
&self.x25519
|
||||
}
|
||||
|
||||
/// Returns the reference to the KEM Public key of the peer (if available).
|
||||
pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
|
||||
self.kem_psq
|
||||
.as_ref()
|
||||
.map(|kp| kp.public_key())
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)
|
||||
}
|
||||
|
||||
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
|
||||
#[doc(hidden)]
|
||||
pub fn as_remote(&self) -> LpRemotePeer {
|
||||
@@ -137,16 +145,37 @@ impl LpRemotePeer {
|
||||
self.expected_signing_key_digests = expected_signing_key_digests;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempt to retrieve expected KEM key hash of the remote
|
||||
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
|
||||
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
|
||||
pub(crate) fn expected_kem_key_hash(
|
||||
&self,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<Vec<u8>, LpError> {
|
||||
let kem = ciphersuite.kem();
|
||||
let hash_function = ciphersuite.hash_function();
|
||||
|
||||
let digests = self
|
||||
.expected_kem_key_digests
|
||||
.get(&kem)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
|
||||
|
||||
digests
|
||||
.get(&hash_function)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peer() -> LpLocalPeer {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
random_peer(&mut rng)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
let x25519 = Arc::new(ed25519.to_x25519());
|
||||
@@ -159,7 +188,7 @@ pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPe
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||
use crate::{LpError, LpPacket};
|
||||
use bytes::BytesMut;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(not(test))]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
|
||||
.map(|d| d.as_secs())
|
||||
}
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransportHandshakeExt: LpTransport {
|
||||
// the outer key is temporary until the algorithm is changed with psqv2
|
||||
async fn receive_packet(
|
||||
&mut self,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let raw = self.receive_raw_packet().await?;
|
||||
parse_lp_packet(&raw, outer_key)
|
||||
}
|
||||
|
||||
async fn send_packet(
|
||||
&mut self,
|
||||
packet: LpPacket,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
let mut packet_buf = BytesMut::new();
|
||||
|
||||
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
|
||||
self.send_serialised_packet(&packet_buf).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
|
||||
@@ -0,0 +1,391 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTRequestData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_initiator_create_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
|
||||
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use tracing::debug;
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
/// Generate and send client hello to the responder
|
||||
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
let timestamp = current_timestamp()?;
|
||||
|
||||
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
|
||||
self.connection
|
||||
.send_packet(client_hello_data.into_lp_packet(protocol), None)
|
||||
.await?;
|
||||
Ok(client_hello_data)
|
||||
}
|
||||
|
||||
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
|
||||
/// whether the request has been successful or whether there has been a collision in receiver
|
||||
/// index requiring a retry
|
||||
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
|
||||
match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Ack => Ok(true),
|
||||
LpMessage::Collision => Ok(false),
|
||||
other => {
|
||||
// TODO: retry on collision
|
||||
Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to send KKT request to begin the handshake
|
||||
pub(crate) async fn send_kkt_request(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
|
||||
let (session_secret, encrypted_frame) =
|
||||
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
|
||||
let lp_message = KKTRequestData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((kkt_context, session_secret))
|
||||
}
|
||||
|
||||
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
|
||||
/// the received encapsulation key
|
||||
pub(crate) async fn receive_kkt_response(
|
||||
&mut self,
|
||||
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<EncapsulationKey<'static>, LpError> {
|
||||
let kkt_response = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTResponse(response) => response,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTResponse,
|
||||
));
|
||||
}
|
||||
};
|
||||
debug!("received KKT response");
|
||||
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
|
||||
|
||||
let (response_frame, remote_context) =
|
||||
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
|
||||
let encapsulation_key = initiator_ingest_response(
|
||||
&kkt_context,
|
||||
&response_frame,
|
||||
&remote_context,
|
||||
&remote_peer.ed25519_public,
|
||||
&expected_kem_key_digest,
|
||||
)?;
|
||||
Ok(encapsulation_key)
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send initial PSQ msg1
|
||||
pub(crate) async fn send_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
let session_id = u32::from_le_bytes(*session_id_bytes);
|
||||
|
||||
let psq_initiator = psq_initiator_create_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
encapsulation_key,
|
||||
self.local_peer.ed25519.private_key(),
|
||||
self.local_peer.ed25519.public_key(),
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
// prepare noise state and msg1
|
||||
let mut noise_protocol = NoiseProtocol::build_new_initiator(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
|
||||
// prepare noise msg1
|
||||
let noise_msg1 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
|
||||
let psq_len = psq_payload.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
|
||||
combined.extend_from_slice(&psq_len.to_le_bytes());
|
||||
combined.extend_from_slice(&psq_payload);
|
||||
combined.extend_from_slice(&noise_msg1);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate received PSQ msg2
|
||||
pub(crate) async fn receive_psq_responder_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg2 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
|
||||
if psq_msg2.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
|
||||
if psq_msg2.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
// Extract and "store" the PSK handle
|
||||
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg2[2 + handle_len..];
|
||||
|
||||
// *sigh* ignore the message
|
||||
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send final PSQ msg3
|
||||
pub(crate) async fn send_final_psq_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let noise_msg3 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
|
||||
|
||||
let lp_message = HandshakeData::new(noise_msg3).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive final ACK that indicates finalisation of the handshake
|
||||
pub(crate) async fn receive_final_ack(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Ack => Ok(()),
|
||||
other => Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete_as_initiator_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 0. retrieve the expected kem key hash. if we don't know it,
|
||||
// there's no point in even trying to start the handshake
|
||||
let Some(remote_peer) = self.remote_peer.take() else {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
|
||||
));
|
||||
};
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
// and keep retrying until we manage to establish a receiver index without collisions
|
||||
let mut attempt = 0;
|
||||
let client_hello_data = loop {
|
||||
attempt += 1;
|
||||
|
||||
debug!("sending client hello");
|
||||
let client_hello = self
|
||||
.send_client_hello()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?;
|
||||
if self
|
||||
.receive_client_hello_ack()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?
|
||||
{
|
||||
debug!("received client hello ACK");
|
||||
break client_hello;
|
||||
}
|
||||
debug!("received client hello collision");
|
||||
|
||||
// TODO: make it configurable
|
||||
if attempt > 3 {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake(
|
||||
"failed to establish receiver index without collision",
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 3. prepare and send KKT request
|
||||
debug!("sending KKT request");
|
||||
let kkt_data = self
|
||||
.send_kkt_request(session_id, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. receive and process KKT response
|
||||
let encapsulation_key = self
|
||||
.receive_kkt_response(kkt_data, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT response");
|
||||
|
||||
// 5. prepare and send PSQ msg1
|
||||
debug!("sending PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
|
||||
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. receive and process PSQ msg2
|
||||
debug!("received PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. prepare and send PSQ msg3
|
||||
debug!("sending PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. receive final ACK and finalise
|
||||
debug!("received final ACK");
|
||||
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
// TODO: missing: receive counter check
|
||||
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_initiator_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::ErrorPacketData;
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use nym_kkt::ciphersuite::Ciphersuite;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use tracing::debug;
|
||||
|
||||
mod helpers;
|
||||
mod initiator;
|
||||
mod responder;
|
||||
|
||||
pub(crate) struct IntermediateHandshakeFailure {
|
||||
/// Session id established during exchange if we managed to derive it
|
||||
session_id: Option<u32>,
|
||||
|
||||
/// Protocol version established during the exchange
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Outer aead key established during exchange if we managed to derive it
|
||||
outer_aead_key: Option<OuterAeadKey>,
|
||||
|
||||
/// The error source
|
||||
source: LpError,
|
||||
}
|
||||
|
||||
impl IntermediateHandshakeFailure {
|
||||
fn plain(source: LpError) -> IntermediateHandshakeFailure {
|
||||
IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: None,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQHandshakeState<'a, S> {
|
||||
/// The underlying connection established for the handshake
|
||||
connection: &'a mut S,
|
||||
|
||||
/// Protocol version used for the exchange.
|
||||
/// either known implicitly through the directory (initiator)
|
||||
/// or established through client hello (responder)
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Ciphersuite selected for the KKT/PSQ exchange
|
||||
ciphersuite: Ciphersuite,
|
||||
|
||||
/// Representation of a local Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
local_peer: LpLocalPeer,
|
||||
|
||||
/// Representation of a remote Lewes Protocol peer
|
||||
/// encapsulating all the known information and keys.
|
||||
remote_peer: Option<LpRemotePeer>,
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: u64,
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
|
||||
PSQHandshakeState {
|
||||
connection,
|
||||
protocol_version: None,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer: None,
|
||||
sending_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
|
||||
self.protocol_version = Some(protocol_version);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
|
||||
self.remote_peer = Some(remote_peer);
|
||||
self
|
||||
}
|
||||
|
||||
fn protocol_version(&self) -> Result<u8, LpError> {
|
||||
self.protocol_version
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
|
||||
}
|
||||
|
||||
/// Generates the next counter value for outgoing packets.
|
||||
pub fn next_counter(&mut self) -> u64 {
|
||||
let counter = self.sending_counter;
|
||||
self.sending_counter += 1;
|
||||
counter
|
||||
}
|
||||
|
||||
pub fn next_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
message: LpMessage,
|
||||
) -> LpPacket {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(session_id, counter, protocol_version);
|
||||
LpPacket::new(header, message)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_send_error_packet(
|
||||
&mut self,
|
||||
err: IntermediateHandshakeFailure,
|
||||
) -> LpError {
|
||||
// if session_id is not known, we can't send the packet back (with the current design)
|
||||
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
|
||||
return err.source;
|
||||
};
|
||||
if let Err(err) = self
|
||||
.send_error_packet(
|
||||
session_id,
|
||||
protocol,
|
||||
err.source.to_string(),
|
||||
err.outer_aead_key.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to send back error response: {err}")
|
||||
}
|
||||
err.source
|
||||
}
|
||||
|
||||
/// Attempt to send an error packet
|
||||
pub(crate) async fn send_error_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
msg: impl Into<String>,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError> {
|
||||
let packet = self.next_packet(
|
||||
session_id,
|
||||
protocol_version,
|
||||
LpMessage::Error(ErrorPacketData::new(msg)),
|
||||
);
|
||||
self.connection.send_packet(packet, outer_aead_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive a packet from connection, explicitly checking for an error response
|
||||
/// and returning corresponding message if received
|
||||
pub(crate) async fn receive_non_error(
|
||||
&mut self,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError> {
|
||||
let packet = self.connection.receive_packet(outer_aead_key).await?;
|
||||
|
||||
match &packet.message {
|
||||
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
|
||||
"remote error: {}",
|
||||
error_packet.message
|
||||
))),
|
||||
_ => Ok(packet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::mock_peers;
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
|
||||
use mock_instant::thread_local::MockClock;
|
||||
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn extract_error(conn: &mut MockIOStream) -> String {
|
||||
let packet = conn.receive_packet(None).await.unwrap();
|
||||
match packet.message {
|
||||
LpMessage::Error(error) => error.message,
|
||||
_ => panic!("non error packet"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn e2e_psq_handshake() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
|
||||
|
||||
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
|
||||
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
|
||||
|
||||
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||
|
||||
let session_init = session_init???;
|
||||
let session_resp = session_resp???;
|
||||
|
||||
assert_eq!(session_init.id(), session_resp.id());
|
||||
assert_eq!(
|
||||
session_init.outer_aead_key().as_bytes(),
|
||||
session_resp.outer_aead_key().as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
session_init.pq_shared_secret().as_bytes(),
|
||||
session_resp.pq_shared_secret().as_bytes()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let client_hello = handshake_init.send_client_hello().await?;
|
||||
let LpMessage::ClientHello(received_client_hello) =
|
||||
conn_resp.receive_packet(None).await?.message
|
||||
else {
|
||||
panic!("wrong message type");
|
||||
};
|
||||
assert_eq!(client_hello, received_client_hello);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// essentially make sure you can't accidentally trigger the handshake as the responder
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (_, resp) = mock_peers();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let sending_res = handshake_resp.send_client_hello().await;
|
||||
assert!(sending_res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
|
||||
let current_time = Duration::from_secs(10000);
|
||||
MockClock::set_system_time(current_time);
|
||||
|
||||
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
|
||||
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
// TOO OLD
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_old.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
assert!(err.to_string().contains("too old"));
|
||||
|
||||
// TOO RECENT
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("too future"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTResponseData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_responder_process_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
|
||||
use nym_kkt::frame::KKTSessionId;
|
||||
use nym_kkt::session::{responder_ingest_message, responder_process};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
|
||||
|
||||
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
|
||||
fn validate_client_hello_timestamp(
|
||||
client_timestamp: u64,
|
||||
tolerance: Duration,
|
||||
) -> Result<(), LpError> {
|
||||
let now = current_timestamp()?;
|
||||
|
||||
let age = now.abs_diff(client_timestamp);
|
||||
if age > tolerance.as_secs() {
|
||||
let direction = if now >= client_timestamp {
|
||||
"old"
|
||||
} else {
|
||||
"future"
|
||||
};
|
||||
|
||||
return Err(LpError::kkt_psq_handshake(format!(
|
||||
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
|
||||
tolerance.as_secs()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub(crate) fn encapsulated_kem_keys(
|
||||
&self,
|
||||
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
|
||||
let kem_keys = self
|
||||
.local_peer
|
||||
.kem_psq
|
||||
.as_ref()
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)?;
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
Ok((dec_key, enc_key))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate ClientHello
|
||||
pub(crate) async fn receive_client_hello(
|
||||
&mut self,
|
||||
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
|
||||
let client_hello_packet = self.receive_non_error(None).await?;
|
||||
let client_hello = match client_hello_packet.message {
|
||||
LpMessage::ClientHello(client_hello) => client_hello,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::ClientHello,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
validate_client_hello_timestamp(
|
||||
client_hello.extract_timestamp(),
|
||||
DEFAULT_TIMESTAMP_TOLERANCE,
|
||||
)?;
|
||||
|
||||
// TODO: somehow check for collision
|
||||
|
||||
// set version and remote peer information
|
||||
self.protocol_version = Some(client_hello_packet.header.protocol_version);
|
||||
let remote_peer = LpRemotePeer::new(
|
||||
client_hello.client_ed25519_public_key,
|
||||
client_hello.client_lp_public_key,
|
||||
);
|
||||
|
||||
Ok((client_hello, remote_peer))
|
||||
}
|
||||
|
||||
/// Send client hello ACK
|
||||
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection.send_packet(ack, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a KKT request
|
||||
pub(crate) async fn receive_kkt_request(
|
||||
&mut self,
|
||||
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
|
||||
let kkt_request = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTRequest(request) => request.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (session_secret, request_frame, remote_context) =
|
||||
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
|
||||
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
|
||||
|
||||
Ok((context, session_secret, request_frame.session_id()))
|
||||
}
|
||||
|
||||
/// Attempt to send KKT response to the previously received request
|
||||
pub(crate) async fn send_kkt_response(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let response_frame = responder_process(
|
||||
&kkt_context,
|
||||
kkt_session_id,
|
||||
self.local_peer.ed25519().private_key(),
|
||||
encapsulation_key,
|
||||
)?;
|
||||
let encrypted_frame = encrypt_kkt_frame(
|
||||
&mut rng(),
|
||||
&session_secret,
|
||||
&response_frame,
|
||||
KKT_RESPONSE_AAD,
|
||||
)?;
|
||||
let lp_message = KKTResponseData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a PSQ msg1 request
|
||||
pub(crate) async fn receive_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
|
||||
let psq_msg1 = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
|
||||
if psq_msg1.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
|
||||
if psq_msg1.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let psq_payload = &psq_msg1[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg1[2 + handle_len..];
|
||||
|
||||
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
|
||||
let psq_responder = psq_responder_process_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
local_kem_keypair,
|
||||
&remote_peer.ed25519_public,
|
||||
psq_payload,
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
|
||||
let psk = psq_responder.psk;
|
||||
let psk_handle = psq_responder.psk_handle;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
let mut noise_protocol = NoiseProtocol::build_new_responder(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
noise_protocol.read_message(noise_payload)?;
|
||||
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
psk_handle,
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to prepare and generate a responder PSQ msg2
|
||||
pub(crate) async fn send_psq_responder_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
psk_handle: &[u8],
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let msg2 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
|
||||
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
|
||||
let handle_len = psk_handle.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
|
||||
combined.extend_from_slice(&handle_len.to_le_bytes());
|
||||
combined.extend_from_slice(psk_handle);
|
||||
combined.extend_from_slice(&msg2);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process final PSQ msg3
|
||||
pub(crate) async fn receive_final_psq_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg3 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
noise_protocol.read_message(&psq_msg3)?;
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send final ACK to indicate finalisation of the handshake
|
||||
pub(crate) async fn send_final_ack(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection
|
||||
.send_packet(ack, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn complete_as_responder_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 1. receive and validate ClientHello
|
||||
let (client_hello_data, remote_peer) =
|
||||
self.receive_client_hello()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received client hello");
|
||||
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 2. send ack
|
||||
debug!("sending client hello ACK");
|
||||
self.send_client_hello_ack(session_id)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 3. receive and process KKT request
|
||||
let kkt_data =
|
||||
self.receive_kkt_request()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT request");
|
||||
|
||||
// TEMP: 'derive' KEM keys
|
||||
let (dec_key, enc_key) =
|
||||
self.encapsulated_kem_keys()
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. prepare and send KKT response
|
||||
debug!("sending KKT response");
|
||||
self.send_kkt_response(session_id, kkt_data, &enc_key)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 5. receive and process PSQ msg1
|
||||
debug!("received PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
|
||||
.receive_psq_initiator_message(
|
||||
&remote_peer,
|
||||
(&dec_key, &enc_key),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. prepare and send PSQ msg2
|
||||
debug!("sending PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.send_psq_responder_message(
|
||||
session_id,
|
||||
&psk_handle,
|
||||
&outer_aead_key,
|
||||
&mut noise_protocol,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. receive and process PSQ msg3
|
||||
debug!("received PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. [optionally] send ACK to finalise
|
||||
debug!("sending final ACK");
|
||||
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_responder_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
-1790
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
mod tests {
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
use crate::{
|
||||
LpError,
|
||||
LpError, SessionsMock,
|
||||
message::LpMessage,
|
||||
packet::{LpHeader, LpPacket, TRAILER_LEN},
|
||||
session_manager::SessionManager,
|
||||
@@ -44,214 +44,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_full_session_flow() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 100001;
|
||||
|
||||
// Test salt
|
||||
let salt = [42u8; 32];
|
||||
|
||||
// 4. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session A");
|
||||
|
||||
let peer_b_sm = session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b.clone(),
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session B");
|
||||
// 2. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
// Verify session count
|
||||
assert_eq!(session_manager_1.session_count(), 1);
|
||||
assert_eq!(session_manager_2.session_count(), 1);
|
||||
|
||||
// Initialize KKT state for both sessions (test bypass)
|
||||
session_manager_1
|
||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer A");
|
||||
session_manager_2
|
||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer B");
|
||||
|
||||
// 5. Simulate Noise Handshake (Sans-IO)
|
||||
println!("Starting handshake simulation...");
|
||||
let mut i_msg_payload;
|
||||
let mut r_msg_payload = None;
|
||||
let mut rounds = 0;
|
||||
const MAX_ROUNDS: usize = 10;
|
||||
|
||||
// Prime initiator's first message
|
||||
i_msg_payload = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
i_msg_payload.is_some(),
|
||||
"Initiator did not produce initial message"
|
||||
);
|
||||
|
||||
while rounds < MAX_ROUNDS {
|
||||
rounds += 1;
|
||||
let mut did_exchange = false;
|
||||
|
||||
// === Initiator -> Responder ===
|
||||
if let Some(payload) = i_msg_payload.take() {
|
||||
did_exchange = true;
|
||||
println!(
|
||||
" Round {}: Initiator -> Responder ({} bytes)",
|
||||
rounds,
|
||||
payload.len()
|
||||
);
|
||||
|
||||
// A prepares packet
|
||||
let counter = session_manager_1.next_counter(receiver_index).unwrap();
|
||||
let message_a_to_b = create_test_packet(1, receiver_index, counter, payload);
|
||||
let mut encoded_msg = BytesMut::new();
|
||||
serialize_lp_packet(&message_a_to_b, &mut encoded_msg, None)
|
||||
.expect("A serialize failed");
|
||||
|
||||
// B parses packet and checks replay
|
||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("B parse failed");
|
||||
assert_eq!(decoded_packet.header.counter, counter);
|
||||
|
||||
// Check replay before processing handshake
|
||||
session_manager_2
|
||||
.receiving_counter_quick_check(peer_b_sm, decoded_packet.header.counter)
|
||||
.expect("B replay check failed (A->B)");
|
||||
|
||||
match session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &decoded_packet.message)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Mark counter only after successful processing
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, decoded_packet.header.counter)
|
||||
.expect("B mark counter failed");
|
||||
}
|
||||
Err(e) => panic!("Responder processing failed: {:?}", e),
|
||||
}
|
||||
// Check if responder needs to send a reply
|
||||
r_msg_payload = session_manager_2
|
||||
.prepare_handshake_message(peer_b_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
println!("{:?}", r_msg_payload);
|
||||
}
|
||||
|
||||
// Check completion
|
||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
{
|
||||
println!("Handshake completed after Initiator->Responder message.");
|
||||
break;
|
||||
}
|
||||
|
||||
// === Responder -> Initiator ===
|
||||
if let Some(payload) = r_msg_payload.take() {
|
||||
did_exchange = true;
|
||||
println!(
|
||||
" Round {}: Responder -> Initiator ({} bytes)",
|
||||
rounds,
|
||||
payload.len()
|
||||
);
|
||||
|
||||
// B prepares packet
|
||||
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
|
||||
let message_b_to_a = create_test_packet(1, receiver_index, counter, payload);
|
||||
let mut encoded_msg = BytesMut::new();
|
||||
serialize_lp_packet(&message_b_to_a, &mut encoded_msg, None)
|
||||
.expect("B serialize failed");
|
||||
|
||||
// A parses packet and checks replay
|
||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("A parse failed");
|
||||
assert_eq!(decoded_packet.header.counter, counter);
|
||||
|
||||
// Check replay before processing handshake
|
||||
session_manager_1
|
||||
.receiving_counter_quick_check(peer_a_sm, decoded_packet.header.counter)
|
||||
.expect("A replay check failed (B->A)");
|
||||
|
||||
match session_manager_1
|
||||
.process_handshake_message(peer_a_sm, &decoded_packet.message)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Mark counter only after successful processing
|
||||
session_manager_1
|
||||
.receiving_counter_mark(peer_a_sm, decoded_packet.header.counter)
|
||||
.expect("A mark counter failed");
|
||||
}
|
||||
Err(e) => panic!("Initiator processing failed: {:?}", e),
|
||||
}
|
||||
|
||||
// Check if initiator needs to send a reply
|
||||
i_msg_payload = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// println!("Initiator state: {}", session_manager_1.get_state(peer_a_sm).unwrap());
|
||||
// println!("Responder state: {}", session_manager_2.get_state(peer_b_sm).unwrap());
|
||||
|
||||
println!(
|
||||
"Initiator state: {}",
|
||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
);
|
||||
println!(
|
||||
"Responder state: {}",
|
||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
);
|
||||
|
||||
// Check completion again
|
||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
{
|
||||
println!("Handshake completed after Responder->Initiator message.");
|
||||
|
||||
// Safety break if no messages were exchanged in a round
|
||||
if !did_exchange {
|
||||
println!("No messages exchanged in round {}, breaking.", rounds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(rounds < MAX_ROUNDS, "Handshake loop exceeded max rounds");
|
||||
}
|
||||
assert!(
|
||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap(),
|
||||
"Initiator handshake did not complete"
|
||||
);
|
||||
assert!(
|
||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap(),
|
||||
"Responder handshake did not complete"
|
||||
);
|
||||
println!(
|
||||
"Handshake simulation completed successfully in {} rounds.",
|
||||
rounds
|
||||
);
|
||||
|
||||
// --- Handshake Complete ---
|
||||
|
||||
// 7. Simulate Data Transfer (Post-Handshake)
|
||||
// 3. Simulate Data Transfer (Post-Handshake)
|
||||
println!("Starting data transfer simulation...");
|
||||
let plaintext_a_to_b = b"Hello from A!";
|
||||
|
||||
@@ -328,7 +135,7 @@ mod tests {
|
||||
|
||||
println!("Data transfer simulation completed.");
|
||||
|
||||
// 8. Replay Protection Test (Data Packet)
|
||||
// 4. Replay Protection Test (Data Packet)
|
||||
println!("Testing data packet replay protection...");
|
||||
// Try to replay the last message from B to A
|
||||
// Need to re-encode because decode consumes the buffer
|
||||
@@ -359,7 +166,7 @@ mod tests {
|
||||
);
|
||||
println!("Data packet replay protection test passed.");
|
||||
|
||||
// 9. Test out-of-order packet reception (send counter N+1 before counter N)
|
||||
// 5. Test out-of-order packet reception (send counter N+1 before counter N)
|
||||
println!("Testing out-of-order data packet reception...");
|
||||
let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1
|
||||
let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
|
||||
@@ -405,7 +212,7 @@ mod tests {
|
||||
String::from_utf8_lossy(&decrypted_payload)
|
||||
);
|
||||
|
||||
// 10. Now send the skipped counter N message (should still work)
|
||||
// 6. Now send the skipped counter N message (should still work)
|
||||
println!("Testing delayed data packet reception...");
|
||||
// Prepare data for counter_a_next (N)
|
||||
let plaintext_delayed = b"Delayed message";
|
||||
@@ -453,7 +260,7 @@ mod tests {
|
||||
|
||||
println!("Delayed data packet reception test passed.");
|
||||
|
||||
// 11. Try to replay message with counter N (should fail)
|
||||
// 7. Try to replay message with counter N (should fail)
|
||||
println!("Testing replay of delayed packet...");
|
||||
let parsed_delayed_replay =
|
||||
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
|
||||
@@ -465,7 +272,7 @@ mod tests {
|
||||
"Should be a replay protection error"
|
||||
);
|
||||
|
||||
// 12. Session removal
|
||||
// 8. Session removal
|
||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||
assert_eq!(session_manager_1.session_count(), 0);
|
||||
|
||||
@@ -482,94 +289,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_bidirectional_communication() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100002;
|
||||
// 2. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
// Test salt
|
||||
let salt = [43u8; 32];
|
||||
// Counters after handshake
|
||||
let mut counter_a = 0; // Next counter for A to send
|
||||
let mut counter_b = 0; // Next counter for B to send
|
||||
|
||||
let peer_a_sm = session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session A");
|
||||
|
||||
let peer_b_sm = session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b.clone(),
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session B");
|
||||
|
||||
// Initialize KKT state for both sessions (test bypass)
|
||||
session_manager_1
|
||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer A");
|
||||
session_manager_2
|
||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer B");
|
||||
|
||||
// Drive handshake to completion (simplified)
|
||||
let mut i_msg = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &i_msg)
|
||||
.unwrap();
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, 0)
|
||||
.unwrap(); // Assume counter 0 for first msg
|
||||
let r_msg = session_manager_2
|
||||
.prepare_handshake_message(peer_b_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
session_manager_1
|
||||
.process_handshake_message(peer_a_sm, &r_msg)
|
||||
.unwrap();
|
||||
session_manager_1
|
||||
.receiving_counter_mark(peer_a_sm, 0)
|
||||
.unwrap(); // Assume counter 0 for first msg
|
||||
i_msg = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &i_msg)
|
||||
.unwrap();
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, 1)
|
||||
.unwrap(); // Assume counter 1 for second msg from A
|
||||
|
||||
assert!(session_manager_1.is_handshake_complete(peer_a_sm).unwrap());
|
||||
assert!(session_manager_2.is_handshake_complete(peer_b_sm).unwrap());
|
||||
println!("Bidirectional test: Handshake complete.");
|
||||
|
||||
// Counters after handshake (A sent 2, B sent 1)
|
||||
let mut counter_a = 2; // Next counter for A to send
|
||||
let mut counter_b = 1; // Next counter for B to send
|
||||
|
||||
// 4. Send multiple encrypted messages both ways
|
||||
// 3. Send multiple encrypted messages both ways
|
||||
const NUM_MESSAGES: u64 = 5;
|
||||
for i in 0..NUM_MESSAGES {
|
||||
println!("Bidirectional test: Round {}", i);
|
||||
@@ -634,36 +368,30 @@ mod tests {
|
||||
// Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7.
|
||||
// Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6.
|
||||
assert_eq!(
|
||||
counter_a,
|
||||
2 + NUM_MESSAGES,
|
||||
counter_a, NUM_MESSAGES,
|
||||
"Peer A final send counter mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
total_recv_a,
|
||||
1 + NUM_MESSAGES,
|
||||
total_recv_a, NUM_MESSAGES,
|
||||
"Peer A total received count mismatch"
|
||||
); // Received 1 handshake + 5 data
|
||||
); // Received 5 data
|
||||
assert_eq!(
|
||||
next_recv_a,
|
||||
1 + NUM_MESSAGES,
|
||||
next_recv_a, NUM_MESSAGES,
|
||||
"Peer A next expected receive counter mismatch"
|
||||
); // Expected counter for msg from B
|
||||
|
||||
// Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6.
|
||||
// Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7.
|
||||
assert_eq!(
|
||||
counter_b,
|
||||
1 + NUM_MESSAGES,
|
||||
counter_b, NUM_MESSAGES,
|
||||
"Peer B final send counter mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
total_recv_b,
|
||||
2 + NUM_MESSAGES,
|
||||
total_recv_b, NUM_MESSAGES,
|
||||
"Peer B total received count mismatch"
|
||||
); // Received 2 handshake + 5 data
|
||||
); // Received 5 data
|
||||
assert_eq!(
|
||||
next_recv_b,
|
||||
2 + NUM_MESSAGES,
|
||||
next_recv_b, NUM_MESSAGES,
|
||||
"Peer B next expected receive counter mismatch"
|
||||
); // Expected counter for msg from A
|
||||
|
||||
@@ -674,28 +402,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_error_handling() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager = SessionManager::new();
|
||||
let mut session_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypair for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100003;
|
||||
|
||||
// Test salt
|
||||
let salt = [44u8; 32];
|
||||
let receiver_index = 123;
|
||||
let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator;
|
||||
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||
|
||||
// 2. Create a session (using real noise state)
|
||||
let _session = session_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session");
|
||||
let _session = session_manager.create_session_state_machine(session1);
|
||||
|
||||
// 3. Try to get a non-existent session
|
||||
let result = session_manager.state_machine_exists(999);
|
||||
@@ -709,20 +423,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// 5. Create and immediately remove a session
|
||||
let receiver_index_temp: u32 = 100004;
|
||||
let _temp_session = session_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index_temp,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create temp session");
|
||||
let _temp_session = session_manager.create_session_state_machine(session2);
|
||||
|
||||
assert!(
|
||||
session_manager.remove_state_machine(receiver_index_temp),
|
||||
session_manager.remove_state_machine(124),
|
||||
"Should remove the session"
|
||||
);
|
||||
|
||||
@@ -769,15 +473,8 @@ mod tests {
|
||||
}
|
||||
// Remove unused imports if SessionManager methods are no longer direct dependencies
|
||||
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
|
||||
use crate::packet::version;
|
||||
use crate::peer::mock_peers;
|
||||
use crate::state_machine::LpData;
|
||||
use crate::{
|
||||
// Bring in state machine types
|
||||
state_machine::{LpAction, LpInput, LpStateBare},
|
||||
// message::LpMessage, // LpMessage likely still needed for LpInput/LpAction
|
||||
// packet::{LpHeader, LpPacket, TRAILER_LEN}, // LpPacket needed for LpAction/LpInput
|
||||
};
|
||||
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||
// Use Bytes for SendData input
|
||||
|
||||
// Keep helper function for creating test packets if needed,
|
||||
@@ -794,309 +491,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_full_session_flow_with_process_input() {
|
||||
// 1. Initialize session managers
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100005;
|
||||
|
||||
// Test salt
|
||||
let salt = [45u8; 32];
|
||||
|
||||
// 3. Create sessions state machines
|
||||
assert!(
|
||||
session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT
|
||||
) // Initiator
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b,
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT
|
||||
) // Responder
|
||||
.is_ok()
|
||||
);
|
||||
// 2. Create sessions state machines
|
||||
session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
assert_eq!(session_manager_1.session_count(), 1);
|
||||
assert_eq!(session_manager_2.session_count(), 1);
|
||||
assert!(session_manager_1.state_machine_exists(receiver_index));
|
||||
assert!(session_manager_2.state_machine_exists(receiver_index));
|
||||
|
||||
// Verify initial states are ReadyToHandshake
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::ReadyToHandshake
|
||||
);
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::ReadyToHandshake
|
||||
);
|
||||
|
||||
// --- 4. Simulate Noise Handshake via process_input ---
|
||||
println!("Starting handshake simulation via process_input...");
|
||||
|
||||
let mut packet_a_to_b: Option<LpPacket>;
|
||||
let mut packet_b_to_a: Option<LpPacket>;
|
||||
let mut rounds = 0;
|
||||
const MAX_ROUNDS: usize = 10; // KKT (2 messages) + XK handshake (3 messages) + PSQ = 6 rounds total
|
||||
|
||||
// --- Round 1: Initiator Starts ---
|
||||
println!(" Round {}: Initiator starts handshake", rounds);
|
||||
let action_a1 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::StartHandshake)
|
||||
.expect("Initiator StartHandshake should produce an action")
|
||||
.expect("Initiator StartHandshake failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_a1 {
|
||||
println!(" Initiator produced SendPacket (KKT request)");
|
||||
packet_a_to_b = Some(packet);
|
||||
} else {
|
||||
panic!("Initiator StartHandshake did not produce SendPacket");
|
||||
}
|
||||
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::KKTExchange,
|
||||
"Initiator state wrong after StartHandshake (should be KKTExchange)"
|
||||
);
|
||||
|
||||
// *** ADD THIS BLOCK for Responder StartHandshake ***
|
||||
println!(
|
||||
" Round {}: Responder explicitly enters KKTExchange state",
|
||||
rounds
|
||||
);
|
||||
let action_b_start =
|
||||
session_manager_2.process_input(receiver_index, LpInput::StartHandshake);
|
||||
// Responder's StartHandshake should not produce an action to send
|
||||
assert!(
|
||||
action_b_start.as_ref().unwrap().is_none(),
|
||||
"Responder StartHandshake should produce None action, got {:?}",
|
||||
action_b_start
|
||||
);
|
||||
// Verify responder transitions to KKTExchange state (not Handshaking yet)
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
|
||||
"Responder state should be KKTExchange after its StartHandshake"
|
||||
);
|
||||
// *** END OF ADDED BLOCK ***
|
||||
|
||||
// --- Round 2: Responder Receives KKT Request, Sends KKT Response ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives KKT request, sends KKT response",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("KKT request from A was missing");
|
||||
|
||||
// Simulate network: serialize -> parse (optional but good practice)
|
||||
let mut buf_a = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a, None).unwrap();
|
||||
let parsed_packet_a = parse_lp_packet(&buf_a, None).unwrap();
|
||||
|
||||
// Responder processes KKT request
|
||||
let action_b1 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
|
||||
.expect("Responder ReceivePacket should produce an action")
|
||||
.expect("Responder ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_b1 {
|
||||
println!(" Responder received KKT request, produced KKT response");
|
||||
packet_b_to_a = Some(packet);
|
||||
} else {
|
||||
panic!("Responder ReceivePacket did not produce SendPacket for KKT response");
|
||||
}
|
||||
// Responder transitions to Handshaking after KKT completes
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Responder state should be Handshaking after KKT exchange"
|
||||
);
|
||||
|
||||
// --- Round 3: Initiator Receives KKT Response, Sends First Noise Message (with PSQ) ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Initiator receives KKT response, sends first Noise message (with PSQ)",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_b_to_a
|
||||
.take()
|
||||
.expect("KKT response from B was missing");
|
||||
|
||||
// Simulate network
|
||||
let mut buf_b = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_b, None).unwrap();
|
||||
let parsed_packet_b = parse_lp_packet(&buf_b, None).unwrap();
|
||||
|
||||
// Initiator processes KKT response
|
||||
let action_a2 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
|
||||
.expect("Initiator ReceivePacket should produce an action")
|
||||
.expect("Initiator ReceivePacket failed");
|
||||
|
||||
match action_a2 {
|
||||
LpAction::SendPacket(packet) => {
|
||||
println!(
|
||||
" Initiator received KKT response, produced first Noise message (-> e)"
|
||||
);
|
||||
packet_a_to_b = Some(packet);
|
||||
// Initiator transitions to Handshaking after KKT completes
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Initiator state should be Handshaking after receiving KKT response"
|
||||
);
|
||||
}
|
||||
LpAction::KKTComplete => {
|
||||
println!(
|
||||
" Initiator received KKT response, produced KKTComplete (will send Noise in next step)"
|
||||
);
|
||||
// KKT completed, now need to explicitly trigger handshake message
|
||||
// This might be the case if KKT completion doesn't automatically send the first Noise message
|
||||
// Let's try to prepare the handshake message
|
||||
if let Some(msg_result) =
|
||||
session_manager_1.prepare_handshake_message(receiver_index)
|
||||
{
|
||||
let msg = msg_result.expect("Failed to prepare handshake message after KKT");
|
||||
// Create a packet from the message
|
||||
let packet = create_test_packet(1, receiver_index, 0, msg);
|
||||
packet_a_to_b = Some(packet);
|
||||
println!(" Prepared first Noise message after KKTComplete");
|
||||
} else {
|
||||
panic!("No handshake message available after KKT complete");
|
||||
}
|
||||
}
|
||||
other => {
|
||||
panic!(
|
||||
"Initiator ReceivePacket produced unexpected action after KKT response: {:?}",
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Round 4: Responder Receives First Noise Message, Sends Second ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives first Noise message, sends second",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("First Noise packet from A was missing");
|
||||
|
||||
// Simulate network
|
||||
let mut buf_a2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a2, None).unwrap();
|
||||
let parsed_packet_a2 = parse_lp_packet(&buf_a2, None).unwrap();
|
||||
|
||||
// Responder processes first Noise message and sends second Noise message
|
||||
let action_b2 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
|
||||
.expect("Responder ReceivePacket should produce an action")
|
||||
.expect("Responder ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_b2 {
|
||||
println!(
|
||||
" Responder received first Noise message, produced second Noise message (<- e, ee, s, es)"
|
||||
);
|
||||
packet_b_to_a = Some(packet);
|
||||
} else {
|
||||
panic!("Responder did not produce SendPacket for second Noise message");
|
||||
}
|
||||
// Responder still in Handshaking, waiting for final message
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Responder state should still be Handshaking after sending second message"
|
||||
);
|
||||
|
||||
// --- Round 5: Initiator Receives Second Noise Message, Sends Third, Completes ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Initiator receives second Noise message, sends third, completes",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_b_to_a
|
||||
.take()
|
||||
.expect("Second Noise packet from B was missing");
|
||||
|
||||
let mut buf_b2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_b2, None).unwrap();
|
||||
let parsed_packet_b2 = parse_lp_packet(&buf_b2, None).unwrap();
|
||||
|
||||
let action_a3 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
|
||||
.expect("Initiator ReceivePacket should produce an action")
|
||||
.expect("Initiator ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_a3 {
|
||||
println!(
|
||||
" Initiator received second Noise message, produced third Noise message (-> s, se)"
|
||||
);
|
||||
packet_a_to_b = Some(packet);
|
||||
} else {
|
||||
panic!("Initiator did not produce SendPacket for third Noise message");
|
||||
}
|
||||
// Initiator transitions to Transport after sending third message
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport,
|
||||
"Initiator state should be Transport after sending third message"
|
||||
);
|
||||
|
||||
// --- Round 6: Responder Receives Third Noise Message, Completes ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives third Noise message, completes",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("Third Noise packet from A was missing");
|
||||
|
||||
let mut buf_a3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a3, None).unwrap();
|
||||
let parsed_packet_a3 = parse_lp_packet(&buf_a3, None).unwrap();
|
||||
|
||||
let action_b3 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
|
||||
.expect("Responder final ReceivePacket should produce an action")
|
||||
.expect("Responder final ReceivePacket failed");
|
||||
|
||||
// Responder completes handshake
|
||||
if let LpAction::HandshakeComplete = action_b3 {
|
||||
println!(" Responder received third Noise message, produced HandshakeComplete");
|
||||
} else {
|
||||
println!(
|
||||
" Responder received third Noise message (Action: {:?})",
|
||||
action_b3
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport,
|
||||
"Responder state should be Transport after processing third message"
|
||||
);
|
||||
|
||||
// --- Verification ---
|
||||
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
|
||||
// Verify initial states are Transport
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport
|
||||
@@ -1105,9 +515,8 @@ mod tests {
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport
|
||||
);
|
||||
println!("Handshake simulation completed successfully via process_input.");
|
||||
|
||||
// --- 5. Simulate Data Transfer via process_input ---
|
||||
// --- 3. Simulate Data Transfer via process_input ---
|
||||
println!("Starting data transfer simulation via process_input...");
|
||||
let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec());
|
||||
let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
|
||||
@@ -1185,7 +594,7 @@ mod tests {
|
||||
}
|
||||
println!("Data transfer simulation completed.");
|
||||
|
||||
// --- 6. Replay Protection Test ---
|
||||
// --- 4. Replay Protection Test ---
|
||||
println!("Testing data packet replay protection via process_input...");
|
||||
let replay_result = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
|
||||
@@ -1199,7 +608,7 @@ mod tests {
|
||||
);
|
||||
println!("Data packet replay protection test passed.");
|
||||
|
||||
// --- 7. Out-of-Order Test ---
|
||||
// --- 5. Out-of-Order Test ---
|
||||
println!("Testing out-of-order reception via process_input...");
|
||||
|
||||
// A prepares N+1 then N
|
||||
@@ -1258,7 +667,7 @@ mod tests {
|
||||
);
|
||||
println!("Out-of-order test passed.");
|
||||
|
||||
// --- 8. Close Test ---
|
||||
// --- 6. Close Test ---
|
||||
println!("Testing close via process_input...");
|
||||
|
||||
// A closes
|
||||
@@ -1306,7 +715,7 @@ mod tests {
|
||||
));
|
||||
println!("Close test passed.");
|
||||
|
||||
// --- 9. Session Removal ---
|
||||
// --- 7. Session Removal ---
|
||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||
assert_eq!(session_manager_1.session_count(), 0);
|
||||
assert!(!session_manager_1.state_machine_exists(receiver_index));
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
//! This module implements session lifecycle management functionality, handling
|
||||
//! creation, retrieval, and storage of sessions.
|
||||
|
||||
use crate::noise_protocol::ReadResult;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
|
||||
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||
use dashmap::DashMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
@@ -18,7 +16,7 @@ use dashmap::DashMap;
|
||||
/// ensuring proper thread-safety for concurrent access.
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
state_machines: DashMap<u32, LpStateMachine>,
|
||||
state_machines: HashMap<u32, LpStateMachine>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
@@ -31,36 +29,18 @@ impl SessionManager {
|
||||
/// Creates a new session manager with empty session storage.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_machines: DashMap::new(),
|
||||
state_machines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
|
||||
pub fn process_input(
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
input: LpInput,
|
||||
) -> Result<Option<LpAction>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
||||
}
|
||||
|
||||
pub fn add(&self, session: LpSession) -> Result<(), LpError> {
|
||||
let sm = LpStateMachine {
|
||||
state: LpState::ReadyToHandshake {
|
||||
session: Box::new(session),
|
||||
},
|
||||
};
|
||||
self.state_machines.insert(sm.id()?, sm);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handshaking(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Handshaking)
|
||||
}
|
||||
|
||||
pub fn should_initiate_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.ready_to_handshake(lp_id)? || self.closed(lp_id)?)
|
||||
}
|
||||
|
||||
pub fn ready_to_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::ReadyToHandshake)
|
||||
}
|
||||
|
||||
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
|
||||
}
|
||||
@@ -84,38 +64,27 @@ impl SessionManager {
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
|
||||
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?.receiving_counter_mark(counter)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.prepare_handshake_message(lp_id)
|
||||
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
|
||||
}
|
||||
|
||||
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub fn is_handshake_complete(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))?
|
||||
}
|
||||
|
||||
pub fn next_counter(&self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))?
|
||||
}
|
||||
|
||||
pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?
|
||||
.decrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn encrypt_data(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?
|
||||
.encrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
@@ -125,14 +94,6 @@ impl SessionManager {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
||||
}
|
||||
|
||||
pub fn process_handshake_message(
|
||||
&self,
|
||||
lp_id: u32,
|
||||
message: &LpMessage,
|
||||
) -> Result<ReadResult, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))?
|
||||
}
|
||||
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.state_machines.len()
|
||||
}
|
||||
@@ -146,7 +107,7 @@ impl SessionManager {
|
||||
F: FnOnce(&LpStateMachine) -> R,
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
Ok(f(&sm))
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
@@ -154,90 +115,48 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
// For mutable access (like running process_input)
|
||||
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||
{
|
||||
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(&mut sm))
|
||||
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session_state_machine(
|
||||
&self,
|
||||
receiver_index: u32,
|
||||
is_initiator: bool,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
salt: &[u8; 32],
|
||||
protocol_version: u8,
|
||||
) -> Result<u32, LpError> {
|
||||
let sm = LpStateMachine::new(
|
||||
receiver_index,
|
||||
is_initiator,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
salt,
|
||||
protocol_version,
|
||||
)?;
|
||||
|
||||
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
|
||||
let receiver_index = lp_session.id();
|
||||
let sm = LpStateMachine::new(lp_session);
|
||||
self.state_machines.insert(receiver_index, sm);
|
||||
Ok(receiver_index)
|
||||
receiver_index
|
||||
}
|
||||
|
||||
/// Method to remove a state machine
|
||||
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
|
||||
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
|
||||
let removed = self.state_machines.remove(&lp_id);
|
||||
|
||||
removed.is_some()
|
||||
}
|
||||
|
||||
/// Test-only method to initialize KKT state to Completed for a session.
|
||||
/// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake.
|
||||
#[cfg(test)]
|
||||
pub fn init_kkt_for_test(
|
||||
&self,
|
||||
lp_id: u32,
|
||||
remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey,
|
||||
) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.set_kkt_completed_for_test(remote_x25519_pub);
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packet::version;
|
||||
use crate::peer::{mock_peers, random_peer};
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use crate::{SessionsMock, mock_session_for_test};
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_get() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let salt = [47u8; 32];
|
||||
let receiver_index: u32 = 1001;
|
||||
let local_session = mock_session_for_test();
|
||||
let id = local_session.id();
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
local,
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
assert_eq!(sm_1_id, id);
|
||||
|
||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
||||
let retrieved = manager.state_machine_exists(id);
|
||||
assert!(retrieved);
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
@@ -246,24 +165,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_remove() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
let local_session = mock_session_for_test();
|
||||
|
||||
let salt = [48u8; 32];
|
||||
let receiver_index: u32 = 2002;
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
local,
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
@@ -275,47 +180,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sessions() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let peer2 = random_peer(&mut rng);
|
||||
let peer3 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
let session1 = SessionsMock::mock_post_handshake(123).initiator;
|
||||
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||
let session3 = SessionsMock::mock_post_handshake(125).initiator;
|
||||
|
||||
let salt = [49u8; 32];
|
||||
|
||||
let sm_1 = manager
|
||||
.create_session_state_machine(
|
||||
3001,
|
||||
true,
|
||||
local.clone(),
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_2 = manager
|
||||
.create_session_state_machine(
|
||||
3002,
|
||||
true,
|
||||
local.clone(),
|
||||
peer2.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_3 = manager
|
||||
.create_session_state_machine(
|
||||
3003,
|
||||
true,
|
||||
local.clone(),
|
||||
peer3.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1 = manager.create_session_state_machine(session1);
|
||||
let sm_2 = manager.create_session_state_machine(session2);
|
||||
let sm_3 = manager.create_session_state_machine(session3);
|
||||
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
|
||||
@@ -330,24 +202,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_create_session() {
|
||||
let manager = SessionManager::new();
|
||||
let (init, resp) = mock_peers();
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let salt = [50u8; 32];
|
||||
let receiver_index: u32 = 4004;
|
||||
|
||||
let sm = manager.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init,
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
);
|
||||
|
||||
assert!(sm.is_ok());
|
||||
let sm = sm.unwrap();
|
||||
let sesion = mock_session_for_test();
|
||||
|
||||
let sm = manager.create_session_state_machine(sesion);
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
|
||||
+195
-847
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ use nyxd_scraper_shared::storage::helpers::log_db_operation_time;
|
||||
use nyxd_scraper_shared::storage::{NyxdScraperStorage, NyxdScraperStorageError};
|
||||
use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PostgresScraperStorage {
|
||||
@@ -22,7 +22,10 @@ pub struct PostgresScraperStorage {
|
||||
|
||||
impl PostgresScraperStorage {
|
||||
#[instrument]
|
||||
pub async fn init(connection_string: &str) -> Result<Self, PostgresScraperError> {
|
||||
pub async fn init(
|
||||
connection_string: &str,
|
||||
run_migrations: &bool,
|
||||
) -> Result<Self, PostgresScraperError> {
|
||||
debug!("initialising scraper database with '{connection_string}'",);
|
||||
|
||||
let connection_pool = match sqlx::PgPool::connect(connection_string).await {
|
||||
@@ -33,12 +36,13 @@ impl PostgresScraperStorage {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./sql_migrations")
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to initialize SQLx database: {err}");
|
||||
// return Err(err.into());
|
||||
if *run_migrations {
|
||||
if let Err(err) = sqlx::migrate!("./sql_migrations")
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
{
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Database migration finished!");
|
||||
@@ -192,8 +196,11 @@ impl PostgresScraperStorage {
|
||||
impl NyxdScraperStorage for PostgresScraperStorage {
|
||||
type StorageTransaction = PostgresStorageTransaction;
|
||||
|
||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError> {
|
||||
PostgresScraperStorage::init(storage)
|
||||
async fn initialise(
|
||||
storage: &str,
|
||||
run_migrations: &bool,
|
||||
) -> Result<Self, NyxdScraperStorageError> {
|
||||
PostgresScraperStorage::init(storage, run_migrations)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ pub struct Config {
|
||||
pub store_precommits: bool,
|
||||
|
||||
pub start_block: StartingBlockOpts,
|
||||
|
||||
pub run_migrations: bool,
|
||||
}
|
||||
|
||||
pub struct NyxdScraperBuilder<S> {
|
||||
@@ -161,7 +163,7 @@ where
|
||||
|
||||
pub async fn new(config: Config) -> Result<Self, ScraperError> {
|
||||
config.pruning_options.validate()?;
|
||||
let storage = S::initialise(&config.database_storage).await?;
|
||||
let storage = S::initialise(&config.database_storage, &config.run_migrations).await?;
|
||||
let rpc_client = RpcClient::new(&config.rpc_url)?;
|
||||
|
||||
Ok(NyxdScraper {
|
||||
|
||||
@@ -33,7 +33,10 @@ pub trait NyxdScraperStorage: Clone + Sized {
|
||||
type StorageTransaction: NyxdScraperTransaction;
|
||||
|
||||
/// Either connection string (postgres) or storage path (sqlite)
|
||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError>;
|
||||
async fn initialise(
|
||||
storage: &str,
|
||||
run_migrations: &bool,
|
||||
) -> Result<Self, NyxdScraperStorageError>;
|
||||
|
||||
async fn begin_processing_tx(
|
||||
&self,
|
||||
|
||||
@@ -207,7 +207,10 @@ impl SqliteScraperStorage {
|
||||
impl NyxdScraperStorage for SqliteScraperStorage {
|
||||
type StorageTransaction = SqliteStorageTransaction;
|
||||
|
||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError> {
|
||||
async fn initialise(
|
||||
storage: &str,
|
||||
_run_migrations: &bool,
|
||||
) -> Result<Self, NyxdScraperStorageError> {
|
||||
SqliteScraperStorage::init(storage)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
|
||||
@@ -18,7 +18,7 @@ rand_chacha = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-bin-common = { workspace = true, features = ["tracing"] }
|
||||
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -13,7 +13,7 @@ description = "Functions and tests for checking Nym's Credential Proxy is being
|
||||
|
||||
[dependencies]
|
||||
jwt-simple = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
|
||||
@@ -21,7 +21,7 @@ zeroize = { workspace = true }
|
||||
|
||||
nym-bin-common = { workspace = true }
|
||||
nym-http-api-client = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["form"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
Generated
+45
-17
@@ -265,9 +265,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
@@ -825,7 +825,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easy-addr"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"quote",
|
||||
@@ -1248,7 +1248,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a95dc43ef8954a4f79846e3224434cf389d4a9c14b77f526dae3cfd2221c6141"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1261,7 +1263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb3e8427c193cd500c802274b11879086863c3643525b6ece3e9ab1c77bddc"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1275,7 +1279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3628aac6715e844f3ab20e3b8ae8c4684f144ccb78e205f002c1c3ae375e956"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
@@ -1289,7 +1295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b710addc28c9950dd961e7dd3837ef3b479492d2b21b5f2437eb7d2899403027"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
@@ -1335,7 +1343,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d022e85291bf51877fbbf4688bc3762c605fdaee3b98c6407414f7c358bc5610"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1349,7 +1359,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb13102740426a4a2b683f54bbd6614fe9ecd745f5117bcf197c49c300b15edf"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -1384,7 +1396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c21bceb3bb8ee2789851b3f381fc035485af825bf7290b7c99a5af4e8f6ba1"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1404,7 +1418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a20b931ee849f6179ce2b387accd058720017f644ffbc8c2422f3e9ac3ff54"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1419,7 +1435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9834193b4641acdf9f360aea684a6bd841cad287930bc0d7c3241a133756464"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"regex",
|
||||
@@ -1427,7 +1445,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03077f9ebeb40caf8aa8e6f7bf8728449f73733e7a246986e492fa34ad3e70ab"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
@@ -1455,7 +1475,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42129a72f4b0dc0304a48b0ca1769b27694d913687ace5692d4c6924ca9f2a13"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1483,7 +1505,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70239cc26beda3ad19289188c50d554522af29646d7d3f855bda6fc8ed332fe7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1495,7 +1519,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba662d39fd6da9e13166fa1162ff41c2cfaed78a77c70df72fbda6fef5eb4f5"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
@@ -1524,7 +1550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "676c2793efbf9ccdf86bb788c903f778a2d5993a5174729303f9511a297f4ca8"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
|
||||
+20
-25
@@ -57,34 +57,25 @@ schemars = "0.8.16"
|
||||
|
||||
thiserror = "2.0.11"
|
||||
|
||||
# Common crates from parent workspace (paths relative to contracts/)
|
||||
#
|
||||
# TODO: Once these crates are published to crates.io, switch from path dependencies
|
||||
# to crates.io versions.
|
||||
#
|
||||
# TODO add a [patch.crates-io] section at the bottom for local development if you need to use a modded version of common import instead e.g.:
|
||||
#
|
||||
# [patch.crates-io]
|
||||
# nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
#
|
||||
easy-addr = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
nym-coconut-dkg-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-contracts-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
nym-crypto = { version = "1.20.1", path = "../common/crypto", default-features = false }
|
||||
nym-ecash-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-group-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-mixnet-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.1", path = "../common/network-defaults", default-features = false }
|
||||
nym-performance-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-pool-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/nym-pool-contract" }
|
||||
nym-vesting-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
# For local development with modifications, add a [patch.crates-io] section (see bottom of file)
|
||||
nym-coconut-dkg-common = "1.20.4"
|
||||
nym-contracts-common = "1.20.4"
|
||||
nym-contracts-common-testing = "1.20.4"
|
||||
nym-crypto = { version = "1.20.4", default-features = false }
|
||||
nym-ecash-contract-common = "1.20.4"
|
||||
nym-group-contract-common = "1.20.4"
|
||||
nym-mixnet-contract-common = "1.20.4"
|
||||
nym-multisig-contract-common = "1.20.4"
|
||||
nym-network-defaults = { version = "1.20.4", default-features = false }
|
||||
nym-performance-contract-common = "1.20.4"
|
||||
nym-pool-contract-common = "1.20.4"
|
||||
nym-vesting-contract-common = "1.20.4"
|
||||
|
||||
# Aliases for crates that some contracts import under different names
|
||||
contracts-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
|
||||
contracts-common = { version = "1.20.4", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.4", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.4", package = "nym-vesting-contract-common" }
|
||||
|
||||
# Internal contract workspace members (for cross-contract testing)
|
||||
cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
|
||||
@@ -101,3 +92,7 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
# For local development, import via path instead of crates.io, e.g.
|
||||
# [patch.crates-io]
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
+17
-5
@@ -9,14 +9,26 @@ This version is defined in the `[workspace.package]` section of the root monorep
|
||||
## When Developing
|
||||
If you add a workspace dependency to the SDK when developing, make sure to add this to the workspace dependencies in the root monorepo `Cargo.toml`.
|
||||
|
||||
## Publishing
|
||||
## Check local publication
|
||||
```
|
||||
# List crates to publish
|
||||
cargo workspaces list
|
||||
|
||||
# Dry run - check for compilation or other problems
|
||||
# Dry run locally - check for compilation or other problems
|
||||
cargo workspaces publish --no-git-commit --dry-run
|
||||
|
||||
# Publish
|
||||
TODO
|
||||
```
|
||||
|
||||
## CI
|
||||
There are several workflows that should be run in the following order:
|
||||
- `ci-crates-publish-dry-run`: **run this first!** This is a remote dry-run on a runner. This greps for any errors that would be a problem when we're not dry-running. It doesn't catch all errors, as `dry-run` has a known issue where, assuming that 2 new crates are being uploaded, and crate B relies on crate A, if crate A isn't on crates.io (which it won't be, since you're dry-running publication), then since `cargo workspaces publish` only checks for available versions on crates.io, it will error. We don't want the CI to fail in that case.
|
||||
- `ci-crates-version-bump`: this bumps the versions of the workspace + dependencies to the passed version, and then commits the change. This is its own CI job so that we don't get into sticky situations whereby the version bump + commit happens, but the publication step fails.
|
||||
- `ci-crates-publish`: this publishes the crates. So long as you're not uploading more than 5 new crates, pass `60` as the `publish_interval`. This is to get around [crates.io rate limiting](https://github.com/rust-lang/crates.io/blob/ad7e58e1afd65b9137e58a7bca3e1fb7f5546682/src/rate_limiter.rs#L24). Pass the Github handle of whoever should be the backup author of the crate for security redundency (see the section below) as the second arg.
|
||||
|
||||
> There is also `ci-crates-publish-resume` which is there in case a publication run fails and needs to be restarted part way through the list of unbumped/unpublished crates. Pass the previously bumped/published crate in the list output of `cargo workspaces list`
|
||||
|
||||
## Crates.io Authors
|
||||
Since Github teams have [limited ownership / mod rights](https://doc.rust-lang.org/cargo/reference/publishing.html#cargo-owner) of crates, and we cannot create a `CARGO_REGISTRY_TOKEN` on behalf of the Nym Github org. As such, we are currently using personal cargo tokens generated by team members (currently Max), and adding the Nym Github org as an owner in the CI job.
|
||||
|
||||
However, since the Github org cannot add or modify owners, are also adding a second user author as a redundency, on the offchance that Max loses access to his Crates.io / Github account, gets struck by lightning, etc. This is the author passed as the second argument to the `ci-crates-publish` CI, and if none is passed, defaults to [https://github.com/jstuczyn](https://github.com/jstuczyn) since he is the Github org owner.
|
||||
|
||||
Authors can also be changed by running `scripts/add-crates-owners.sh`.
|
||||
|
||||
+12
-14
@@ -2,7 +2,7 @@
|
||||
|
||||
This is v2 of the nym docs, condensed from various mdbooks projects that we had previously.
|
||||
|
||||
These docs are hosted at [nymtech.net/docs](www.nymtech.net/docs).
|
||||
These docs are hosted at [nym.com/docs](https://nym.com/docs).
|
||||
|
||||
## Doc projects
|
||||
`docs/pages/` contains several subdirs, each hosting a subsection of the docs:
|
||||
@@ -11,6 +11,7 @@ These docs are hosted at [nymtech.net/docs](www.nymtech.net/docs).
|
||||
* `operators` contains node setup and maintenance guides.
|
||||
|
||||
## Local development
|
||||
|
||||
### Dependencies
|
||||
Our `prebuild` script relies on the following:
|
||||
- `python`
|
||||
@@ -21,13 +22,10 @@ Our `prebuild` script relies on the following:
|
||||
|
||||
Otherwise make sure to have `node` installed.
|
||||
|
||||
#### Binary dependencies
|
||||
If you don't want/need build our binaries in Rust localy you can download them from latest Release page on [github](https://github.com/nymtech/nym/releases) into target/release in root of this repo.
|
||||
{'nym-node', 'nym-api', 'nymvisor'}
|
||||
|
||||
Make them executable
|
||||
```bash
|
||||
chmod +x target/release/*
|
||||
### Link checking (optional)
|
||||
We use [lychee](https://github.com/lycheeverse/lychee) to check for broken links. Install via your package manager or `cargo install lychee`, then run:
|
||||
```sh
|
||||
lychee documentation/docs/ --config lychee.toml --root-dir documentation/docs/pages/
|
||||
```
|
||||
|
||||
### Serve Local (Hot Reload)
|
||||
@@ -43,27 +41,27 @@ Open `http://localhost:3000`.
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
The static output will be in `./out`;
|
||||
|
||||
## Contribution
|
||||
* If you wish to add to the documentation please create a PR against this repo, with a `patch` against `develop`.
|
||||
|
||||
## Scripts
|
||||
* There are several autogenerated command files for clients and binaries. These are generated with `generate:commands`, which runs the `autodoc` rust binary, moves the files to their required places, and then if there is an update, commits them to git. This is for remote deployments.
|
||||
* Python scripts TODO
|
||||
* `generate:commands`: generates command output files for clients and binaries. This script runs the `autodoc` rust binary, moves the files to their required places, and then if there is an update, commits them to git. We commit the files as our remote deployments pull from a git repo. **Only run this script on branches where you want to push e.g. the build info of a binary to production docs**; it will build the monorepo binaries and use their command output for the produced markdown files.
|
||||
* `generate:tables`: generates various information tables containing some repo-wide variables and information about ISPs.
|
||||
|
||||
### Autodoc
|
||||
`autodoc` is a script that generates markdown files containing commands and their output (both command and `--help` output). For the moment the binaries and their commands are manually configured in the script.
|
||||
|
||||
> **Only run this script on branches where you want to push e.g. the build info of a binary to production docs**; it will build the monorepo binaries and use their command output for the produced markdown files.
|
||||
|
||||
## CI/CD
|
||||
TODO
|
||||
- **Link checking**: Runs on every push to `documentation/docs/` via `.github/workflows/ci-docs-linkcheck.yml`
|
||||
|
||||
## Licensing and copyright information
|
||||
This is a monorepo and components that make up Nym as a system are licensed individually, so for accurate information, please check individual files.
|
||||
|
||||
As a general approach, licensing is as follows this pattern:
|
||||
|
||||
* <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://nymtech.net/docs">Nym Documentation</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://nymtech.net">Nym Technologies</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>
|
||||
* <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://nym.com/docs">Nym Documentation</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://nym.com">Nym Technologies</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>
|
||||
|
||||
* Nym applications and binaries are [GPL-3.0-only](https://www.gnu.org/licenses/)
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# Nym Docs v2
|
||||
|
||||
This is v2 of the nym docs, condensed from various mdbooks projects that we had previously.
|
||||
|
||||
These docs are hosted at [nym.com/docs](https://nym.com/docs).
|
||||
|
||||
## Doc projects
|
||||
`docs/pages/` contains several subdirs, each hosting a subsection of the docs:
|
||||
* `network` contains key concepts, cryptosystems, architecture.
|
||||
* `developers` contains key concepts for developers, required architecture, and Rust/Typescript SDK docs.
|
||||
* `operators` contains node setup and maintenance guides.
|
||||
|
||||
## Local development
|
||||
|
||||
### Dependencies
|
||||
Our `prebuild` script relies on the following:
|
||||
- `python`
|
||||
- `pip`
|
||||
- [`pandas`](https://pandas.pydata.org/)
|
||||
- [`tabulate`](https://pypi.org/project/tabulate/)
|
||||
- `jq`
|
||||
|
||||
Otherwise make sure to have `node` installed.
|
||||
|
||||
### Serve Local (Hot Reload)
|
||||
```sh
|
||||
pnpm i
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:3000`.
|
||||
|
||||
## Build
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Contribution
|
||||
* If you wish to add to the documentation please create a PR against this repo, with a `patch` against `develop`.
|
||||
|
||||
## Scripts
|
||||
* `generate:commands`: generates command output files for clients and binaries. This script runs the `autodoc` rust binary, moves the files to their required places, and then if there is an update, commits them to git. We commit the files as our remote deployments pull from a git repo. **Only run this script on branches where you want to push e.g. the build info of a binary to production docs**; it will build the monorepo binaries and use their command output for the produced markdown files.
|
||||
* `generate:tables`: generates various information tables containing some repo-wide variables and information about ISPs.
|
||||
|
||||
### Autodoc
|
||||
`autodoc` is a script that generates markdown files containing commands and their output (both command and `--help` output). For the moment the binaries and their commands are manually configured in the script.
|
||||
|
||||
> **Only run this script on branches where you want to push e.g. the build info of a binary to production docs**; it will build the monorepo binaries and use their command output for the produced markdown files.
|
||||
|
||||
## CI/CD
|
||||
TODO
|
||||
|
||||
## Licensing and copyright information
|
||||
This is a monorepo and components that make up Nym as a system are licensed individually, so for accurate information, please check individual files.
|
||||
|
||||
As a general approach, licensing is as follows this pattern:
|
||||
|
||||
* <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://nym.com/docs">Nym Documentation</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://nym.com">Nym Technologies</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>
|
||||
|
||||
* Nym applications and binaries are [GPL-3.0-only](https://www.gnu.org/licenses/)
|
||||
|
||||
* Used libraries and different components are [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) or [MIT](https://mit-license.org/)
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "174592308324894"
|
||||
"amount": "172542733168986"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "825407691675106"
|
||||
"amount": "827457266831014"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
825_407_691
|
||||
827_457_266
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4_849
|
||||
4_792
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
252_536
|
||||
253_163
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_608_795
|
||||
60_759_293
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_608_794
|
||||
60_759_292
|
||||
|
||||
+3
-3
@@ -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 | 174_592_308 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 172_542_733 |
|
||||
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 825_407_691 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 252_536 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 827_457_266 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 253_163 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "174592308324894.719164125613282609",
|
||||
"staking_supply": "60608794397902.33807512267258462",
|
||||
"reward_pool": "172542733168986.886192478589145223",
|
||||
"staking_supply": "60759292488059.481869771104188242",
|
||||
"staking_supply_scale_factor": "0.07342892",
|
||||
"epoch_reward_budget": "4849786342.358186643447933702",
|
||||
"stake_saturation_point": "252536643324.593075313011135769",
|
||||
"epoch_reward_budget": "4792853699.138524616457738587",
|
||||
"stake_saturation_point": "253163718700.247841124046267451",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Wednesday, January 28th 2026, 09:28:07 UTC
|
||||
Wednesday, February 11th 2026, 11:35:05 UTC
|
||||
|
||||
@@ -11,7 +11,7 @@ options:
|
||||
--no_routing_history Display node stats without routing history
|
||||
--no_verloc_metrics Display node stats without verloc metrics
|
||||
-m, --markdown Display results in markdown format
|
||||
-o [OUTPUT], --output [OUTPUT]
|
||||
-o, --output [OUTPUT]
|
||||
Save results to file (in current dir or supply with
|
||||
path without filename)
|
||||
```
|
||||
|
||||
@@ -12,8 +12,7 @@ usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v]
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-V, --version show program's version number and exit
|
||||
-d BRANCH, --dev BRANCH
|
||||
Define github branch (default: develop)
|
||||
-d, --dev BRANCH Define github branch (default: develop)
|
||||
-v, --verbose Show full error tracebacks
|
||||
--mode {mixnode,entry-gateway,exit-gateway}
|
||||
Node mode: 'mixnode', 'entry-gateway', or 'exit-
|
||||
|
||||
@@ -62,6 +62,8 @@ Options:
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE>
|
||||
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
@@ -80,6 +82,8 @@ Options:
|
||||
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
|
||||
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
|
||||
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
|
||||
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "15.5.9",
|
||||
"next": "15.5.10",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Validator REST API
|
||||
|
||||
Since the [Nyx validators](../../operators/nodes/validator-setup) are built with the Cosmos SDK, they by default expose a [REST API](https://docs.cosmos.network/api) which can be used to query the state of the chain.
|
||||
Since the [Nyx validators](/operators/nodes/validator-setup) are built with the Cosmos SDK, they by default expose a [REST API](https://docs.cosmos.network/api) which can be used to query the state of the chain.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# NymAPI
|
||||
|
||||
The [NymAPI](../../operators/nodes/validator-setup/nym-api) is a sidecar binary operated by members of the Nym Validator set. Amongst other things, the API offers endpoints to query regarding circulating `NYM` supply, and global and ticketbook-specific [zk-nym](../../network/cryptography/zk-nym) data. This is all information contained in various smart contracts on the Nyx blockchain; the NymAPI caches this information periodically to make queries faster and more scalable.
|
||||
The [NymAPI](/operators/nodes/validator-setup/nym-api) is a sidecar binary operated by members of the Nym Validator set. Amongst other things, the API offers endpoints to query regarding circulating `NYM` supply, and global and ticketbook-specific [zk-nym](/network/cryptography/zk-nym) data. This is all information contained in various smart contracts on the Nyx blockchain; the NymAPI caches this information periodically to make queries faster and more scalable.
|
||||
|
||||
The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/nym-api).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
```admonish warning
|
||||
Since the beginning of 2024 NymConnect is no longer maintained. Nym is developing a new client called [NymVPN](https://nymvpn.com), an application routing all users traffic thorugh the mixnet.
|
||||
If users want to route their traffic through socks5 we advice to use maintained [Nym Socks5 Client](../clients/socks5/setup.md).
|
||||
If users want to route their traffic through socks5 we advice to use maintained [Nym Socks5 Client](../clients/socks5/setup).
|
||||
```
|
||||
|
||||
In case you want to run deprecated NymConnect, follow these steps:
|
||||
@@ -27,7 +27,7 @@ Here are some examples of applications which will work behind Socks5 proxy (`nym
|
||||
|
||||
To download Electrum visit the [official webpage](https://electrum.org/#download). To connect to the Mixnet follow these steps:
|
||||
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup.md))
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup))
|
||||
2. Start your Electrum Bitcoin wallet
|
||||
3. Go to: *Tools* -> *Network* -> *Proxy*
|
||||
4. Set *Use proxy* to ✅, choose `SOCKS5` from the drop-down and add (copy-paste) the values from your NymConnect application
|
||||
@@ -39,7 +39,7 @@ To download Electrum visit the [official webpage](https://electrum.org/#download
|
||||
|
||||
To download Monero wallet visit [getmonero.org](https://www.getmonero.org/downloads/). To connect to the Mixnet follow these steps:
|
||||
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup.md))
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup))
|
||||
2. Start your Monero wallet
|
||||
3. Go to: *Settings* -> *Interface* -> *Socks5 proxy* -> Add values: IP address `127.0.0.1`, Port `1080` (the values copied from NymConnect)
|
||||
5. Now your Monero wallet runs through the Mixnet and it will be connected only if your NymConnect or `nym-socks5-client` are connected.
|
||||
@@ -49,7 +49,7 @@ To download Monero wallet visit [getmonero.org](https://www.getmonero.org/downlo
|
||||
|
||||
To download Element (chat client for Matrix) visit [element.io](https://element.io/download). To connect to the Mixnet follow these steps:
|
||||
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup.md))
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup))
|
||||
2. Start `element-desktop` with `--proxy-server` argument:
|
||||
|
||||
**Linux**
|
||||
@@ -68,7 +68,7 @@ To make the start of Element over NymConnect simplier, you can add this command
|
||||
|
||||
### Telegram via NymConnect
|
||||
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup.md))
|
||||
1. Start and connect NymConnect (or [`nym-socks5-client`](../clients/socks5/setup))
|
||||
2. Start your Telegram chat application
|
||||
3. Open the Telegram proxy settings.
|
||||
- Linux: *Settings* -> *Advanced* -> *Connection type* -> *Use custom proxy*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Interacting with Nyx Chain and Smart Contracts
|
||||
|
||||
There are two options for interacting with the blockchain to send tokens or interact with deployed smart contracts:
|
||||
* [`Nym-CLI`](../tools/nym-cli.md) tool
|
||||
* [`Nym-CLI`](./tools/nym-cli) tool
|
||||
* `nyxd` binary
|
||||
|
||||
## Nym-CLI tool (recommended in most cases)
|
||||
|
||||
@@ -28,7 +28,7 @@ Before you can use the client, you need to initalise a new instance of it, which
|
||||
|
||||
The `--id` in the example above is a local identifier so that you can name your clients and keep track of them on your local system; it is **never** transmitted over the network.
|
||||
|
||||
The `--use-reply-surbs` field denotes whether you wish to send [SURBs](../../network/concepts/anonymous-replies) along with your request. It defaults to `false`, we are explicitly setting it as `true`. It defaults to `false` for compatibility with versions of the pre-smoosh `nym-network-requester` binary which will soon be deprecated.
|
||||
The `--use-reply-surbs` field denotes whether you wish to send [SURBs](/network/concepts/anonymous-replies) along with your request. It defaults to `false`, we are explicitly setting it as `true`. It defaults to `false` for compatibility with versions of the pre-smoosh `nym-network-requester` binary which will soon be deprecated.
|
||||
|
||||
The `--provider` field needs to be filled with the Nym address of an Exit Gateway that can make network requests on your behalf. You can select one from the [mixnet explorer](https://nym.com/explorer) by copying its `Client ID` and using this as the value of the `--provider` flag. Alternatively, you could use [Harbourmaster](https://harbourmaster.nymtech.net/).
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ When trying to connect your app, generally the proxy settings are found in `sett
|
||||
|
||||
Here is an example of setting the proxy connecting in Blockstream Green:
|
||||
|
||||

|
||||

|
||||
|
||||
Most wallets and other applications will work basically the same way: find the network proxy settings, enter the proxy url (host: **localhost**, port: **1080**).
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ In some applications, e.g. where people are chatting with friends who they know,
|
||||
|
||||
**If that fits your security model, good. However, will probably be the case that you want to send anonymous replies using Single Use Reply Blocks (SURBs)**.
|
||||
|
||||
You can read more about SURBs [here](../../network/concepts/anonymous-replies) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - **without them having to know your client address**.
|
||||
You can read more about SURBs [here](/network/concepts/anonymous-replies) but in short they are ways for the receiver of this message to anonymously reply to you - the sender - **without them having to know your client address**.
|
||||
|
||||
Your client will send along a number of `replySurbs` to the recipient of the message. These are pre-addressed Sphinx packets that the recipient can write to the payload of (i.e. write response data to), but not view the final destination of. If the recipient is unable to fit the response data into the bucket of SURBs sent to it, it will use a SURB to request more SURBs be sent to it from your client.
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ However, this is not true.
|
||||
|
||||
**This will only block until the message is put into client's internal queue**. Therefore in the above example, the client is being shut down before the message is _actually sent to the mixnet_; after being placed in the client's internal queue, there is still work to be done under the hood, such as route encrypting the message and placing it amongst the stream of cover traffic.
|
||||
|
||||
The simple solution? Make sure the program/client stays active, either by calling `sleep`, or listening out for new messages. As sending a one-shot message without listening out for a response is likely not what you'll be doing, then you will be then awaiting a response (see the [message helpers page](./message-helpers.md) for an example of this).
|
||||
The simple solution? Make sure the program/client stays active, either by calling `sleep`, or listening out for new messages. As sending a one-shot message without listening out for a response is likely not what you'll be doing, then you will be then awaiting a response (see the [message helpers page](./message-helpers) for an example of this).
|
||||
|
||||
Furthermore, you should always **manually disconnect your client** with `client.disconnect().await` as seen in the code examples. This is important as your client is writing to a local DB and dealing with SURB storage.
|
||||
|
||||
@@ -122,7 +122,7 @@ You might however be receiving messages without data attached to them / empty pa
|
||||
|
||||
Whether the `data` of a SURB request being empty is a feature or a bug is to be decided - there is some discussion surrounding whether we can use SURB requests to send additional data to streamline the process of sending large replies across the mixnet.
|
||||
|
||||
You can find a few helper functions [here](./message-helpers.md) to help deal with this issue in the meantime.
|
||||
You can find a few helper functions [here](./message-helpers) to help deal with this issue in the meantime.
|
||||
|
||||
> If you can think of a more succinct or different way of handling this do reach out - we're happy to hear other opinions
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user