Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db64706a1f | |||
| aac983d922 | |||
| 577675bab3 | |||
| ec015618cd | |||
| fa40acbeca | |||
| 386e1790dd | |||
| d07f9c8fad | |||
| 0dc071daeb | |||
| 10951d4cd3 | |||
| 872c25bfcc | |||
| 5acce42c64 | |||
| 4848d081d0 | |||
| b3452ede77 | |||
| a44819b14c | |||
| 5455110810 | |||
| a0178d28f7 | |||
| 3e42160426 | |||
| 2f752a6c42 | |||
| 806f807f02 | |||
| 1400db6156 | |||
| 673a3e45d3 | |||
| d9c2f6ebda | |||
| e24e094711 | |||
| 378f32e6d7 | |||
| 3e9b8d237f | |||
| f5a4dbc555 | |||
| 4480534e4d | |||
| d355f9d752 | |||
| 9f3a370496 | |||
| e4adc5d954 | |||
| 00373b70e2 | |||
| 65f2017422 | |||
| 192f258463 | |||
| a5eee7444b | |||
| 6abd7e7352 | |||
| 3306ca5357 | |||
| 9c2ccead0e | |||
| b7aeb51362 | |||
| e9e725defe | |||
| c74494a21d | |||
| 54f6c98c22 | |||
| 846484bbb4 | |||
| fb3f5501ba | |||
| e8a607f520 | |||
| f5f6df9eaf | |||
| c647ab5605 | |||
| 416c21a42e | |||
| fd5a95fa4d | |||
| c61df79182 | |||
| 08559a7660 | |||
| 6dce55a99b | |||
| bc0b89b31c | |||
| 67c32faa11 | |||
| aa0d15ee67 | |||
| 4f0974fcf1 | |||
| 17d11f201e | |||
| fef7e42eb4 | |||
| ceeeb6211b | |||
| cd77b1032f | |||
| 6bbb14f12f | |||
| de8030d85a | |||
| e18e64bf21 | |||
| a50c9ac3fb | |||
| 2235a6e147 | |||
| db6defa122 | |||
| df7768dec0 | |||
| f3a449b7cc | |||
| cf21593ffa | |||
| f0d8dabb9f | |||
| f105bcbafe | |||
| dc0f4af2c1 | |||
| 2a621e07a8 | |||
| 485aeebabd | |||
| 3b726bada9 | |||
| 1d1b2e17d2 | |||
| b5b2dbdfd8 | |||
| 82806f47d8 | |||
| c6f85cf23e | |||
| ed8de7234d | |||
| e25d83b047 | |||
| 9974d480b5 | |||
| 2211f13cdd | |||
| 4505f18a02 | |||
| a717a18948 |
@@ -8,10 +8,13 @@ on:
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
- 'nym-authenticator-client/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- 'nym-ip-packet-client/**'
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
- 'nym-node-status-api/**'
|
||||
- 'nym-registration-client/**'
|
||||
- 'nym-statistics-api/**'
|
||||
- 'nym-outfox/**'
|
||||
- 'nym-validator-rewarder/**'
|
||||
|
||||
@@ -6,16 +6,14 @@ on:
|
||||
paths:
|
||||
- "ts-packages/**"
|
||||
- "sdk/typescript/**"
|
||||
- "nym-connect/desktop/src/**"
|
||||
- "nym-connect/desktop/package.json"
|
||||
- "nym-wallet/src/**"
|
||||
- "nym-wallet/package.json"
|
||||
- "explorer/**"
|
||||
- "explorer-v2/**"
|
||||
- ".github/workflows/ci-lint-typescript.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: arc-linux-latest
|
||||
env:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
@@ -25,6 +23,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
@@ -37,14 +36,12 @@ jobs:
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '116'
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23.7"
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Install
|
||||
run: yarn
|
||||
@@ -52,7 +49,11 @@ jobs:
|
||||
- name: Build packages
|
||||
run: yarn build:ci
|
||||
|
||||
- name: Install again
|
||||
run: yarn
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Typecheck with tsc
|
||||
run: yarn tsc
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: custom-linux
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -17,10 +17,13 @@ jobs:
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust stable
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
@@ -29,9 +32,9 @@ jobs:
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23.7"
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
@@ -3,11 +3,6 @@ name: Build and upload Node Status agent container to harbor.nymte.ch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gateway_probe_git_ref:
|
||||
type: string
|
||||
default: nym-vpn-core-v1.4.0
|
||||
required: true
|
||||
description: Which gateway probe git ref to build the image with
|
||||
release_image:
|
||||
description: 'Tag image as a release'
|
||||
required: true
|
||||
@@ -43,16 +38,6 @@ jobs:
|
||||
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: cleanup-gateway-probe-ref
|
||||
id: cleanup_gateway_probe_ref
|
||||
run: |
|
||||
GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }}
|
||||
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
|
||||
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Initialize RELEASE_TAG
|
||||
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||
|
||||
@@ -61,24 +46,12 @@ jobs:
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: New env vars
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
# - name: Remove existing tag if exists
|
||||
# run: |
|
||||
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
|
||||
# git push --delete origin $${{ env.GIT_TAG }}
|
||||
# git tag -d $${{ env.GIT_TAG }}
|
||||
# fi
|
||||
|
||||
# - name: Create tag
|
||||
# run: |
|
||||
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
|
||||
# git push origin $${{ env.GIT_TAG }}
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
- name: BuildAndPushImageOnHarbor
|
||||
run: |
|
||||
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
@@ -63,4 +63,3 @@ nym-api/redocly/formatted-openapi.json
|
||||
|
||||
**/settings.sql
|
||||
**/enter_db.sh
|
||||
.beads
|
||||
@@ -4,6 +4,46 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.19-kase] (2025-10-30)
|
||||
|
||||
- update ns agent workflow ([#6154])
|
||||
- Cherry pick - request #6143 from nymtech/bugfix/mix-tx-closed-v2 ([#6153])
|
||||
- bugfix: nym-credential-proxy query params parsing regression ([#6121])
|
||||
- bugfix: revert some dep updates introduced in #6043 ([#6120])
|
||||
- Skip ipv6 metadata endpoint request ([#6118])
|
||||
- update to no longer use 1mb files ([#6117])
|
||||
- chore: restore pending dkg contract state migration ([#6116])
|
||||
- Revert "Propagate cancel token to mixnet client" ([#6115])
|
||||
- Update dirs to 6.0 ([#6109])
|
||||
- Propagate cancel token to mixnet client ([#6105])
|
||||
- bugfix: retrieve and update ticketbook in the same query ([#6101])
|
||||
- bugfix: include network name in the default gateway probe config path ([#6100])
|
||||
- Bugfix/incompatibility fixes ([#6099])
|
||||
- [DOCs/operators] QUIC deployment script & docs ([#6098])
|
||||
- bugfix: testnet manager 02sql migration ([#6096])
|
||||
- feat: move gateway probe to monorepo (and update to rust edition 2024) ([#6094])
|
||||
- bugfix: use custom topology provider for list of init gateways ([#6092])
|
||||
- Max/fix wasm client + build commands ([#6043])
|
||||
|
||||
[#6154]: https://github.com/nymtech/nym/pull/6154
|
||||
[#6153]: https://github.com/nymtech/nym/pull/6153
|
||||
[#6121]: https://github.com/nymtech/nym/pull/6121
|
||||
[#6120]: https://github.com/nymtech/nym/pull/6120
|
||||
[#6118]: https://github.com/nymtech/nym/pull/6118
|
||||
[#6117]: https://github.com/nymtech/nym/pull/6117
|
||||
[#6116]: https://github.com/nymtech/nym/pull/6116
|
||||
[#6115]: https://github.com/nymtech/nym/pull/6115
|
||||
[#6109]: https://github.com/nymtech/nym/pull/6109
|
||||
[#6105]: https://github.com/nymtech/nym/pull/6105
|
||||
[#6101]: https://github.com/nymtech/nym/pull/6101
|
||||
[#6100]: https://github.com/nymtech/nym/pull/6100
|
||||
[#6099]: https://github.com/nymtech/nym/pull/6099
|
||||
[#6098]: https://github.com/nymtech/nym/pull/6098
|
||||
[#6096]: https://github.com/nymtech/nym/pull/6096
|
||||
[#6094]: https://github.com/nymtech/nym/pull/6094
|
||||
[#6092]: https://github.com/nymtech/nym/pull/6092
|
||||
[#6043]: https://github.com/nymtech/nym/pull/6043
|
||||
|
||||
## [2025.18-jarlsberg] (2025-10-14)
|
||||
|
||||
- ns-api: add descriptions to dVPN gateway responses ([#6102])
|
||||
|
||||
@@ -12,16 +12,6 @@ Nym is a privacy platform that uses mixnet technology to protect against metadat
|
||||
- Validators for network consensus
|
||||
- Various service providers and integrations
|
||||
|
||||
## Navigation Aids
|
||||
|
||||
This repository includes comprehensive navigation documents for efficient code exploration:
|
||||
|
||||
- **[CODEMAP.md](./CODEMAP.md)**: Structural overview of the entire repository with directory hierarchy, package descriptions, and navigation hints. Use this to quickly understand the codebase layout and find specific components.
|
||||
|
||||
- **[FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md)**: Comprehensive catalog of key functions, signatures, and API patterns across all major modules. Use this to quickly find available functions and understand their usage patterns.
|
||||
|
||||
When working with this codebase, start by consulting these documents to understand the structure and available APIs before diving into specific files.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Rust Components
|
||||
@@ -160,7 +150,7 @@ dotenv -f envs/sandbox.env -- cargo run -p nym-api
|
||||
|
||||
## Architecture
|
||||
|
||||
The Nym platform consists of various components organized as a monorepo. For a detailed structural overview with directory hierarchy and navigation hints, see [CODEMAP.md](./CODEMAP.md).
|
||||
The Nym platform consists of various components organized as a monorepo:
|
||||
|
||||
1. **Core Mixnet Infrastructure**:
|
||||
- `nym-node`: Core binary supporting mixnode and gateway modes
|
||||
@@ -432,8 +422,6 @@ The system uses SQLite databases with tables like:
|
||||
|
||||
## Development Workflows
|
||||
|
||||
**Note**: Before diving into specific workflows, consult [CODEMAP.md](./CODEMAP.md) to understand the repository structure and [FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md) to discover available APIs and functions.
|
||||
|
||||
### Running a Node
|
||||
|
||||
To run the mixnode or gateway:
|
||||
@@ -462,8 +450,6 @@ To monitor the health of your node:
|
||||
|
||||
## Common Libraries
|
||||
|
||||
For a comprehensive catalog of functions and APIs available in these libraries, see [FUNCTION_LEXICON.md](./FUNCTION_LEXICON.md).
|
||||
|
||||
- `common/types`: Shared data types across all components
|
||||
- `common/crypto`: Cryptographic primitives and wrappers
|
||||
- `common/client-core`: Core client functionality
|
||||
|
||||
-452
@@ -1,452 +0,0 @@
|
||||
# Nym Repository Codemap
|
||||
<!-- AIDEV-NOTE: This codemap provides structural navigation for the Nym privacy platform monorepo -->
|
||||
<!-- Last updated: 2024-10-22 (branch: drazen/lp-reg) -->
|
||||
|
||||
## Quick Navigation Index
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| [Main Executables](#main-executables) | Root directories | Core binaries and services |
|
||||
| [Client Implementations](#client-implementations) | `/clients/` | Various client types |
|
||||
| [Common Libraries](#common-libraries) | `/common/` | 70+ shared modules |
|
||||
| [Smart Contracts](#smart-contracts) | `/contracts/` | CosmWasm contracts |
|
||||
| [SDKs](#sdks) | `/sdk/` | Multi-language SDKs |
|
||||
| [WASM Modules](#wasm-modules) | `/wasm/` | Browser implementations |
|
||||
| [Service Providers](#service-providers) | `/service-providers/` | Exit nodes & routers |
|
||||
| [Tools](#tools-and-utilities) | `/tools/` | CLI tools & utilities |
|
||||
| [Configuration](#configuration-and-environments) | `/envs/` | Environment configs |
|
||||
|
||||
## Repository Structure Overview
|
||||
|
||||
```
|
||||
nym/
|
||||
├── Cargo.toml # Workspace manifest (170+ members)
|
||||
├── Cargo.lock # Locked dependencies
|
||||
├── Makefile # Build automation
|
||||
├── CLAUDE.md # Development guidelines
|
||||
├── envs/ # Environment configurations
|
||||
│ ├── local.env # Local development
|
||||
│ ├── sandbox.env # Test network
|
||||
│ ├── mainnet.env # Production
|
||||
│ └── canary.env # Pre-release
|
||||
├── assets/ # Images, logos, fonts
|
||||
├── docker/ # Docker configurations
|
||||
└── scripts/ # Deployment & setup scripts
|
||||
```
|
||||
|
||||
<!-- AIDEV-NOTE: Navigation hint - Use envs/ for network-specific configurations -->
|
||||
|
||||
## Main Executables
|
||||
|
||||
### Core Network Nodes
|
||||
|
||||
#### **nym-node** (v1.19.0) - Universal Node Binary
|
||||
- **Path**: `/nym-node/`
|
||||
- **Entry**: `src/main.rs`
|
||||
- **Modes**: `mixnode`, `gateway`
|
||||
- **Key Modules**:
|
||||
- `cli/` - Command-line interface
|
||||
- `config/` - Configuration management
|
||||
- `node/` - Core node logic
|
||||
- `wireguard/` - WireGuard VPN integration
|
||||
- `throughput_tester/` - Performance testing
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - nym-node replaces legacy gateway and mixnode binaries -->
|
||||
|
||||
#### **nym-api** - Network API Server
|
||||
- **Path**: `/nym-api/`
|
||||
- **Entry**: `src/main.rs`
|
||||
- **Database**: PostgreSQL with SQLx
|
||||
- **Migrations**: `/migrations/` (25+ migration files)
|
||||
- **Key Subsystems**:
|
||||
- `circulating_supply_api/` - Token supply tracking
|
||||
- `ecash/` - E-cash credential management
|
||||
- `epoch_operations/` - Epoch advancement
|
||||
- `network_monitor/` - Health monitoring
|
||||
- `node_performance/` - Performance metrics
|
||||
- `nym_nodes/` - Node registry
|
||||
|
||||
#### **gateway** (Legacy, v1.1.36)
|
||||
- **Path**: `/gateway/`
|
||||
- **Status**: Being phased out for nym-node
|
||||
- **New**: `src/node/lp_listener/` (branch: drazen/lp-reg)
|
||||
|
||||
### Supporting Services
|
||||
|
||||
| Service | Path | Purpose |
|
||||
|---------|------|---------|
|
||||
| `nym-network-monitor` | `/nym-network-monitor/` | Network reliability testing |
|
||||
| `nym-validator-rewarder` | `/nym-validator-rewarder/` | Reward calculation |
|
||||
| `nyx-chain-watcher` | `/nyx-chain-watcher/` | Blockchain monitoring |
|
||||
| `nym-credential-proxy` | `/nym-credential-proxy/` | Credential services |
|
||||
| `nym-statistics-api` | `/nym-statistics-api/` | Statistics aggregation |
|
||||
| `nym-node-status-api` | `/nym-node-status-api/` | Node status tracking |
|
||||
|
||||
## Client Implementations
|
||||
|
||||
### Directory: `/clients/`
|
||||
|
||||
```
|
||||
clients/
|
||||
├── native/ # Native Rust client
|
||||
│ └── websocket-requests/ # WebSocket protocol
|
||||
├── socks5/ # SOCKS5 proxy client
|
||||
├── validator/ # Blockchain validator client
|
||||
└── webassembly/ # Browser-based client
|
||||
```
|
||||
|
||||
<!-- AIDEV-NOTE: Pattern reference - All clients use common/client-core for shared functionality -->
|
||||
|
||||
## Common Libraries
|
||||
|
||||
### Directory: `/common/` (70+ modules)
|
||||
|
||||
### Core Infrastructure
|
||||
| Module | Purpose | Key Types |
|
||||
|--------|---------|-----------|
|
||||
| `nym-common` | Shared utilities | Constants, helpers |
|
||||
| `types` | Common data types | NodeId, MixId |
|
||||
| `config` | Configuration system | Config traits |
|
||||
| `commands` | CLI structures | Command builders |
|
||||
| `bin-common` | Binary utilities | Logging, banners |
|
||||
|
||||
### Cryptography & Security
|
||||
| Module | Purpose | Dependencies |
|
||||
|--------|---------|-------------|
|
||||
| `crypto` | Crypto primitives | Ed25519, X25519 |
|
||||
| `credentials` | Credential system | BLS12-381 |
|
||||
| `credentials-interface` | Interface definitions | - |
|
||||
| `credential-verification` | Validation logic | - |
|
||||
| `pemstore` | PEM storage | - |
|
||||
|
||||
### Network Protocol (Sphinx)
|
||||
<!-- AIDEV-NOTE: Complex area - Sphinx is the core privacy protocol -->
|
||||
|
||||
```
|
||||
nymsphinx/
|
||||
├── types/ # Core types
|
||||
├── chunking/ # Message fragmentation
|
||||
├── forwarding/ # Packet forwarding
|
||||
├── routing/ # Route selection
|
||||
├── addressing/ # Address handling
|
||||
├── anonymous-replies/ # SURB system
|
||||
├── acknowledgements/ # ACK handling
|
||||
├── cover/ # Cover traffic
|
||||
├── params/ # Protocol parameters
|
||||
└── framing/ # Wire format
|
||||
```
|
||||
|
||||
### New Components (Branch: drazen/lp-reg)
|
||||
<!-- AIDEV-NOTE: Current branch changes - These are new additions -->
|
||||
|
||||
| Module | Path | Status |
|
||||
|--------|------|--------|
|
||||
| `nym-lp` | `/common/nym-lp/` | New LP protocol |
|
||||
| `nym-lp-common` | `/common/nym-lp-common/` | LP utilities |
|
||||
| `nym-kcp` | `/common/nym-kcp/` | KCP protocol |
|
||||
|
||||
### Client Systems
|
||||
```
|
||||
client-core/
|
||||
├── config-types/ # Configuration types
|
||||
├── gateways-storage/ # Gateway persistence
|
||||
└── surb-storage/ # SURB storage
|
||||
|
||||
client-libs/
|
||||
├── gateway-client/ # Gateway connection
|
||||
├── mixnet-client/ # Mixnet interaction
|
||||
└── validator-client/ # Blockchain queries
|
||||
```
|
||||
|
||||
### Additional Common Modules
|
||||
|
||||
**Storage & Data**:
|
||||
- `statistics/` - Statistical collection
|
||||
- `topology/` - Network topology
|
||||
- `node-tester-utils/` - Testing utilities
|
||||
- `ticketbooks-merkle/` - Merkle trees
|
||||
|
||||
**Advanced Features**:
|
||||
- `dkg/` - Distributed Key Generation
|
||||
- `ecash-signer-check/` - E-cash validation
|
||||
- `nym_offline_compact_ecash/` - Offline e-cash
|
||||
|
||||
**Blockchain**:
|
||||
- `ledger/` - Ledger operations
|
||||
- `nyxd-scraper/` - Chain scraping
|
||||
- `cosmwasm-smart-contracts/` - Contract interfaces
|
||||
|
||||
**Utilities**:
|
||||
- `task/` - Async task management
|
||||
- `async-file-watcher/` - File watching
|
||||
- `nym-cache/` - Caching layer
|
||||
- `nym-metrics/` - Metrics (Prometheus)
|
||||
- `bandwidth-controller/` - Bandwidth accounting
|
||||
|
||||
## Smart Contracts
|
||||
|
||||
### Directory: `/contracts/`
|
||||
|
||||
<!-- AIDEV-NOTE: Navigation hint - All contracts use CosmWasm 2.2.2 -->
|
||||
|
||||
```
|
||||
contracts/
|
||||
├── Cargo.toml # Workspace config
|
||||
├── .cargo/config.toml # WASM build config
|
||||
├── coconut-dkg/ # DKG contract
|
||||
├── ecash/ # E-cash contract
|
||||
├── mixnet/ # Node registry
|
||||
├── vesting/ # Token vesting
|
||||
├── nym-pool/ # Liquidity pool
|
||||
├── multisig/ # Multi-sig wallet
|
||||
├── performance/ # Performance tracking
|
||||
└── mixnet-vesting-integration-tests/
|
||||
```
|
||||
|
||||
### Contract Build Process
|
||||
```bash
|
||||
make contracts # Build all
|
||||
make contract-schema # Generate schemas
|
||||
make wasm-opt-contracts # Optimize
|
||||
```
|
||||
|
||||
## SDKs
|
||||
|
||||
### Directory: `/sdk/`
|
||||
|
||||
```
|
||||
sdk/
|
||||
├── rust/
|
||||
│ └── nym-sdk/ # Primary Rust SDK
|
||||
├── typescript/
|
||||
│ ├── packages/ # NPM packages
|
||||
│ ├── codegen/ # Code generation
|
||||
│ └── examples/ # Usage examples
|
||||
└── ffi/
|
||||
├── cpp/ # C++ bindings
|
||||
├── go/ # Go bindings
|
||||
└── shared/ # Shared FFI code
|
||||
```
|
||||
|
||||
## WASM Modules
|
||||
|
||||
### Directory: `/wasm/`
|
||||
|
||||
| Module | Purpose | Build Command |
|
||||
|--------|---------|---------------|
|
||||
| `client` | Browser client | `make` in directory |
|
||||
| `mix-fetch` | Privacy fetch API | `make` in directory |
|
||||
| `node-tester` | Network testing | `make` in directory |
|
||||
| `zknym-lib` | Zero-knowledge lib | `make` in directory |
|
||||
|
||||
<!-- AIDEV-NOTE: Pattern reference - WASM modules compile from Rust using wasm-pack -->
|
||||
|
||||
## Service Providers
|
||||
|
||||
### Directory: `/service-providers/`
|
||||
|
||||
```
|
||||
service-providers/
|
||||
├── network-requester/ # Exit node for external requests
|
||||
├── ip-packet-router/ # IP packet routing (VPN-like)
|
||||
└── common/ # Shared utilities
|
||||
```
|
||||
|
||||
## Tools and Utilities
|
||||
|
||||
### Directory: `/tools/`
|
||||
|
||||
### Public Tools
|
||||
| Tool | Path | Purpose |
|
||||
|------|------|---------|
|
||||
| `nym-cli` | `/tools/nym-cli/` | Node management CLI |
|
||||
| `nym-id-cli` | `/tools/nym-id-cli/` | Identity management |
|
||||
| `nymvisor` | `/tools/nymvisor/` | Process supervisor |
|
||||
| `nym-nr-query` | `/tools/nym-nr-query/` | Network queries |
|
||||
| `echo-server` | `/tools/echo-server/` | Testing server |
|
||||
|
||||
### Internal Tools
|
||||
```
|
||||
internal/
|
||||
├── mixnet-connectivity-check/ # Network diagnostics
|
||||
├── contract-state-importer/ # Migration tools
|
||||
├── validator-status-check/ # Validator health
|
||||
├── ssl-inject/ # SSL injection
|
||||
├── testnet-manager/ # Testnet management
|
||||
└── sdk-version-bump/ # Version management
|
||||
```
|
||||
|
||||
## Configuration and Environments
|
||||
|
||||
### Environment Files: `/envs/`
|
||||
|
||||
<!-- AIDEV-NOTE: Navigation hint - Always use dotenv -f envs/[env].env for proper configuration -->
|
||||
|
||||
| Environment | File | API Endpoint | Use Case |
|
||||
|------------|------|--------------|----------|
|
||||
| Local | `local.env` | localhost | Development |
|
||||
| Sandbox | `sandbox.env` | sandbox-nym-api1.nymtech.net | Testing |
|
||||
| Mainnet | `mainnet.env` | validator.nymtech.net | Production |
|
||||
| Canary | `canary.env` | - | Pre-release |
|
||||
|
||||
### Key Environment Variables
|
||||
```bash
|
||||
NETWORK_NAME # Network identifier
|
||||
NYM_API # API endpoint
|
||||
NYXD # Blockchain RPC
|
||||
MIXNET_CONTRACT_ADDRESS # Contract addresses
|
||||
MNEMONIC # Test mnemonic (NEVER in production)
|
||||
RUST_LOG # Logging level
|
||||
DATABASE_URL # PostgreSQL connection
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### Primary Build Commands
|
||||
```bash
|
||||
make build # Debug build
|
||||
make build-release # Release build
|
||||
make test # Run tests
|
||||
make clippy # Lint code
|
||||
make fmt # Format code
|
||||
make contracts # Build contracts
|
||||
make sdk-wasm-build # Build WASM
|
||||
```
|
||||
|
||||
### Workspace Configuration
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - Root Cargo.toml manages 170+ workspace members -->
|
||||
|
||||
**Root Cargo.toml Structure**:
|
||||
- `[workspace]` - Lists all 170+ members
|
||||
- `[workspace.dependencies]` - Shared dependency versions
|
||||
- `[workspace.lints]` - Shared lint rules
|
||||
- `[profile.*]` - Build profiles
|
||||
|
||||
## Database Structure
|
||||
|
||||
### SQLx Usage Pattern
|
||||
- **Compile-time verified**: All queries checked at build
|
||||
- **Migration files**: In package `/migrations/` directories
|
||||
- **Query cache**: `.sqlx/` directory
|
||||
|
||||
### Key Tables (nym-api)
|
||||
```sql
|
||||
-- Network monitoring
|
||||
mixnode_status
|
||||
gateway_status
|
||||
routes
|
||||
monitor_run
|
||||
|
||||
-- Node registry
|
||||
nym_nodes
|
||||
node_descriptions
|
||||
|
||||
-- Performance
|
||||
node_uptime
|
||||
node_performance
|
||||
```
|
||||
|
||||
## Current Branch Context (drazen/lp-reg)
|
||||
|
||||
### New Additions
|
||||
- `/common/nym-lp/` - Low-level protocol implementation
|
||||
- `/common/nym-lp-common/` - LP common utilities
|
||||
- `/common/nym-kcp/` - KCP protocol
|
||||
- `/gateway/src/node/lp_listener/` - LP listener
|
||||
|
||||
### Modified Files
|
||||
```
|
||||
M Cargo.lock
|
||||
M Cargo.toml
|
||||
M common/registration/
|
||||
M common/wireguard/
|
||||
M gateway/
|
||||
M nym-node/
|
||||
M nym-node/nym-node-metrics/
|
||||
```
|
||||
|
||||
## Navigation Patterns
|
||||
|
||||
<!-- AIDEV-NOTE: Navigation hint - Use these patterns to quickly find code -->
|
||||
|
||||
### Finding Code by Type
|
||||
| Code Type | Look In |
|
||||
|-----------|---------|
|
||||
| Main executables | Root directories with `src/main.rs` |
|
||||
| Libraries | `/common/` with descriptive names |
|
||||
| Contracts | `/contracts/[name]/src/contract.rs` |
|
||||
| Tests | Colocated with source, `#[cfg(test)]` |
|
||||
| Configurations | `/envs/` and `config/` subdirs |
|
||||
| Database queries | Files with `.sql` or SQLx macros |
|
||||
| API endpoints | `/nym-api/src/` subdirectories |
|
||||
| CLI commands | `/cli/commands/` in executables |
|
||||
|
||||
### Common Import Locations
|
||||
```rust
|
||||
// Crypto
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
|
||||
// Network
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_topology::NymTopology;
|
||||
|
||||
// Client
|
||||
use nym_client_core::client::Client;
|
||||
|
||||
// Configuration
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
|
||||
// Contracts
|
||||
use nym_mixnet_contract_common::*;
|
||||
```
|
||||
|
||||
## Module Relationships
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - Understanding dependencies helps navigation -->
|
||||
|
||||
### Dependency Graph (Simplified)
|
||||
```
|
||||
nym-node
|
||||
├── common/nym-common
|
||||
├── common/crypto
|
||||
├── common/nymsphinx
|
||||
├── common/topology
|
||||
├── common/client-libs/validator-client
|
||||
└── common/wireguard
|
||||
|
||||
nym-api
|
||||
├── common/nym-common
|
||||
├── nym-api-requests
|
||||
├── common/client-libs/validator-client
|
||||
├── common/credentials
|
||||
└── sqlx (database)
|
||||
|
||||
clients/native
|
||||
├── common/client-core
|
||||
├── common/client-libs/gateway-client
|
||||
├── common/nymsphinx
|
||||
└── common/credentials
|
||||
```
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Adding New Feature
|
||||
1. Check `/envs/` for configuration
|
||||
2. Find similar code in `/common/`
|
||||
3. Implement in appropriate module
|
||||
4. Add tests colocated with code
|
||||
5. Update `/nym-api/` if needed
|
||||
6. Run `make test` and `make clippy`
|
||||
|
||||
### Debugging Network Issues
|
||||
1. Start with `/nym-network-monitor/`
|
||||
2. Check `/common/topology/` for routing
|
||||
3. Review `/common/nymsphinx/` for protocol
|
||||
4. Examine logs with `RUST_LOG=debug`
|
||||
|
||||
### Contract Development
|
||||
1. Create in `/contracts/[name]/`
|
||||
2. Use existing contracts as templates
|
||||
3. Build with `make contracts`
|
||||
4. Test with `cw-multi-test`
|
||||
Generated
+49
-91
@@ -133,9 +133,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "ammonia"
|
||||
version = "4.1.1"
|
||||
version = "4.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
|
||||
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever",
|
||||
@@ -165,15 +165,6 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
@@ -1000,12 +991,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "byte_string"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed"
|
||||
|
||||
[[package]]
|
||||
name = "bytecodec"
|
||||
version = "0.4.15"
|
||||
@@ -2594,7 +2579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "extension-storage"
|
||||
version = "1.4.0-rc.0"
|
||||
version = "1.4.1"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"console_error_panic_hook",
|
||||
@@ -4493,7 +4478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.0-rc.0"
|
||||
version = "1.4.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -4503,6 +4488,7 @@ dependencies = [
|
||||
"nym-ordered-buffer",
|
||||
"nym-service-providers-common",
|
||||
"nym-socks5-requests",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
@@ -4839,7 +4825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.67"
|
||||
version = "1.1.68"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5065,7 +5051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.64"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5148,7 +5134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.64"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5298,7 +5284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client-wasm"
|
||||
version = "1.4.0-rc.0"
|
||||
version = "1.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -5457,7 +5443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-credential-proxy"
|
||||
version = "0.3.0"
|
||||
version = "0.3.0-test"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5477,8 +5463,11 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-ecash-signer-check",
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-network-defaults",
|
||||
"nym-pemstore",
|
||||
"nym-upgrade-mode-check",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.22",
|
||||
@@ -5549,6 +5538,7 @@ dependencies = [
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-serde-helpers",
|
||||
"nym-upgrade-mode-check",
|
||||
"reqwest 0.12.22",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
@@ -5618,7 +5608,6 @@ dependencies = [
|
||||
"nym-ecash-contract-common",
|
||||
"nym-gateway-requests",
|
||||
"nym-gateway-storage",
|
||||
"nym-metrics",
|
||||
"nym-task",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
@@ -5677,12 +5666,12 @@ dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"aes-gcm-siv",
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"bs58",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"ed25519-dalek",
|
||||
"generic-array 0.14.7",
|
||||
@@ -5691,10 +5680,12 @@ dependencies = [
|
||||
"jwt-simple",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"nym-test-utils",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"subtle-encoding",
|
||||
"thiserror 2.0.12",
|
||||
@@ -5814,7 +5805,6 @@ dependencies = [
|
||||
"bincode",
|
||||
"bip39",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"defguard_wireguard_rs",
|
||||
"fastrand 2.3.0",
|
||||
@@ -5833,15 +5823,11 @@ dependencies = [
|
||||
"nym-gateway-storage",
|
||||
"nym-id",
|
||||
"nym-ip-packet-router",
|
||||
"nym-kcp",
|
||||
"nym-lp",
|
||||
"nym-metrics",
|
||||
"nym-mixnet-client",
|
||||
"nym-mixnode-common",
|
||||
"nym-network-defaults",
|
||||
"nym-network-requester",
|
||||
"nym-node-metrics",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-service-provider-requests-common",
|
||||
"nym-sphinx",
|
||||
@@ -6072,6 +6058,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"wasmtimer",
|
||||
]
|
||||
@@ -6222,19 +6209,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-kcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"byte_string",
|
||||
"bytes",
|
||||
"env_logger",
|
||||
"log",
|
||||
"thiserror 2.0.12",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-ledger"
|
||||
version = "0.1.0"
|
||||
@@ -6246,33 +6220,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"bincode",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"criterion",
|
||||
"dashmap",
|
||||
"nym-crypto",
|
||||
"nym-lp-common",
|
||||
"nym-sphinx",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"snow",
|
||||
"thiserror 2.0.12",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "nym-metrics"
|
||||
version = "0.1.0"
|
||||
@@ -6416,7 +6363,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.65"
|
||||
version = "1.1.66"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6466,7 +6413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.19.0"
|
||||
version = "1.20.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6608,7 +6555,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.0.10"
|
||||
version = "4.0.11-rc1"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6704,7 +6651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-tester-wasm"
|
||||
version = "1.3.0-rc.0"
|
||||
version = "1.3.1"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
@@ -6847,25 +6794,20 @@ dependencies = [
|
||||
name = "nym-registration-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bytes",
|
||||
"futures",
|
||||
"nym-authenticator-client",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-credential-storage",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ip-packet-client",
|
||||
"nym-lp",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-validator-client",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typed-builder",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -6873,15 +6815,10 @@ dependencies = [
|
||||
name = "nym-registration-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"nym-authenticator-requests",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ip-packet-requests",
|
||||
"nym-sphinx",
|
||||
"nym-wireguard-types",
|
||||
"serde",
|
||||
"time",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
@@ -7003,7 +6940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.64"
|
||||
version = "1.1.65"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7450,12 +7387,14 @@ dependencies = [
|
||||
"jwt-simple",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"nym-test-utils",
|
||||
"reqwest 0.12.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7651,7 +7590,6 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-gateway-requests",
|
||||
"nym-gateway-storage",
|
||||
"nym-metrics",
|
||||
"nym-network-defaults",
|
||||
"nym-node-metrics",
|
||||
"nym-task",
|
||||
@@ -7743,7 +7681,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.29"
|
||||
version = "0.1.30"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7897,9 +7835,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
version = "0.9.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -11465,6 +11403,26 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d0dd654273fc253fde1df4172c31fb6615cf8b041d3a4008a028ef8b1119e66"
|
||||
dependencies = [
|
||||
"typed-builder-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder-macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016c26257f448222014296978b2c8456e2cad4de308c35bdb1e383acd569ef5b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
|
||||
+7
-17
@@ -72,9 +72,6 @@ members = [
|
||||
"common/nym-cache",
|
||||
"common/nym-connection-monitor",
|
||||
"common/nym-id",
|
||||
"common/nym-kcp",
|
||||
"common/nym-lp",
|
||||
"common/nym-lp-common",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -153,7 +150,7 @@ members = [
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/mixnet-connectivity-check",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
@@ -206,7 +203,6 @@ aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
ammonia = "4"
|
||||
ansi_term = "0.12"
|
||||
anyhow = "1.0.98"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
@@ -219,7 +215,6 @@ base64 = "0.22.1"
|
||||
base85rs = "0.1.3"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bit-vec = "0.7.0" # can we unify those?
|
||||
bitvec = "1.0.0"
|
||||
blake3 = "1.7.0"
|
||||
bloomfilter = "3.0.1"
|
||||
@@ -247,13 +242,11 @@ criterion = "0.5"
|
||||
csv = "1.3.1"
|
||||
ctr = "0.9.1"
|
||||
cupid = "0.6.1"
|
||||
curve25519-dalek = "4.1"
|
||||
dashmap = "5.5.3"
|
||||
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
|
||||
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
|
||||
digest = "0.10.7"
|
||||
dirs = "6.0"
|
||||
doc-comment = "0.3"
|
||||
dotenvy = "0.15.6"
|
||||
dyn-clone = "1.0.19"
|
||||
ecdsa = "0.16"
|
||||
@@ -269,11 +262,8 @@ futures = "0.3.31"
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
getset = "0.1.5"
|
||||
handlebars = "3.5.5"
|
||||
headers = "0.4.0"
|
||||
hex = "0.4.3"
|
||||
hex-literal = "0.3.3"
|
||||
hickory-resolver = "0.25"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
@@ -297,12 +287,10 @@ lazy_static = "1.5.0"
|
||||
ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
log = "0.4"
|
||||
maxminddb = "0.23.0"
|
||||
mime = "0.3.17"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
nix = "0.27.1"
|
||||
notify = "5.1.0"
|
||||
okapi = "0.7.0"
|
||||
once_cell = "1.21.3"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
@@ -311,7 +299,6 @@ pem = "0.8"
|
||||
petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pnet_packet = "0.35.0"
|
||||
pin-project-lite = "0.2.16"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
@@ -319,13 +306,10 @@ rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rand_pcg = "0.3.1"
|
||||
rand_seeder = "0.2.3"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
safer-ffi = "0.1.13"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
serde = "1.0.219"
|
||||
@@ -372,6 +356,7 @@ tracing-indicatif = "0.3.9"
|
||||
tracing-test = "0.2.5"
|
||||
ts-rs = "10.1.0"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
typed-builder = "0.23.0"
|
||||
uniffi = "0.29.2"
|
||||
uniffi_build = "0.29.0"
|
||||
url = "2.5"
|
||||
@@ -468,6 +453,11 @@ opt-level = 'z'
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
suspicious = "deny"
|
||||
complexity = "deny"
|
||||
perf = "deny"
|
||||
style = "deny"
|
||||
|
||||
unwrap_used = "deny"
|
||||
expect_used = "deny"
|
||||
todo = "deny"
|
||||
|
||||
@@ -1,909 +0,0 @@
|
||||
# Nym Function Lexicon
|
||||
<!-- AIDEV-NOTE: This lexicon catalogs key functions, signatures, and API patterns across the Nym codebase -->
|
||||
<!-- Last updated: 2024-10-22 (branch: drazen/lp-reg) -->
|
||||
|
||||
## Quick Reference Index
|
||||
|
||||
| Category | Section | Key Operations |
|
||||
|----------|---------|----------------|
|
||||
| [Node Operations](#1-node-operations) | Mixnode & Gateway | Initialization, key management, tasks |
|
||||
| [Sphinx Protocol](#2-sphinx-packet-protocol) | Packet Processing | Message creation, chunking, routing |
|
||||
| [Client APIs](#3-client-apis) | Client Operations | Connection, sending, receiving |
|
||||
| [Network Topology](#4-network-topology) | Routing | Topology queries, route selection |
|
||||
| [Blockchain](#5-blockchain-operations) | Validator Client | Queries, transactions, contracts |
|
||||
| [REST APIs](#6-rest-api-endpoints) | HTTP Handlers | API routes and responses |
|
||||
| [Credentials](#7-credential--ecash) | E-cash | Credential creation, verification |
|
||||
| [Smart Contracts](#8-smart-contracts) | CosmWasm | Entry points, messages |
|
||||
| [Common Patterns](#9-common-patterns) | Conventions | Naming, errors, async |
|
||||
|
||||
---
|
||||
|
||||
## 1. Node Operations
|
||||
|
||||
### nym-node Core Functions
|
||||
<!-- AIDEV-NOTE: Complex area - nym-node unifies mixnode and gateway functionality -->
|
||||
|
||||
**Module**: `nym-node/src/node/mod.rs`
|
||||
|
||||
```rust
|
||||
// Node initialization
|
||||
pub async fn initialise_node(
|
||||
config: &Config,
|
||||
rng: &mut impl CryptoRng + RngCore,
|
||||
) -> Result<NodeData, NymNodeError>
|
||||
|
||||
// Key management
|
||||
pub fn load_x25519_wireguard_keypair(
|
||||
paths: &KeysPaths,
|
||||
) -> Result<x25519::KeyPair, NymNodeError>
|
||||
|
||||
pub fn load_ed25519_identity_keypair(
|
||||
paths: &KeysPaths,
|
||||
) -> Result<ed25519::KeyPair, NymNodeError>
|
||||
|
||||
// Gateway-specific initialization
|
||||
impl GatewayTasksData {
|
||||
pub async fn new(
|
||||
config: &GatewayTasksConfig,
|
||||
client_storage: ClientStorage,
|
||||
) -> Result<GatewayTasksData, GatewayError>
|
||||
|
||||
pub fn initialise(
|
||||
config: &GatewayTasksConfig,
|
||||
force_init: bool,
|
||||
) -> Result<(), GatewayError>
|
||||
}
|
||||
|
||||
// Service provider initialization
|
||||
impl ServiceProvidersData {
|
||||
pub fn initialise_client_keys<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
gateway_paths: &GatewayPaths,
|
||||
) -> Result<ed25519::KeyPair, GatewayError>
|
||||
|
||||
pub async fn initialise_network_requester<R>(
|
||||
rng: &mut R,
|
||||
config: &Config,
|
||||
) -> Result<Option<LocalNetworkRequester>, GatewayError>
|
||||
}
|
||||
```
|
||||
|
||||
### Gateway Task Builder Pattern
|
||||
**Module**: `gateway/src/node/mod.rs`
|
||||
|
||||
```rust
|
||||
pub struct GatewayTasksBuilder {
|
||||
// Builder methods
|
||||
pub fn new(
|
||||
identity_keypair: Arc<ed25519::KeyPair>,
|
||||
config: Config,
|
||||
client_storage: ClientStorage,
|
||||
) -> GatewayTasksBuilder
|
||||
|
||||
pub fn set_network_requester_opts(
|
||||
&mut self,
|
||||
opts: Option<LocalNetworkRequesterOpts>
|
||||
) -> &mut Self
|
||||
|
||||
pub fn set_ip_packet_router_opts(
|
||||
&mut self,
|
||||
opts: Option<LocalIpPacketRouterOpts>
|
||||
) -> &mut Self
|
||||
|
||||
pub async fn build_and_run(
|
||||
self,
|
||||
shutdown: TaskManager,
|
||||
) -> Result<(), GatewayError>
|
||||
}
|
||||
```
|
||||
|
||||
<!-- AIDEV-NOTE: Pattern reference - Builder pattern is common for complex initialization -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Sphinx Packet Protocol
|
||||
|
||||
### Message Construction & Processing
|
||||
**Module**: `common/nymsphinx/src/message.rs`
|
||||
|
||||
```rust
|
||||
// Core message types
|
||||
pub enum NymMessage {
|
||||
Plain(Vec<u8>),
|
||||
Repliable(RepliableMessage),
|
||||
Reply(ReplyMessage),
|
||||
}
|
||||
|
||||
impl NymMessage {
|
||||
// Constructors
|
||||
pub fn new_plain(msg: Vec<u8>) -> NymMessage
|
||||
pub fn new_repliable(msg: RepliableMessage) -> NymMessage
|
||||
pub fn new_reply(msg: ReplyMessage) -> NymMessage
|
||||
pub fn new_additional_surbs_request(
|
||||
recipient: Recipient,
|
||||
amount: u32
|
||||
) -> NymMessage
|
||||
|
||||
// Processing
|
||||
pub fn pad_to_full_packet_lengths(
|
||||
self,
|
||||
plaintext_per_packet: usize
|
||||
) -> PaddedMessage
|
||||
|
||||
pub fn split_into_fragments<R: Rng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
packet_size: PacketSize,
|
||||
) -> Vec<Fragment>
|
||||
|
||||
pub fn remove_padding(self) -> Result<NymMessage, NymMessageError>
|
||||
|
||||
// Queries
|
||||
pub fn is_reply_surb_request(&self) -> bool
|
||||
pub fn available_sphinx_plaintext_per_packet(
|
||||
&self,
|
||||
packet_size: PacketSize
|
||||
) -> usize
|
||||
pub fn required_packets(&self, packet_size: PacketSize) -> usize
|
||||
}
|
||||
```
|
||||
|
||||
### Payload Building & Preparation
|
||||
**Module**: `common/nymsphinx/src/preparer.rs`
|
||||
|
||||
```rust
|
||||
pub struct NymPayloadBuilder {
|
||||
// Main preparation methods
|
||||
pub async fn prepare_chunk_for_sending(
|
||||
&mut self,
|
||||
message: NymMessage,
|
||||
topology: &NymTopology,
|
||||
) -> Result<Vec<MixPacket>, NymPayloadBuilderError>
|
||||
|
||||
pub async fn prepare_reply_chunk_for_sending(
|
||||
&mut self,
|
||||
reply: NymMessage,
|
||||
reply_surb: ReplySurb,
|
||||
) -> Result<Vec<MixPacket>, NymPayloadBuilderError>
|
||||
|
||||
// SURB generation
|
||||
pub fn generate_reply_surbs(
|
||||
&mut self,
|
||||
amount: u32,
|
||||
topology: &NymTopology,
|
||||
) -> Result<Vec<SurbAck>, NymPayloadBuilderError>
|
||||
|
||||
// Fragment splitting
|
||||
pub fn pad_and_split_message(
|
||||
&mut self,
|
||||
message: NymMessage,
|
||||
) -> Result<Vec<Fragment>, NymPayloadBuilderError>
|
||||
}
|
||||
|
||||
// Builder constructors
|
||||
pub fn build_regular<R: CryptoRng + Rng>(
|
||||
rng: R,
|
||||
sender_address: Option<Recipient>,
|
||||
) -> NymPayloadBuilder
|
||||
|
||||
pub fn build_reply(
|
||||
sender_address: Recipient,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
) -> NymPayloadBuilder
|
||||
```
|
||||
|
||||
### Chunking & Fragmentation
|
||||
**Module**: `common/nymsphinx/chunking/src/lib.rs`
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - Chunking splits messages into Sphinx-sized packets -->
|
||||
|
||||
```rust
|
||||
// Main chunking function
|
||||
pub fn split_into_sets(
|
||||
message: &[u8],
|
||||
max_plaintext_size: usize,
|
||||
max_fragments_per_set: usize,
|
||||
) -> Result<Vec<Vec<Fragment>>, ChunkingError>
|
||||
|
||||
// Fragment monitoring (optional feature)
|
||||
pub mod monitoring {
|
||||
pub fn enable()
|
||||
pub fn enabled() -> bool
|
||||
pub fn fragment_received(fragment: &Fragment)
|
||||
pub fn fragment_sent(
|
||||
fragment: &Fragment,
|
||||
client_nonce: i32,
|
||||
destination: PublicKey
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Client APIs
|
||||
|
||||
### Gateway Client
|
||||
**Module**: `common/client-libs/gateway-client/src/lib.rs`
|
||||
|
||||
```rust
|
||||
pub struct GatewayClient {
|
||||
// Connection management
|
||||
pub async fn connect(
|
||||
config: GatewayClientConfig,
|
||||
) -> Result<GatewayClient, GatewayClientError>
|
||||
|
||||
pub async fn authenticate(
|
||||
&mut self,
|
||||
credentials: Credentials,
|
||||
) -> Result<(), GatewayClientError>
|
||||
|
||||
// Message operations
|
||||
pub async fn send_mix_packet(
|
||||
&self,
|
||||
packet: MixPacket,
|
||||
) -> Result<(), GatewayClientError>
|
||||
|
||||
pub async fn receive_messages(
|
||||
&mut self,
|
||||
) -> Result<Vec<ReconstructedMessage>, GatewayClientError>
|
||||
}
|
||||
|
||||
// Packet routing
|
||||
pub struct PacketRouter {
|
||||
pub fn new(
|
||||
mix_tx: MixnetMessageSender,
|
||||
ack_tx: AcknowledgementSender,
|
||||
) -> PacketRouter
|
||||
|
||||
pub async fn route_packet(
|
||||
&self,
|
||||
packet: MixPacket,
|
||||
) -> Result<(), PacketRouterError>
|
||||
}
|
||||
```
|
||||
|
||||
### Mixnet Client
|
||||
**Module**: `common/client-libs/mixnet-client/src/lib.rs`
|
||||
|
||||
```rust
|
||||
pub struct Client {
|
||||
// Core client operations
|
||||
pub async fn new(config: Config) -> Result<Client, ClientError>
|
||||
|
||||
pub async fn send_message(
|
||||
&mut self,
|
||||
recipient: Recipient,
|
||||
message: Vec<u8>,
|
||||
) -> Result<(), ClientError>
|
||||
|
||||
pub async fn receive_message(
|
||||
&mut self,
|
||||
) -> Result<ReconstructedMessage, ClientError>
|
||||
|
||||
// Connection management
|
||||
pub fn is_connected(&self) -> bool
|
||||
pub async fn reconnect(&mut self) -> Result<(), ClientError>
|
||||
}
|
||||
|
||||
// Send without response trait
|
||||
pub trait SendWithoutResponse {
|
||||
fn send_without_response(
|
||||
&self,
|
||||
packet: MixPacket,
|
||||
) -> io::Result<()>
|
||||
}
|
||||
```
|
||||
|
||||
<!-- AIDEV-NOTE: Pattern reference - Async/await is standard for network operations -->
|
||||
|
||||
### Client Core Initialization
|
||||
**Module**: `common/client-core/src/init.rs`
|
||||
|
||||
```rust
|
||||
// Key generation
|
||||
pub fn generate_new_client_keys<R: CryptoRng + Rng>(
|
||||
rng: &mut R,
|
||||
) -> (ed25519::KeyPair, x25519::KeyPair)
|
||||
|
||||
// Storage initialization
|
||||
pub async fn init_storage(
|
||||
paths: &ClientPaths,
|
||||
) -> Result<ClientStorage, ClientCoreError>
|
||||
|
||||
// Configuration setup
|
||||
pub fn setup_client_config(
|
||||
id: &str,
|
||||
network: Network,
|
||||
) -> Result<Config, ClientCoreError>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Network Topology
|
||||
|
||||
### Topology Management
|
||||
**Module**: `common/topology/src/lib.rs`
|
||||
|
||||
```rust
|
||||
pub struct NymTopology {
|
||||
// Query methods
|
||||
pub fn mixnodes(&self) -> &[RoutingNode]
|
||||
pub fn gateways(&self) -> &[RoutingNode]
|
||||
pub fn layer_nodes(&self, layer: MixLayer) -> Vec<&RoutingNode>
|
||||
|
||||
// Route selection
|
||||
pub fn random_route<R: Rng>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
) -> Option<Vec<RoutingNode>>
|
||||
|
||||
pub fn get_node_by_id(&self, node_id: NodeId) -> Option<&RoutingNode>
|
||||
}
|
||||
|
||||
// Route provider
|
||||
pub struct NymRouteProvider {
|
||||
pub fn new(topology: NymTopology) -> NymRouteProvider
|
||||
|
||||
pub fn random_route<R: Rng>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
) -> Option<Vec<RoutingNode>>
|
||||
}
|
||||
|
||||
// Topology provider trait
|
||||
pub trait TopologyProvider {
|
||||
async fn get_topology(&self) -> Result<NymTopology, NymTopologyError>
|
||||
async fn refresh_topology(&mut self) -> Result<(), NymTopologyError>
|
||||
}
|
||||
```
|
||||
|
||||
### Routing Node
|
||||
**Module**: `common/topology/src/node.rs`
|
||||
|
||||
```rust
|
||||
pub struct RoutingNode {
|
||||
pub fn node_id(&self) -> NodeId
|
||||
pub fn identity_key(&self) -> &ed25519::PublicKey
|
||||
pub fn sphinx_key(&self) -> &x25519::PublicKey
|
||||
pub fn mix_host(&self) -> &SocketAddr
|
||||
pub fn clients_ws_address(&self) -> Option<&Url>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Blockchain Operations
|
||||
|
||||
### Validator Client
|
||||
**Module**: `common/client-libs/validator-client/src/client.rs`
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - Handles all blockchain interactions -->
|
||||
|
||||
```rust
|
||||
pub struct Client<C, S = NoSigner> {
|
||||
// Contract queries
|
||||
pub async fn query_contract_state<T>(
|
||||
&self,
|
||||
contract: &str,
|
||||
query: T,
|
||||
) -> Result<ContractStateResponse, ValidatorClientError>
|
||||
where T: Into<Binary>
|
||||
|
||||
// Transaction execution (requires signer)
|
||||
pub async fn execute_contract_message<M>(
|
||||
&self,
|
||||
contract: &str,
|
||||
msg: M,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<TxResponse, ValidatorClientError>
|
||||
where M: Into<Binary>
|
||||
|
||||
// Specific contract operations
|
||||
pub async fn bond_mixnode(
|
||||
&self,
|
||||
mixnode: MixNode,
|
||||
cost_params: MixNodeCostParams,
|
||||
pledge: Coin,
|
||||
) -> Result<TxResponse, ValidatorClientError>
|
||||
|
||||
pub async fn unbond_mixnode(&self) -> Result<TxResponse, ValidatorClientError>
|
||||
|
||||
pub async fn delegate_to_mixnode(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
) -> Result<TxResponse, ValidatorClientError>
|
||||
}
|
||||
|
||||
// Nyxd-specific client
|
||||
pub type DirectSigningHttpRpcNyxdClient =
|
||||
nyxd::NyxdClient<HttpRpcClient, DirectSecp256k1HdWallet>;
|
||||
```
|
||||
|
||||
### Contract Queries
|
||||
**Module**: `common/client-libs/validator-client/src/nyxd/contract_traits/`
|
||||
|
||||
```rust
|
||||
// Mixnet contract queries
|
||||
pub trait MixnetQueryClient {
|
||||
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NyxdError>
|
||||
async fn get_gateways(&self) -> Result<Vec<Gateway>, NyxdError>
|
||||
async fn get_current_epoch(&self) -> Result<Epoch, NyxdError>
|
||||
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError>
|
||||
}
|
||||
|
||||
// Vesting contract queries
|
||||
pub trait VestingQueryClient {
|
||||
async fn get_vesting_details(&self, address: &str)
|
||||
-> Result<VestingDetails, NyxdError>
|
||||
}
|
||||
|
||||
// E-cash contract queries
|
||||
pub trait EcashQueryClient {
|
||||
async fn get_deposit(&self, id: DepositId)
|
||||
-> Result<Deposit, NyxdError>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. REST API Endpoints
|
||||
|
||||
### nym-api Main Routes
|
||||
**Module**: `nym-api/src/main.rs` and submodules
|
||||
|
||||
<!-- AIDEV-NOTE: Navigation hint - Each module contains router setup and handlers -->
|
||||
|
||||
```rust
|
||||
// Main API setup
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
// Router configuration
|
||||
let app = Router::new()
|
||||
.merge(api_routes())
|
||||
.merge(swagger_ui())
|
||||
.layer(cors_layer())
|
||||
.layer(trace_layer());
|
||||
}
|
||||
|
||||
// Core API routes (various modules)
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.nest("/v1/status", status_routes())
|
||||
.nest("/v1/mixnodes", mixnode_routes())
|
||||
.nest("/v1/gateways", gateway_routes())
|
||||
.nest("/v1/network", network_routes())
|
||||
.nest("/v1/ecash", ecash_routes())
|
||||
}
|
||||
```
|
||||
|
||||
### Status Routes
|
||||
**Module**: `nym-api/src/status/mod.rs`
|
||||
|
||||
```rust
|
||||
pub async fn status_handler() -> impl IntoResponse {
|
||||
Json(ApiStatusResponse {
|
||||
status: "ok",
|
||||
uptime: get_uptime(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn health_check() -> impl IntoResponse {
|
||||
StatusCode::OK
|
||||
}
|
||||
```
|
||||
|
||||
### Network Monitor Routes
|
||||
**Module**: `nym-api/src/network_monitor/mod.rs`
|
||||
|
||||
```rust
|
||||
pub async fn get_monitor_report(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<MonitorReport>, ApiError> {
|
||||
// Returns network reliability report
|
||||
}
|
||||
|
||||
pub async fn get_node_reliability(
|
||||
Path(node_id): Path<NodeId>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<NodeReliability>, ApiError> {
|
||||
// Returns specific node reliability
|
||||
}
|
||||
```
|
||||
|
||||
### E-cash API
|
||||
**Module**: `nym-api/src/ecash/mod.rs`
|
||||
|
||||
```rust
|
||||
pub async fn verify_credential(
|
||||
Json(credential): Json<Credential>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<VerificationResponse>, ApiError> {
|
||||
// Verifies e-cash credentials
|
||||
}
|
||||
|
||||
pub async fn issue_credential(
|
||||
Json(request): Json<IssuanceRequest>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<IssuedCredential>, ApiError> {
|
||||
// Issues new e-cash credentials
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Credential & E-cash
|
||||
|
||||
### Credential Operations
|
||||
**Module**: `common/credentials/src/ecash/mod.rs`
|
||||
|
||||
```rust
|
||||
// Credential spending
|
||||
pub struct CredentialSpendingData {
|
||||
pub fn new(
|
||||
ticketbook: IssuedTicketBook,
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
) -> CredentialSpendingData
|
||||
|
||||
pub fn prepare_for_spending(
|
||||
&self,
|
||||
request_id: i64,
|
||||
) -> PreparedCredential
|
||||
}
|
||||
|
||||
// Credential signing
|
||||
pub struct CredentialSigningData {
|
||||
pub fn sign_credential(
|
||||
&self,
|
||||
blinded_credential: BlindedCredential,
|
||||
) -> Result<BlindedSignature, CredentialError>
|
||||
}
|
||||
|
||||
// Aggregation utilities
|
||||
pub fn aggregate_verification_keys(
|
||||
keys: Vec<VerificationKey>,
|
||||
) -> AggregatedVerificationKey
|
||||
|
||||
pub fn obtain_aggregate_wallet(
|
||||
verification_keys: Vec<VerificationKey>,
|
||||
commitments: Vec<Commitment>,
|
||||
) -> Result<AggregateWallet, CredentialError>
|
||||
```
|
||||
|
||||
### Ticketbook Operations
|
||||
**Module**: `common/credentials/src/ecash/bandwidth/mod.rs`
|
||||
|
||||
<!-- AIDEV-NOTE: Complex area - Ticketbooks contain bandwidth credentials -->
|
||||
|
||||
```rust
|
||||
pub struct IssuedTicketBook {
|
||||
pub fn new(
|
||||
tickets: Vec<IssuedTicket>,
|
||||
expiration: OffsetDateTime,
|
||||
) -> IssuedTicketBook
|
||||
|
||||
pub fn total_bandwidth(&self) -> Bandwidth
|
||||
pub fn is_expired(&self) -> bool
|
||||
pub fn consume_ticket(&mut self) -> Option<IssuedTicket>
|
||||
}
|
||||
|
||||
pub struct ImportableTicketBook {
|
||||
pub fn try_from_base58(s: &str) -> Result<Self, CredentialError>
|
||||
pub fn into_issued(self) -> Result<IssuedTicketBook, CredentialError>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Smart Contracts
|
||||
|
||||
### Mixnet Contract Entry Points
|
||||
**Module**: `contracts/mixnet/src/contract.rs`
|
||||
|
||||
```rust
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
#[entry_point]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
#[entry_point]
|
||||
pub fn query(
|
||||
deps: Deps,
|
||||
env: Env,
|
||||
msg: QueryMsg,
|
||||
) -> StdResult<Binary>
|
||||
```
|
||||
|
||||
### Execute Message Handlers
|
||||
**Module**: `contracts/mixnet/src/contract.rs`
|
||||
|
||||
```rust
|
||||
// Node operations
|
||||
fn try_bond_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mixnode: MixNode,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
fn try_unbond_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
// Delegation operations
|
||||
fn try_delegate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
fn try_undelegate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, ContractError>
|
||||
|
||||
// Reward operations
|
||||
fn try_reward_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
mix_id: MixId,
|
||||
performance: Performance,
|
||||
) -> Result<Response, ContractError>
|
||||
```
|
||||
|
||||
### Query Message Handlers
|
||||
```rust
|
||||
fn query_mixnode(deps: Deps, mix_id: MixId) -> StdResult<MixnodeDetails>
|
||||
fn query_gateways(deps: Deps) -> StdResult<Vec<Gateway>>
|
||||
fn query_rewarded_set(deps: Deps, epoch: Epoch) -> StdResult<EpochRewardedSet>
|
||||
fn query_current_epoch(deps: Deps) -> StdResult<Epoch>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Common Patterns
|
||||
|
||||
### Function Naming Conventions
|
||||
<!-- AIDEV-NOTE: Pattern reference - Consistent naming helps code discovery -->
|
||||
|
||||
```rust
|
||||
// Constructors
|
||||
pub fn new(...) -> Self // Standard constructor
|
||||
pub fn with_defaults() -> Self // Constructor with defaults
|
||||
pub fn from_config(config: Config) -> Self // From configuration
|
||||
|
||||
// Async initialization
|
||||
pub async fn init(...) -> Result<T> // Async initialization
|
||||
pub async fn initialise(...) -> Result<T> // British spelling variant
|
||||
pub async fn setup(...) -> Result<T> // Setup function
|
||||
|
||||
// Builder pattern
|
||||
pub fn builder() -> TBuilder // Create builder
|
||||
pub fn set_field(mut self, val: T) -> Self // Builder setter
|
||||
pub fn build(self) -> Result<T> // Build final object
|
||||
|
||||
// Getters
|
||||
pub fn field(&self) -> &T // Immutable reference
|
||||
pub fn field_mut(&mut self) -> &mut T // Mutable reference
|
||||
pub fn into_inner(self) -> T // Consume and return inner
|
||||
|
||||
// Queries
|
||||
pub fn is_valid(&self) -> bool // Boolean check
|
||||
pub fn has_field(&self) -> bool // Existence check
|
||||
pub fn contains(&self, item: &T) -> bool // Contains check
|
||||
|
||||
// Transformations
|
||||
pub fn to_type(&self) -> Type // Convert to type
|
||||
pub fn into_type(self) -> Type // Consume and convert
|
||||
pub fn try_into_type(self) -> Result<Type> // Fallible conversion
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
|
||||
```rust
|
||||
// Custom error types with thiserror
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ModuleError {
|
||||
#[error("Network error: {0}")]
|
||||
Network(#[from] NetworkError),
|
||||
|
||||
#[error("Invalid configuration: {reason}")]
|
||||
InvalidConfig { reason: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
// Result type alias
|
||||
pub type Result<T> = std::result::Result<T, ModuleError>;
|
||||
|
||||
// Error conversion
|
||||
impl From<io::Error> for ModuleError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
ModuleError::Io(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
|
||||
```rust
|
||||
// Async trait (with async-trait crate)
|
||||
#[async_trait]
|
||||
pub trait AsyncOperation {
|
||||
async fn perform(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
// Spawning tasks
|
||||
tokio::spawn(async move {
|
||||
// Task code
|
||||
});
|
||||
|
||||
// Channels for communication
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
|
||||
// Select on multiple futures
|
||||
tokio::select! {
|
||||
result = future1 => { /* handle */ },
|
||||
result = future2 => { /* handle */ },
|
||||
_ = shutdown.recv() => { /* shutdown */ },
|
||||
}
|
||||
```
|
||||
|
||||
### Storage Patterns
|
||||
|
||||
```rust
|
||||
// SQLx queries
|
||||
sqlx::query!(
|
||||
"SELECT * FROM nodes WHERE id = ?",
|
||||
node_id
|
||||
)
|
||||
.fetch_optional(&pool)
|
||||
.await?;
|
||||
|
||||
// In-memory caching
|
||||
use dashmap::DashMap;
|
||||
let cache: DashMap<Key, Value> = DashMap::new();
|
||||
|
||||
// File storage
|
||||
use std::fs;
|
||||
fs::write(path, data)?;
|
||||
let content = fs::read_to_string(path)?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Import Reference
|
||||
|
||||
### Standard Imports by Category
|
||||
|
||||
```rust
|
||||
// Nym crypto
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_crypto::symmetric::stream_cipher;
|
||||
|
||||
// Sphinx protocol
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::params::{PacketSize, DEFAULT_PACKET_SIZE};
|
||||
|
||||
// Client libraries
|
||||
use nym_client_core::client::Client;
|
||||
use nym_gateway_client::GatewayClient;
|
||||
use nym_validator_client::ValidatorClient;
|
||||
|
||||
// Topology
|
||||
use nym_topology::{NymTopology, RoutingNode};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
// Configuration
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_config::defaults::NymNetwork;
|
||||
|
||||
// Async runtime
|
||||
use tokio::sync::{mpsc, RwLock, Mutex};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use futures::{StreamExt, SinkExt};
|
||||
|
||||
// Error handling
|
||||
use thiserror::Error;
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
|
||||
// Logging
|
||||
use tracing::{debug, info, warn, error, instrument};
|
||||
|
||||
// Serialization
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
// Web framework (API)
|
||||
use axum::{Router, extract::{Path, Query, State}, response::IntoResponse};
|
||||
use axum::Json;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Feature Flags
|
||||
|
||||
### Common Feature Gates
|
||||
|
||||
```rust
|
||||
// Client-specific features
|
||||
#[cfg(feature = "client")]
|
||||
#[cfg(feature = "cli")]
|
||||
|
||||
// Platform-specific
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
// Testing
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "testing")]
|
||||
#[cfg(feature = "contract-testing")]
|
||||
|
||||
// Storage backends
|
||||
#[cfg(feature = "fs-surb-storage")]
|
||||
#[cfg(feature = "fs-credentials-storage")]
|
||||
|
||||
// Network features
|
||||
#[cfg(feature = "http-client")]
|
||||
#[cfg(feature = "websocket")]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Lookup Tables
|
||||
|
||||
### Async vs Sync Functions
|
||||
|
||||
| Operation Type | Typically Async | Typically Sync |
|
||||
|---------------|-----------------|----------------|
|
||||
| Network I/O | ✓ | |
|
||||
| Database queries | ✓ | |
|
||||
| Contract execution | ✓ | |
|
||||
| Cryptographic ops | | ✓ |
|
||||
| Message construction | | ✓ |
|
||||
| Configuration parsing | | ✓ |
|
||||
| Topology queries | Both | Both |
|
||||
|
||||
### Return Type Patterns
|
||||
|
||||
| Pattern | Usage | Example |
|
||||
|---------|-------|---------|
|
||||
| `Result<T, E>` | Fallible operations | `connect() -> Result<Client>` |
|
||||
| `Option<T>` | May not exist | `get_node() -> Option<Node>` |
|
||||
| `impl Trait` | Return trait impl | `handler() -> impl IntoResponse` |
|
||||
| `Box<dyn Trait>` | Dynamic dispatch | `create() -> Box<dyn Storage>` |
|
||||
| Direct type | Infallible ops | `new() -> Self` |
|
||||
|
||||
### Module Organization
|
||||
|
||||
| Module Type | Location Pattern | Naming Convention |
|
||||
|------------|------------------|-------------------|
|
||||
| Binary entry | `/src/main.rs` | - |
|
||||
| Library root | `/src/lib.rs` | - |
|
||||
| Submodules | `/src/module/mod.rs` | snake_case |
|
||||
| Tests | `/src/module/tests.rs` | #[cfg(test)] |
|
||||
| Errors | `/src/error.rs` | ModuleError |
|
||||
| Config | `/src/config.rs` | Config struct |
|
||||
|
||||
---
|
||||
|
||||
<!-- AIDEV-NOTE: This lexicon provides rapid function lookup. Use Ctrl+F to search for specific operations -->
|
||||
@@ -107,16 +107,16 @@ sdk-wasm-build:
|
||||
$(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/node-tester
|
||||
# $(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/full-nym-wasm
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
sdk-typescript-build:
|
||||
npx lerna run --scope @nymproject/sdk build --stream
|
||||
# npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
# npx lerna run --scope @nymproject/node-tester build --stream
|
||||
# yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
npx lerna run --scope @nymproject/node-tester build --stream
|
||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.64"
|
||||
version = "1.1.65"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.64"
|
||||
version = "1.1.65"
|
||||
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"
|
||||
|
||||
@@ -801,7 +801,7 @@ where
|
||||
event_tx,
|
||||
);
|
||||
|
||||
let mix_tx = mix_traffic_controller.mix_rx();
|
||||
let mix_tx = mix_traffic_controller.mix_tx();
|
||||
let client_tx = mix_traffic_controller.client_tx();
|
||||
|
||||
shutdown_tracker.try_spawn_named(
|
||||
@@ -1004,8 +1004,8 @@ where
|
||||
// Create a shutdown tracker for this client - either as a child of provided tracker
|
||||
// or get one from the registry
|
||||
let shutdown_tracker = match self.shutdown {
|
||||
Some(parent_tracker) => parent_tracker.child_tracker(),
|
||||
None => nym_task::get_sdk_shutdown_tracker()?,
|
||||
Some(parent_tracker) => parent_tracker.clone(),
|
||||
None => nym_task::create_sdk_shutdown_tracker()?,
|
||||
};
|
||||
|
||||
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
|
||||
@@ -1044,7 +1044,7 @@ where
|
||||
self.user_agent.clone(),
|
||||
generate_client_stats_id(*self_address.identity()),
|
||||
input_sender.clone(),
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
);
|
||||
|
||||
// needs to be started as the first thing to block if required waiting for the gateway
|
||||
@@ -1054,7 +1054,7 @@ where
|
||||
shared_topology_accessor.clone(),
|
||||
self_address.gateway(),
|
||||
self.wait_for_gateway,
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1074,7 +1074,7 @@ where
|
||||
stats_reporter.clone(),
|
||||
#[cfg(unix)]
|
||||
self.connection_fd_callback,
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
)
|
||||
.await?;
|
||||
let gateway_ws_fd = gateway_transceiver.ws_fd();
|
||||
@@ -1082,7 +1082,7 @@ where
|
||||
let reply_storage = Self::setup_persistent_reply_storage(
|
||||
reply_storage_backend,
|
||||
key_rotation_config,
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1093,7 +1093,7 @@ where
|
||||
reply_storage.key_storage(),
|
||||
reply_controller_sender.clone(),
|
||||
stats_reporter.clone(),
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
);
|
||||
|
||||
// The message_sender is the transmitter for any component generating sphinx packets
|
||||
@@ -1103,7 +1103,7 @@ where
|
||||
|
||||
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
|
||||
gateway_transceiver,
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
EventSender(event_sender),
|
||||
);
|
||||
|
||||
@@ -1134,7 +1134,7 @@ where
|
||||
shared_lane_queue_lengths.clone(),
|
||||
client_connection_rx,
|
||||
stats_reporter.clone(),
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
);
|
||||
|
||||
if !self
|
||||
@@ -1150,7 +1150,7 @@ where
|
||||
shared_topology_accessor.clone(),
|
||||
message_sender,
|
||||
stats_reporter.clone(),
|
||||
&shutdown_tracker.child_tracker(),
|
||||
&shutdown_tracker.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
TrySendError::Full(_) => {
|
||||
// This isn't a problem, if the channel is full means we're already sending the
|
||||
// max amount of messages downstream can handle.
|
||||
tracing::debug!("Failed to send cover message - channel full");
|
||||
tracing::trace!("Failed to send cover message - channel full");
|
||||
}
|
||||
TrySendError::Closed(_) => {
|
||||
tracing::warn!("Failed to send cover message - channel closed");
|
||||
|
||||
@@ -20,7 +20,10 @@ pub mod transceiver;
|
||||
|
||||
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
|
||||
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
|
||||
const MAX_FAILURE_COUNT: usize = 100;
|
||||
|
||||
/// Reduced from 100 to 20 to fail fast (~1-2 seconds instead of ~6 seconds).
|
||||
/// If we can't send 20 packets in a row, the gateway is unreachable.
|
||||
const MAX_FAILURE_COUNT: usize = 20;
|
||||
|
||||
// that's also disgusting.
|
||||
pub struct Empty;
|
||||
@@ -84,7 +87,7 @@ impl MixTrafficController {
|
||||
self.client_tx.clone()
|
||||
}
|
||||
|
||||
pub fn mix_rx(&self) -> BatchMixMessageSender {
|
||||
pub fn mix_tx(&self) -> BatchMixMessageSender {
|
||||
self.mix_tx.clone()
|
||||
}
|
||||
|
||||
@@ -156,6 +159,11 @@ impl MixTrafficController {
|
||||
// Do we need to handle the embedded mixnet client case
|
||||
// separately?
|
||||
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
|
||||
// IMO it shouldn't be signalled from there but it is how it is
|
||||
// TODO : report the failure upwards and shutdown from upwards
|
||||
// Gateway is dead, we have to shut down currently
|
||||
error!("Signalling shutdown from the MixTrafficController");
|
||||
self.shutdown_token.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +298,8 @@ where
|
||||
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
|
||||
);
|
||||
}
|
||||
// Early return to avoid further processing when channel is closed
|
||||
return;
|
||||
}
|
||||
Ok(_) => {
|
||||
let event = if fragment_id.is_some() {
|
||||
|
||||
@@ -45,7 +45,7 @@ pub enum ClientCoreError {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
|
||||
#[error("no gateways on network")]
|
||||
NoGatewaysOnNetwork,
|
||||
|
||||
@@ -441,7 +441,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_urls_prepared_for_retries() {
|
||||
let urls = vec![
|
||||
let urls = [
|
||||
Url::parse("https://api1.nym.com").unwrap(),
|
||||
Url::parse("https://api2.nym.com").unwrap(),
|
||||
Url::parse("https://api3.nym.com").unwrap(),
|
||||
|
||||
@@ -30,7 +30,6 @@ pub(crate) async fn connect_async(
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ pub(crate) async fn connect_async(
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ pub enum GatewayClientError {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
|
||||
#[error("No shared key was provided or obtained")]
|
||||
NoSharedKeyAvailable,
|
||||
|
||||
@@ -27,9 +27,6 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub identity_key: String,
|
||||
|
||||
#[clap(long, help = "LP (Lewes Protocol) listener port (default: 41264)")]
|
||||
pub lp_port: Option<u16>,
|
||||
|
||||
#[clap(long)]
|
||||
pub profit_margin_percent: Option<u64>,
|
||||
|
||||
@@ -60,13 +57,10 @@ pub async fn bond_nymnode(args: Args, client: SigningClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lp_address = args.lp_port.map(|port| format!("{}:{}", args.host, port));
|
||||
|
||||
let nymnode = nym_mixnet_contract_common::NymNode {
|
||||
host: args.host,
|
||||
custom_http_port: args.http_api_port,
|
||||
identity_key: args.identity_key,
|
||||
lp_address,
|
||||
};
|
||||
|
||||
let coin = Coin::new(args.amount, denom);
|
||||
|
||||
@@ -25,9 +25,6 @@ pub struct Args {
|
||||
#[clap(long)]
|
||||
pub custom_http_api_port: Option<u16>,
|
||||
|
||||
#[clap(long, help = "LP (Lewes Protocol) listener port (default: 41264)")]
|
||||
pub lp_port: Option<u16>,
|
||||
|
||||
#[clap(long)]
|
||||
pub profit_margin_percent: Option<u64>,
|
||||
|
||||
@@ -50,13 +47,10 @@ pub struct Args {
|
||||
pub async fn create_payload(args: Args, client: SigningClient) {
|
||||
let denom = client.current_chain_details().mix_denom.base.as_str();
|
||||
|
||||
let lp_address = args.lp_port.map(|port| format!("{}:{}", args.host, port));
|
||||
|
||||
let mixnode = nym_mixnet_contract_common::NymNode {
|
||||
host: args.host,
|
||||
custom_http_port: args.custom_http_api_port,
|
||||
identity_key: args.identity_key,
|
||||
lp_address,
|
||||
};
|
||||
|
||||
let coin = Coin::new(args.amount, denom);
|
||||
|
||||
@@ -19,16 +19,6 @@ pub struct Args {
|
||||
// equivalent to setting `custom_http_port` to `None`
|
||||
#[clap(long)]
|
||||
pub restore_default_http_port: bool,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "LP (Lewes Protocol) listener address (format: host:port)"
|
||||
)]
|
||||
pub lp_address: Option<String>,
|
||||
|
||||
// equivalent to setting `lp_address` to `None`
|
||||
#[clap(long)]
|
||||
pub restore_default_lp_address: bool,
|
||||
}
|
||||
|
||||
pub async fn update_config(args: Args, client: SigningClient) {
|
||||
@@ -49,8 +39,6 @@ pub async fn update_config(args: Args, client: SigningClient) {
|
||||
host: args.host,
|
||||
custom_http_port: args.custom_http_port,
|
||||
restore_default_http_port: args.restore_default_http_port,
|
||||
lp_address: args.lp_address,
|
||||
restore_default_lp_address: args.restore_default_lp_address,
|
||||
};
|
||||
|
||||
let res = client
|
||||
|
||||
@@ -241,23 +241,28 @@ impl Epoch {
|
||||
//
|
||||
// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd`
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
#[derive(Copy, Default)]
|
||||
pub enum EpochState {
|
||||
#[default]
|
||||
WaitingInitialisation,
|
||||
PublicKeySubmission { resharing: bool },
|
||||
DealingExchange { resharing: bool },
|
||||
VerificationKeySubmission { resharing: bool },
|
||||
VerificationKeyValidation { resharing: bool },
|
||||
VerificationKeyFinalization { resharing: bool },
|
||||
PublicKeySubmission {
|
||||
resharing: bool,
|
||||
},
|
||||
DealingExchange {
|
||||
resharing: bool,
|
||||
},
|
||||
VerificationKeySubmission {
|
||||
resharing: bool,
|
||||
},
|
||||
VerificationKeyValidation {
|
||||
resharing: bool,
|
||||
},
|
||||
VerificationKeyFinalization {
|
||||
resharing: bool,
|
||||
},
|
||||
InProgress,
|
||||
}
|
||||
|
||||
impl Default for EpochState {
|
||||
fn default() -> Self {
|
||||
Self::WaitingInitialisation
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EpochState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -373,11 +373,6 @@ pub struct NymNode {
|
||||
/// Base58-encoded ed25519 EdDSA public key.
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
|
||||
pub identity_key: IdentityKey,
|
||||
|
||||
/// Optional LP (Lewes Protocol) listener address for direct gateway connections.
|
||||
/// Format: "host:port", for example "1.1.1.1:41264" or "gateway.example.com:41264"
|
||||
#[serde(default)]
|
||||
pub lp_address: Option<String>,
|
||||
// TODO: I don't think we want to include sphinx keys here,
|
||||
// given we want to rotate them and keeping that in sync with contract will be a PITA
|
||||
}
|
||||
@@ -410,7 +405,6 @@ impl From<MixNode> for NymNode {
|
||||
host: value.host,
|
||||
custom_http_port: Some(value.http_api_port),
|
||||
identity_key: value.identity_key,
|
||||
lp_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +415,6 @@ impl From<Gateway> for NymNode {
|
||||
host: value.host,
|
||||
custom_http_port: None,
|
||||
identity_key: value.identity_key,
|
||||
lp_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,13 +437,6 @@ pub struct NodeConfigUpdate {
|
||||
// equivalent to setting `custom_http_port` to `None`
|
||||
#[serde(default)]
|
||||
pub restore_default_http_port: bool,
|
||||
|
||||
/// LP listener address for direct gateway connections (format: "host:port")
|
||||
pub lp_address: Option<String>,
|
||||
|
||||
// equivalent to setting `lp_address` to `None`
|
||||
#[serde(default)]
|
||||
pub restore_default_lp_address: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
use std::env;
|
||||
|
||||
let out_dir = env::var("OUT_DIR")?;
|
||||
let out_dir = env::var("OUT_DIR").context("missing OUT_DIR env variable")?;
|
||||
let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite");
|
||||
|
||||
// remove the db file if it already existed from previous build
|
||||
|
||||
@@ -127,6 +127,13 @@ pub enum CredentialProxyError {
|
||||
#[error("failed to create deposit")]
|
||||
DepositFailure,
|
||||
|
||||
#[error("failed to load jwt signing key from {path}: {err}")]
|
||||
JWTSigningKeyLoadFailure {
|
||||
path: String,
|
||||
#[source]
|
||||
err: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("can't obtain sufficient number of credential shares due to unavailable quorum")]
|
||||
UnavailableSigningQuorum,
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct QuorumStateChecker {
|
||||
cancellation_token: CancellationToken,
|
||||
check_interval: Duration,
|
||||
quorum_state: QuorumState,
|
||||
max_retries: u32,
|
||||
retry_initial_delay: Duration,
|
||||
}
|
||||
|
||||
impl QuorumStateChecker {
|
||||
@@ -42,6 +44,8 @@ impl QuorumStateChecker {
|
||||
quorum_state: QuorumState {
|
||||
available: Arc::new(Default::default()),
|
||||
},
|
||||
max_retries: 3,
|
||||
retry_initial_delay: Duration::from_secs(2),
|
||||
};
|
||||
|
||||
// first check MUST succeed, otherwise we shouldn't start
|
||||
@@ -56,7 +60,102 @@ impl QuorumStateChecker {
|
||||
self.quorum_state.clone()
|
||||
}
|
||||
|
||||
fn is_retryable_error(&self, err: &CredentialProxyError) -> bool {
|
||||
let err_str = err.to_string().to_lowercase();
|
||||
|
||||
// Check for DNS-related errors
|
||||
if err_str.contains("dns")
|
||||
|| err_str.contains("lookup")
|
||||
|| err_str.contains("name resolution")
|
||||
|| err_str.contains("temporary failure")
|
||||
|| err_str.contains("failed to lookup address")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a Tendermint RPC error (which could be DNS/timeout related)
|
||||
if let CredentialProxyError::NyxdFailure { source: nyxd_err } = err {
|
||||
let nyxd_err_str = nyxd_err.to_string().to_lowercase();
|
||||
if nyxd_err_str.contains("tendermint rpc request failed") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if nyxd_err.is_tendermint_response_timeout() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
self.check_quorum_state_with_retry().await
|
||||
}
|
||||
|
||||
async fn check_quorum_state_with_retry(&self) -> Result<bool, CredentialProxyError> {
|
||||
let mut last_error_msg = None;
|
||||
let delay = self.retry_initial_delay;
|
||||
|
||||
for attempt in 0..=self.max_retries {
|
||||
match self.check_quorum_state_once().await {
|
||||
Ok(result) => {
|
||||
if attempt > 0 {
|
||||
info!("quorum check succeeded after {} retry attempt(s)", attempt);
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
Err(err) => {
|
||||
let err_msg = err.to_string();
|
||||
|
||||
// Check if this error is retryable
|
||||
if !self.is_retryable_error(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
last_error_msg = Some(err_msg.clone());
|
||||
|
||||
if attempt >= self.max_retries {
|
||||
break;
|
||||
}
|
||||
|
||||
// Log the retry attempt
|
||||
warn!(
|
||||
"quorum check failed (attempt {}/{}): {}. Retrying in {:?}...",
|
||||
attempt + 1,
|
||||
self.max_retries + 1,
|
||||
err_msg,
|
||||
delay
|
||||
);
|
||||
|
||||
// Wait before retrying with exponential backoff
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try one final time to get the actual error
|
||||
match self.check_quorum_state_once().await {
|
||||
Ok(result) => {
|
||||
warn!(
|
||||
"quorum check succeeded on final attempt after {} retries",
|
||||
self.max_retries
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(error_msg) = last_error_msg {
|
||||
error!(
|
||||
"quorum check failed after {} retry attempts. Last error: {}",
|
||||
self.max_retries + 1,
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_quorum_state_once(&self) -> Result<bool, CredentialProxyError> {
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -93,7 +192,7 @@ impl QuorumStateChecker {
|
||||
break
|
||||
}
|
||||
_ = tokio::time::sleep(self.check_interval) => {
|
||||
match self.check_quorum_state().await {
|
||||
match self.check_quorum_state_with_retry().await {
|
||||
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
|
||||
Err(err) => error!("failed to check current quorum state: {err}"),
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::ticketbook_manager::TicketbookManager;
|
||||
use nym_compact_ecash::Base58;
|
||||
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
|
||||
CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse,
|
||||
PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest,
|
||||
TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse,
|
||||
TicketbookWalletSharesResponse,
|
||||
ObtainTicketBookSharesAsyncResponse, PartialVerificationKey, PartialVerificationKeysResponse,
|
||||
TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest,
|
||||
TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{Instrument, Level, error, info, span, warn};
|
||||
@@ -65,7 +65,7 @@ impl TicketbookManager {
|
||||
uuid: Uuid,
|
||||
request: TicketbookAsyncRequest,
|
||||
params: TicketbookObtainParams,
|
||||
) -> Result<TicketbookWalletSharesAsyncResponse, CredentialProxyError> {
|
||||
) -> Result<ObtainTicketBookSharesAsyncResponse, CredentialProxyError> {
|
||||
let requested_on = OffsetDateTime::now_utc();
|
||||
let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid);
|
||||
async move {
|
||||
@@ -110,7 +110,7 @@ impl TicketbookManager {
|
||||
}
|
||||
|
||||
// 4. in the meantime, return the id to the user
|
||||
Ok(TicketbookWalletSharesAsyncResponse { id, uuid })
|
||||
Ok(TicketbookWalletSharesAsyncResponse { id, uuid }.into())
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
|
||||
@@ -246,7 +246,7 @@ mod tests {
|
||||
let _exp_date_sigs = generate_expiration_date_signatures(
|
||||
sig_req.expiration_date.ecash_unix_timestamp(),
|
||||
&[signing_keys.secret_key()],
|
||||
&vec![signing_keys.verification_key()],
|
||||
&[signing_keys.verification_key()],
|
||||
&signing_keys.verification_key(),
|
||||
&[1],
|
||||
)?;
|
||||
@@ -263,7 +263,7 @@ mod tests {
|
||||
|
||||
let wallet = issuance.aggregate_signature_shares(
|
||||
&signing_keys.verification_key(),
|
||||
&vec![partial_wallet],
|
||||
&[partial_wallet],
|
||||
sig_req,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -30,6 +30,5 @@ nym-credentials-interface = { path = "../credentials-interface" }
|
||||
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-gateway-requests = { path = "../gateway-requests" }
|
||||
nym-gateway-storage = { path = "../gateway-storage" }
|
||||
nym-metrics = { path = "../nym-metrics" }
|
||||
nym-task = { path = "../task" }
|
||||
nym-validator-client = { path = "../client-libs/validator-client" }
|
||||
|
||||
@@ -59,13 +59,9 @@ impl traits::EcashManager for EcashManager {
|
||||
.verify(aggregated_verification_key)
|
||||
.map_err(|err| match err {
|
||||
CompactEcashError::ExpirationDateSignatureValidity => {
|
||||
nym_metrics::inc!("ecash_verification_failures_invalid_date_signature");
|
||||
EcashTicketError::MalformedTicketInvalidDateSignatures
|
||||
}
|
||||
_ => {
|
||||
nym_metrics::inc!("ecash_verification_failures_signature");
|
||||
EcashTicketError::MalformedTicket
|
||||
}
|
||||
_ => EcashTicketError::MalformedTicket,
|
||||
})?;
|
||||
|
||||
self.insert_pay_info(credential.pay_info.into(), insert_index)
|
||||
|
||||
@@ -222,13 +222,9 @@ impl SharedState {
|
||||
RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.master_key))
|
||||
{
|
||||
trace!("we already had cached api clients for epoch {epoch_id}");
|
||||
nym_metrics::inc!("ecash_verification_key_cache_hits");
|
||||
return Ok(mapped);
|
||||
}
|
||||
|
||||
// Cache miss - need to fetch and set epoch data
|
||||
nym_metrics::inc!("ecash_verification_key_cache_misses");
|
||||
|
||||
let write_guard = self.set_epoch_data(epoch_id).await?;
|
||||
let guard = write_guard.downgrade();
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use nym_credentials::ecash::utils::{EcashTime, cred_exp_date, ecash_today};
|
||||
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
|
||||
use nym_gateway_requests::models::CredentialSpendingRequest;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use time::{Date, OffsetDateTime};
|
||||
use tracing::*;
|
||||
|
||||
@@ -20,11 +19,6 @@ mod client_bandwidth;
|
||||
pub mod ecash;
|
||||
pub mod error;
|
||||
|
||||
// Histogram buckets for ecash verification duration (in seconds)
|
||||
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] = &[
|
||||
0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0,
|
||||
];
|
||||
|
||||
pub struct CredentialVerifier {
|
||||
credential: CredentialSpendingRequest,
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
@@ -68,7 +62,6 @@ impl CredentialVerifier {
|
||||
.await?;
|
||||
if spent {
|
||||
trace!("the credential has already been spent before at this gateway");
|
||||
nym_metrics::inc!("ecash_verification_failures_double_spending");
|
||||
return Err(Error::BandwidthCredentialAlreadySpent);
|
||||
}
|
||||
Ok(())
|
||||
@@ -110,9 +103,6 @@ impl CredentialVerifier {
|
||||
}
|
||||
|
||||
pub async fn verify(&mut self) -> Result<i64> {
|
||||
let start = Instant::now();
|
||||
nym_metrics::inc!("ecash_verification_attempts");
|
||||
|
||||
let received_at = OffsetDateTime::now_utc();
|
||||
let spend_date = ecash_today();
|
||||
|
||||
@@ -121,36 +111,15 @@ impl CredentialVerifier {
|
||||
let credential_type = TicketType::try_from_encoded(self.credential.data.payment.t_type)?;
|
||||
|
||||
if self.credential.data.payment.spend_value != 1 {
|
||||
nym_metrics::inc!("ecash_verification_failures_multiple_tickets");
|
||||
return Err(Error::MultipleTickets);
|
||||
}
|
||||
|
||||
if let Err(e) = self.check_credential_spending_date(spend_date.ecash_date()) {
|
||||
nym_metrics::inc!("ecash_verification_failures_invalid_spend_date");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
self.check_credential_spending_date(spend_date.ecash_date())?;
|
||||
self.check_local_db_for_double_spending(&serial_number)
|
||||
.await?;
|
||||
|
||||
// TODO: do we HAVE TO do it?
|
||||
let verify_result = self.cryptographically_verify_ticket().await;
|
||||
|
||||
// Track verification duration
|
||||
let duration = start.elapsed().as_secs_f64();
|
||||
nym_metrics::add_histogram_obs!(
|
||||
"ecash_verification_duration_seconds",
|
||||
duration,
|
||||
ECASH_VERIFICATION_DURATION_BUCKETS
|
||||
);
|
||||
|
||||
// Track epoch ID - use dynamic metric name via registry
|
||||
let epoch_id = self.credential.data.epoch_id;
|
||||
let epoch_metric = format!("nym_credential_verification_ecash_epoch_{}_verifications", epoch_id);
|
||||
nym_metrics::metrics_registry().maybe_register_and_inc(&epoch_metric, None);
|
||||
|
||||
// Check verification result after timing
|
||||
verify_result?;
|
||||
self.cryptographically_verify_ticket().await?;
|
||||
|
||||
let ticket_id = self.store_received_ticket(received_at).await?;
|
||||
self.async_verify_ticket(ticket_id);
|
||||
@@ -164,8 +133,6 @@ impl CredentialVerifier {
|
||||
.increase_bandwidth(bandwidth, cred_exp_date())
|
||||
.await?;
|
||||
|
||||
nym_metrics::inc!("ecash_verification_success");
|
||||
|
||||
Ok(self
|
||||
.bandwidth_storage_manager
|
||||
.client_bandwidth
|
||||
|
||||
@@ -15,7 +15,6 @@ base64.workspace = true
|
||||
bs58 = { workspace = true }
|
||||
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
|
||||
ctr = { workspace = true, optional = true }
|
||||
curve25519-dalek = { workspace = true, optional = true }
|
||||
digest = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
@@ -37,14 +36,18 @@ nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-fea
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
nym-test-utils = { path = "../test-utils" }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
|
||||
@@ -213,37 +213,6 @@ impl PublicKey {
|
||||
) -> Result<(), SignatureError> {
|
||||
self.0.verify(message.as_ref(), &signature.0)
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 public key to an X25519 public key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion by converting the Edwards point
|
||||
/// to Montgomery form. This is the same approach as libsodium's
|
||||
/// `crypto_sign_ed25519_pk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(x25519::PublicKey)` - The converted X25519 public key
|
||||
/// * `Err(Ed25519RecoveryError)` - If the conversion fails (e.g., low-order point)
|
||||
pub fn to_x25519(&self) -> Result<crate::asymmetric::x25519::PublicKey, Ed25519RecoveryError> {
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
|
||||
// Decompress the Ed25519 point
|
||||
let compressed = CompressedEdwardsY((*self).to_bytes());
|
||||
let edwards_point = compressed.decompress().ok_or_else(|| {
|
||||
Ed25519RecoveryError::MalformedBytes(SignatureError::from_source(
|
||||
"Failed to decompress Ed25519 point".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Convert to Montgomery form
|
||||
let montgomery = edwards_point.to_montgomery();
|
||||
|
||||
// Create X25519 public key
|
||||
crate::asymmetric::x25519::PublicKey::from_bytes(montgomery.as_bytes()).map_err(|_| {
|
||||
Ed25519RecoveryError::MalformedBytes(SignatureError::from_source(
|
||||
"Failed to convert to X25519".to_string(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
@@ -365,28 +334,6 @@ impl PrivateKey {
|
||||
let signature_bytes = self.sign(text).to_bytes();
|
||||
bs58::encode(signature_bytes).into_string()
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 private key to an X25519 private key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion via SHA-512 hash and clamping.
|
||||
/// This is the same approach as libsodium's `crypto_sign_ed25519_sk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// The converted X25519 private key
|
||||
pub fn to_x25519(&self) -> crate::asymmetric::x25519::PrivateKey {
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
// Hash the Ed25519 secret key with SHA-512
|
||||
let hash = Sha512::digest(self.0);
|
||||
|
||||
// Take first 32 bytes (clamping is done automatically by x25519_dalek::StaticSecret)
|
||||
let mut x25519_bytes = [0u8; 32];
|
||||
x25519_bytes.copy_from_slice(&hash[..32]);
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
crate::asymmetric::x25519::PrivateKey::from_bytes(&x25519_bytes)
|
||||
.expect("x25519 key conversion should never fail")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -570,27 +517,4 @@ mod tests {
|
||||
|
||||
assert_eq!(sig1.to_vec(), sig2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "rand")]
|
||||
fn test_ed25519_to_x25519_ecdh() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Create two ed25519 keypairs
|
||||
let alice_ed = KeyPair::new(&mut rng);
|
||||
let bob_ed = KeyPair::new(&mut rng);
|
||||
|
||||
// Convert to x25519
|
||||
let alice_x25519_private = alice_ed.private_key().to_x25519();
|
||||
let alice_x25519_public = alice_ed.public_key().to_x25519().unwrap();
|
||||
let bob_x25519_private = bob_ed.private_key().to_x25519();
|
||||
let bob_x25519_public = bob_ed.public_key().to_x25519().unwrap();
|
||||
|
||||
// Perform ECDH both ways
|
||||
let alice_shared = alice_x25519_private.diffie_hellman(&bob_x25519_public);
|
||||
let bob_shared = bob_x25519_private.diffie_hellman(&alice_x25519_public);
|
||||
|
||||
// Both should produce the same shared secret
|
||||
assert_eq!(alice_shared, bob_shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,51 @@ pub mod bs58_ed25519_pubkey {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vec_bs58_ed25519_pubkey {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
keys: &Vec<PublicKey>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut seq = serializer.serialize_seq(Some(keys.len()))?;
|
||||
for key in keys {
|
||||
seq.serialize_element(&Bs58KeyWrapper(*key))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<PublicKey>, D::Error> {
|
||||
let wrapped = Vec::<Bs58KeyWrapper>::deserialize(deserializer)?;
|
||||
Ok(wrapped.into_iter().map(|k| k.0).collect())
|
||||
}
|
||||
|
||||
struct Bs58KeyWrapper(PublicKey);
|
||||
|
||||
impl serde::Serialize for Bs58KeyWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
bs58_ed25519_pubkey::serialize(&self.0, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Bs58KeyWrapper {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(Bs58KeyWrapper(bs58_ed25519_pubkey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bs58_ed25519_signature {
|
||||
use crate::asymmetric::ed25519::Signature;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
@@ -33,3 +78,53 @@ pub mod bs58_ed25519_signature {
|
||||
Signature::from_base58_string(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jwt_simple::reexports::{anyhow, serde_json};
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[test]
|
||||
fn vec_bs58_ed25519_pubkey_json() -> anyhow::Result<()> {
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct KeysWrapper(#[serde(with = "vec_bs58_ed25519_pubkey")] Vec<PublicKey>);
|
||||
|
||||
use crate::asymmetric::ed25519;
|
||||
let mut rng = deterministic_rng();
|
||||
let empty = KeysWrapper(vec![]);
|
||||
let single_key = KeysWrapper(vec![PublicKey::from_base58_string(
|
||||
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
|
||||
)?]);
|
||||
let three_keys = KeysWrapper(vec![
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
]);
|
||||
|
||||
let se_empty = serde_json::to_string(&empty)?;
|
||||
let se_single_key = serde_json::to_string(&single_key)?;
|
||||
let se_three_keys = serde_json::to_string(&three_keys)?;
|
||||
|
||||
assert_eq!(se_empty, r#"[]"#);
|
||||
assert_eq!(
|
||||
se_single_key,
|
||||
r#"["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
se_three_keys,
|
||||
r#"["HmgHDV79LpnEaSUp8QZQwSroxVvS4RewF7yM9e7qu8y3","311xRh859qCd5MVqoPRCoNx26eYhLknGwtjzkkTJFGhf","A5BMp8WJ6Uk91U4JpWRv2Bc6X35AaRaSEy8QEWeAkaBv"]"#
|
||||
);
|
||||
|
||||
let empty_de = serde_json::from_str::<KeysWrapper>(&se_empty)?;
|
||||
let single_key_de = serde_json::from_str::<KeysWrapper>(&se_single_key)?;
|
||||
let three_keys_de = serde_json::from_str::<KeysWrapper>(&se_three_keys)?;
|
||||
|
||||
assert_eq!(empty, empty_de);
|
||||
assert_eq!(single_key, single_key_de);
|
||||
assert_eq!(three_keys, three_keys_de);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different contexts should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(
|
||||
key1, key2,
|
||||
"Different key material should produce different keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -82,7 +82,7 @@ mod tests {
|
||||
let exp_date_sigs = generate_expiration_date_signatures(
|
||||
sig_req.expiration_date.ecash_unix_timestamp(),
|
||||
&[keypair.secret_key()],
|
||||
&vec![keypair.verification_key()],
|
||||
&[keypair.verification_key()],
|
||||
&keypair.verification_key(),
|
||||
&[keypair.index.unwrap()],
|
||||
)
|
||||
@@ -106,14 +106,14 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let wallet = issuance
|
||||
.aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req)
|
||||
.aggregate_signature_shares(&keypair.verification_key(), &[partial_wallet], sig_req)
|
||||
.unwrap();
|
||||
|
||||
let mut issued = issuance.into_issued_ticketbook(wallet, 1);
|
||||
let coin_indices_signatures = generate_coin_indices_signatures(
|
||||
nym_credentials_interface::ecash_parameters(),
|
||||
&[keypair.secret_key()],
|
||||
&vec![keypair.verification_key()],
|
||||
&[keypair.verification_key()],
|
||||
&keypair.verification_key(),
|
||||
&[keypair.index.unwrap()],
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
bytes = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
@@ -52,5 +52,4 @@ workspace = true
|
||||
features = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -30,19 +30,26 @@
|
||||
use crate::ClientBuilder;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hickory_resolver::{
|
||||
ResolveError, TokioResolver,
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy},
|
||||
lookup_ip::{LookupIp, LookupIpIntoIter},
|
||||
TokioResolver,
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
name_server::TokioConnectionProvider,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use tracing::warn;
|
||||
use tracing::*;
|
||||
|
||||
mod constants;
|
||||
mod static_resolver;
|
||||
pub use static_resolver::*;
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
@@ -59,10 +66,6 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
|
||||
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
|
||||
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
|
||||
@@ -72,11 +75,17 @@ static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("hickory-dns resolver error: {hickory_error}")]
|
||||
#[allow(missing_docs)]
|
||||
/// Error occurring while resolving a hostname into an IP address.
|
||||
pub struct HickoryDnsError {
|
||||
#[from]
|
||||
hickory_error: ResolveError,
|
||||
pub enum ResolveError {
|
||||
#[error("invalid name: {0}")]
|
||||
InvalidNameError(String),
|
||||
#[error("hickory-dns resolver error: {0}")]
|
||||
ResolveError(#[from] hickory_resolver::ResolveError),
|
||||
#[error("high level lookup timed out")]
|
||||
Timeout,
|
||||
#[error("hostname not found in static lookup table")]
|
||||
StaticLookupMiss,
|
||||
}
|
||||
|
||||
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
|
||||
@@ -87,69 +96,116 @@ pub struct HickoryDnsError {
|
||||
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
|
||||
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
|
||||
/// internal `AsyncResolver`.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HickoryDnsResolver {
|
||||
// Since we might not have been called in the context of a
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
dont_use_shared: bool,
|
||||
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
||||
/// use of retries, server_ordering_strategy, etc. ends absolutely if this timeout is reached.
|
||||
overall_dns_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for HickoryDnsResolver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
fallback: Default::default(),
|
||||
static_base: Default::default(),
|
||||
dont_use_shared: Default::default(),
|
||||
overall_dns_timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 independent = self.dont_use_shared;
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
let resolver = resolver.get_or_try_init(|| {
|
||||
// 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 independent {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
})?;
|
||||
resolve(
|
||||
name,
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
independent,
|
||||
overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name.as_str()).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
if let Some(ref fallback) = maybe_fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// 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 independent {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.ok_or(e)? // if the shared resolver has no fallback return the original error
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
})?;
|
||||
|
||||
resolver.lookup_ip(name.as_str()).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
async fn resolve(
|
||||
name: Name,
|
||||
resolver: Arc<OnceCell<TokioResolver>>,
|
||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
independent: bool,
|
||||
overall_dns_timeout: Duration,
|
||||
) -> Result<Addrs, ResolveError> {
|
||||
let resolver = resolver.get_or_try_init(|| HickoryDnsResolver::new_resolver(independent))?;
|
||||
|
||||
// Attempt a lookup using the primary resolver
|
||||
let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
|
||||
let primary_err = match resolve_fut.await {
|
||||
Err(_) => ResolveError::Timeout,
|
||||
Ok(Ok(lookup)) => {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
Ok(addrs)
|
||||
})
|
||||
return Ok(addrs);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error: {e}");
|
||||
}
|
||||
e.into()
|
||||
}
|
||||
};
|
||||
|
||||
// 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 {
|
||||
debug!("checking static");
|
||||
let resolver =
|
||||
static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
|
||||
|
||||
if let Ok(addrs) = resolver.resolve(name).await {
|
||||
return Ok(addrs);
|
||||
}
|
||||
}
|
||||
|
||||
Err(primary_err)
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for SocketAddrs {
|
||||
@@ -162,28 +218,22 @@ impl Iterator for SocketAddrs {
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
|
||||
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
if let Some(ref fallback) = self.fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
|
||||
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
|
||||
resolver.lookup_ip(name).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(lookup)
|
||||
pub async fn resolve_str(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
||||
let n =
|
||||
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
||||
resolve(
|
||||
n,
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
self.static_base.clone(),
|
||||
self.dont_use_shared,
|
||||
self.overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
.map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
|
||||
}
|
||||
|
||||
/// Create a (lazy-initialized) resolver that is not shared across threads.
|
||||
@@ -194,16 +244,20 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
fn new_resolver(dont_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 dont_use_shared {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
fn new_resolver_system(dont_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 dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
@@ -215,8 +269,18 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_static_fallback(dont_use_shared: bool) -> StaticResolver {
|
||||
if !dont_use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
|
||||
shared_resolver
|
||||
.get_or_init(new_default_static_fallback)
|
||||
.clone()
|
||||
} else {
|
||||
new_default_static_fallback()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
|
||||
self.fallback = Some(Default::default());
|
||||
let _ = self
|
||||
.fallback
|
||||
@@ -231,22 +295,51 @@ impl HickoryDnsResolver {
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
}
|
||||
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
/// exists.
|
||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||
Some(self.static_base.as_ref()?.get()?.get_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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
|
||||
/// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
|
||||
///
|
||||
/// Timeout Defaults to 5 seconds
|
||||
/// Number of retries after lookup failure before giving up Defaults to 2
|
||||
///
|
||||
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
|
||||
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
|
||||
fn new_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new configured resolver");
|
||||
|
||||
let mut name_servers = NameServerConfigGroup::quad9_tls();
|
||||
name_servers.merge(NameServerConfigGroup::quad9_https());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_https());
|
||||
|
||||
configure_and_build_resolver(name_servers)
|
||||
}
|
||||
|
||||
fn configure_and_build_resolver(
|
||||
name_servers: NameServerConfigGroup,
|
||||
) -> Result<TokioResolver, ResolveError> {
|
||||
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
|
||||
let mut resolver_builder =
|
||||
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
|
||||
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
|
||||
// Cache successful responses for queries received by this resolver for 30 min minimum.
|
||||
resolver_builder.options_mut().positive_min_ttl = Some(Duration::from_secs(1800));
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
@@ -254,20 +347,27 @@ fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
/// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
|
||||
/// addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver_system() -> Result<TokioResolver, HickoryDnsError> {
|
||||
fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
let mut resolver_builder = TokioResolver::builder_tokio()?;
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reqwest_hickory_doh() {
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
async fn reqwest_with_custom_dns() {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver.into())
|
||||
.build()
|
||||
@@ -286,7 +386,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dns_lookup() -> Result<(), HickoryDnsError> {
|
||||
async fn dns_lookup() -> Result<(), ResolveError> {
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
|
||||
let domain = "ifconfig.me";
|
||||
@@ -296,4 +396,124 @@ mod test {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
|
||||
let example_domain = "non-existent.nymvpn.com";
|
||||
let mut resolver = HickoryDnsResolver {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = resolver.resolve_str(example_domain).await;
|
||||
assert!(result.is_err()); // should be NXDomain
|
||||
|
||||
resolver.static_base = Some(Default::default());
|
||||
|
||||
let mut addr_map = HashMap::new();
|
||||
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
|
||||
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);
|
||||
|
||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||
assert!(addrs.contains(&example_ip4));
|
||||
assert!(addrs.contains(&example_ip6));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod failure_test {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
/// Addresses drawn from blocks set off by RFC5737 (ipv4) and RFC3849 (ipv6)
|
||||
const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
|
||||
];
|
||||
|
||||
// Create a resolver that behaves the same as the custom configured router, except for the fact
|
||||
// that it is guaranteed to fail.
|
||||
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new faulty resolver");
|
||||
|
||||
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
853,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
broken_ns_group.merge(broken_ns_https);
|
||||
|
||||
configure_and_build_resolver(broken_ns_group)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dns_lookup_failures() -> Result<(), ResolveError> {
|
||||
let time_start = std::time::Instant::now();
|
||||
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
dont_use_shared: true,
|
||||
state: Arc::new(r),
|
||||
overall_dns_timeout: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
};
|
||||
build_broken_resolver()?;
|
||||
let domain = "ifconfig.me";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
|
||||
let duration = time_start.elapsed();
|
||||
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_to_static() -> Result<(), ResolveError> {
|
||||
let r = OnceCell::new();
|
||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
||||
.expect("broken resolver init error");
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
dont_use_shared: true,
|
||||
state: Arc::new(r),
|
||||
static_base: Some(Default::default()),
|
||||
overall_dns_timeout: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
};
|
||||
build_broken_resolver()?;
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "nymvpn.com";
|
||||
let _ = resolver
|
||||
.resolve_str(domain)
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub const NYM_API_DOMAIN: &str = "validator.nymtech.net";
|
||||
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(212, 71, 233, 232))];
|
||||
|
||||
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
|
||||
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
|
||||
|
||||
pub const NYM_FRONTDOOR_VERCEL_DOMAIN: &str = "nym-frontdoor.vercel.app";
|
||||
pub const NYM_FRONTDOOR_VERCEL_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
|
||||
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
|
||||
];
|
||||
|
||||
pub const NYM_FRONTDOOR_FASTLY_DOMAIN: &str = "nym-frontdoor.global.ssl.fastly.net";
|
||||
pub const NYM_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const NYMVPN_FRONTDOOR_FASTLY_DOMAIN: &str = "nymvpn-frontdoor.global.ssl.fastly.net";
|
||||
pub const NYMVPN_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const YELP_FASTLY_DOMAIN: &str = "yelp.global.ssl.fastly.net";
|
||||
pub const YELP_FASTLY_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
|
||||
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
|
||||
];
|
||||
|
||||
pub const VERCEL_APP_DOMAIN: &str = "vercel.app";
|
||||
pub const VERCEL_APP_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
|
||||
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
|
||||
];
|
||||
|
||||
pub const VERCEL_COM_DOMAIN: &str = "vercel.com";
|
||||
pub const VERCEL_COM_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(198, 169, 2, 129)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
|
||||
];
|
||||
|
||||
pub const NYM_COM_DOMAIN: &str = "nym.com";
|
||||
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
|
||||
|
||||
pub const NYM_STATS_API_DOMAIN: &str = "nym-statistics-api.nymtech.cc";
|
||||
pub const NYM_STATS_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(91, 92, 153, 96))];
|
||||
|
||||
pub const NYM_RPC_DOMAIN: &str = "rpc.nymtech.net";
|
||||
pub const NYM_RPC_IPS: &[IpAddr] = &[
|
||||
IpAddr::V4(Ipv4Addr::new(194, 182, 169, 49)),
|
||||
IpAddr::V4(Ipv4Addr::new(91, 92, 200, 116)),
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
0x2a04, 0xc43, 0xe00, 0x6f28, 0x400, 0xd8ff, 0xfe00, 0x1483,
|
||||
)),
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
0x2a04, 0xc46, 0xe00, 0x6f28, 0x4b3, 0x68ff, 0xfe00, 0x460,
|
||||
)),
|
||||
];
|
||||
|
||||
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());
|
||||
m.insert(NYM_VPN_API_DOMAIN.to_string(), NYM_VPN_API_IPS.to_vec());
|
||||
m.insert(
|
||||
NYM_FRONTDOOR_VERCEL_DOMAIN.to_string(),
|
||||
NYM_FRONTDOOR_VERCEL_IPS.to_vec(),
|
||||
);
|
||||
m.insert(
|
||||
NYM_FRONTDOOR_FASTLY_DOMAIN.to_string(),
|
||||
NYM_FRONTDOOR_FASTLY_IPS.to_vec(),
|
||||
);
|
||||
m.insert(
|
||||
NYMVPN_FRONTDOOR_FASTLY_DOMAIN.to_string(),
|
||||
NYMVPN_FRONTDOOR_FASTLY_IPS.to_vec(),
|
||||
);
|
||||
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
|
||||
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
|
||||
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
|
||||
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
|
||||
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
|
||||
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
|
||||
m
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::dns::ResolveError;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use tracing::*;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StaticResolver {
|
||||
static_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||
}
|
||||
|
||||
impl StaticResolver {
|
||||
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
|
||||
debug!("building static resolver");
|
||||
Self {
|
||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||
self.static_addr_map.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
Box::pin(async move {
|
||||
let addr_map = addr_map.lock().unwrap();
|
||||
let lookup = match addr_map.get(name.as_str()) {
|
||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||
Some(addrs) => addrs,
|
||||
};
|
||||
let addrs: Addrs = Box::new(
|
||||
lookup
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|ip_addr| SocketAddr::new(ip_addr, 0)),
|
||||
);
|
||||
|
||||
Ok(addrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_using_static_resolver() -> Result<(), Box<dyn StdError + Send + Sync>> {
|
||||
let example_domain = String::from("static.nymvpn.com");
|
||||
|
||||
// lookup for domain for which there is no entry
|
||||
let resolver = StaticResolver::new(HashMap::new());
|
||||
|
||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||
let result = resolver.resolve(url).await;
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Ok(_) => panic!("lookup with empty map should fail"),
|
||||
Err(e) => assert_eq!(e.to_string(), ResolveError::StaticLookupMiss.to_string()),
|
||||
}
|
||||
|
||||
// Successful lookup
|
||||
let mut addr_map = HashMap::new();
|
||||
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
|
||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||
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 mut addrs = resolver.resolve(url).await?;
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
||||
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ mod dns;
|
||||
mod path;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use dns::{HickoryDnsError, HickoryDnsResolver};
|
||||
pub use dns::{HickoryDnsResolver, ResolveError};
|
||||
|
||||
// helper for generating user agent based on binary information
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -902,6 +902,8 @@ impl Client {
|
||||
|
||||
if self.base_urls.len() > 1 {
|
||||
let orig = self.current_idx.load(Ordering::Relaxed);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut next = (orig + 1) % self.base_urls.len();
|
||||
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# CLAUDE.md - nym-kcp
|
||||
|
||||
KCP (Fast and Reliable ARQ Protocol) implementation providing reliability over UDP for the Nym network. This crate ensures ordered, reliable delivery of packets.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
|
||||
**KcpDriver** (src/driver.rs)
|
||||
- High-level interface for KCP operations
|
||||
- Manages single KCP session and I/O buffer
|
||||
- Handles packet encoding/decoding
|
||||
|
||||
**KcpSession** (src/session.rs)
|
||||
- Core KCP state machine
|
||||
- Manages send/receive windows, RTT, congestion control
|
||||
- Implements ARQ (Automatic Repeat Request) logic
|
||||
|
||||
**KcpPacket** (src/packet.rs)
|
||||
- Wire format: conv(4B) | cmd(1B) | frg(1B) | wnd(2B) | ts(4B) | sn(4B) | una(4B) | len(4B) | data
|
||||
- Commands: PSH (data), ACK, WND (window probe), ERR
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Conversation ID (conv)
|
||||
- Unique identifier for each KCP connection
|
||||
- Generated from hash of destination in nym-lp-node
|
||||
- Must match on both ends for successful communication
|
||||
|
||||
### Packet Flow
|
||||
1. **Send Path**: `send()` → Queue in send buffer → `fetch_outgoing()` → Wire
|
||||
2. **Receive Path**: Wire → `input()` → Process ACKs/data → Application buffer
|
||||
3. **Update Loop**: Call `update()` regularly to handle timeouts/retransmissions
|
||||
|
||||
### Reliability Mechanisms
|
||||
- **Sequence Numbers (sn)**: Track packet ordering
|
||||
- **Fragment Numbers (frg)**: Handle message fragmentation
|
||||
- **UNA (Unacknowledged)**: Cumulative ACK up to this sequence
|
||||
- **Selective ACK**: Via individual ACK packets
|
||||
- **Fast Retransmit**: Triggered by duplicate ACKs
|
||||
- **RTO Calculation**: Smoothed RTT with variance
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
```rust
|
||||
// In KcpSession
|
||||
MSS: 1400 // Maximum segment size
|
||||
WINDOW_SIZE: 128 // Send/receive window
|
||||
RTO_MIN: 100ms // Minimum retransmission timeout
|
||||
RTO_MAX: 60000ms // Maximum retransmission timeout
|
||||
FAST_RESEND: 2 // Fast retransmit threshold
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Processing Incoming Data
|
||||
```rust
|
||||
driver.input(data)?; // Decode and process packets
|
||||
let packets = driver.fetch_outgoing(); // Get any response packets
|
||||
```
|
||||
|
||||
### Sending Data
|
||||
```rust
|
||||
driver.send(&data); // Queue for sending
|
||||
driver.update(current_time); // Trigger flush
|
||||
let packets = driver.fetch_outgoing(); // Get packets to send
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- Enable `trace!` logs to see packet-level details
|
||||
- Monitor `ts_flush` vs `ts_current` for timing issues
|
||||
- Check `snd_wnd` and `rcv_wnd` for flow control problems
|
||||
- Watch for "fast retransmit" messages indicating packet loss
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- AIDEV-NOTE: MSS must account for Sphinx packet overhead
|
||||
- AIDEV-NOTE: Window size affects memory usage and throughput
|
||||
- Update frequency impacts latency vs CPU usage tradeoff
|
||||
- Conv ID must be consistent across session lifecycle
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "nym-kcp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wire_format"
|
||||
path = "bin/wire_format/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "session"
|
||||
path = "bin/session/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
byte_string = "1.0"
|
||||
bytes = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
ansi_term = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
@@ -1,80 +0,0 @@
|
||||
use bytes::BytesMut;
|
||||
use log::info;
|
||||
use nym_kcp::{packet::KcpPacket, session::KcpSession};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create two KcpSessions, simulating two endpoints
|
||||
let mut local_sess = KcpSession::new(42);
|
||||
let mut remote_sess = KcpSession::new(42);
|
||||
|
||||
// Set an MSS (max segment size) smaller than our data to force fragmentation
|
||||
local_sess.set_mtu(40);
|
||||
remote_sess.set_mtu(40);
|
||||
|
||||
// Some data larger than 30 bytes to demonstrate multi-fragment
|
||||
let big_data = b"The quick brown fox jumps over the lazy dog. This is a test.";
|
||||
|
||||
// --- LOCAL sends data ---
|
||||
info!(
|
||||
"Local: sending data: {:?}",
|
||||
String::from_utf8_lossy(big_data)
|
||||
);
|
||||
local_sess.send(big_data);
|
||||
|
||||
// Update local session's logic at time=0
|
||||
local_sess.update(100);
|
||||
|
||||
// LOCAL fetches outgoing (to be sent across the network)
|
||||
let outgoing_pkts = local_sess.fetch_outgoing();
|
||||
info!("Local: outgoing pkts: {:?}", outgoing_pkts);
|
||||
// Here you'd normally encrypt and send them. We’ll just encode them into a buffer.
|
||||
// Then that buffer is "transferred" to the remote side.
|
||||
let mut wire_buf = BytesMut::new();
|
||||
for pkt in &outgoing_pkts {
|
||||
pkt.encode(&mut wire_buf);
|
||||
}
|
||||
|
||||
// --- REMOTE receives data ---
|
||||
// The remote side "decrypts" (here we just clone) and decodes
|
||||
let mut remote_in = wire_buf.clone();
|
||||
|
||||
// Decode zero or more KcpPackets from remote_in
|
||||
while let Some(decoded_pkt) = KcpPacket::decode(&mut remote_in)? {
|
||||
info!(
|
||||
"Decoded packet, sn: {}, frg: {}",
|
||||
decoded_pkt.sn(),
|
||||
decoded_pkt.frg()
|
||||
);
|
||||
remote_sess.input(&decoded_pkt);
|
||||
}
|
||||
|
||||
// Update remote session to process newly received data
|
||||
remote_sess.update(100);
|
||||
|
||||
// The remote session likely generated ACK packets
|
||||
let ack_pkts = remote_sess.fetch_outgoing();
|
||||
|
||||
// --- LOCAL receives ACKs ---
|
||||
// The local side decodes them
|
||||
let mut ack_buf = BytesMut::new();
|
||||
for pkt in &ack_pkts {
|
||||
pkt.encode(&mut ack_buf);
|
||||
}
|
||||
|
||||
while let Some(decoded_pkt) = KcpPacket::decode(&mut ack_buf)? {
|
||||
local_sess.input(&decoded_pkt);
|
||||
}
|
||||
|
||||
// Update local again with some arbitrary time, e.g. 50 ms later
|
||||
local_sess.update(100);
|
||||
|
||||
// Just for completeness, local might produce more packets, though typically it's just empty now
|
||||
let _ = local_sess.fetch_outgoing();
|
||||
|
||||
// --- REMOTE reads reassembled data ---
|
||||
|
||||
let incoming = remote_sess.fetch_incoming();
|
||||
info!("Remote: incoming pkts: {:?}", incoming);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead as _, BufReader},
|
||||
};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use log::info;
|
||||
use nym_kcp::{
|
||||
codec::KcpCodec,
|
||||
packet::{KcpCommand, KcpPacket},
|
||||
};
|
||||
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1) Open a file and read lines
|
||||
let file = File::open("bin/wire_format/packets.txt")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
// 2) Create our KcpCodec
|
||||
let mut codec = KcpCodec {};
|
||||
|
||||
// We'll use out_buf for encoded data from *all* lines
|
||||
let mut out_buf = BytesMut::new();
|
||||
|
||||
let mut input_lines = vec![];
|
||||
|
||||
// Read lines & encode them all
|
||||
for (i, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
info!("Original line #{}: {}", i + 1, line);
|
||||
|
||||
// Construct a KcpPacket
|
||||
let pkt = KcpPacket::new(
|
||||
42,
|
||||
KcpCommand::Push,
|
||||
0,
|
||||
128,
|
||||
0,
|
||||
i as u32,
|
||||
0,
|
||||
line.as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
input_lines.push(pkt.clone_data());
|
||||
|
||||
// Encode (serialize) the packet into out_buf
|
||||
codec.encode(pkt, &mut out_buf)?;
|
||||
}
|
||||
|
||||
// === Simulate encryption & transmission ===
|
||||
// In reality, you might do `encrypt(&out_buf)` and then
|
||||
// send it over the network. We'll just clone here:
|
||||
let mut received_buf = out_buf.clone();
|
||||
|
||||
// 3) Now decode (deserialize) all packets at once
|
||||
// For demonstration, read them back out
|
||||
let mut count = 0;
|
||||
|
||||
let mut decoded_lines = vec![];
|
||||
|
||||
#[allow(clippy::while_let_loop)]
|
||||
loop {
|
||||
match codec.decode(&mut received_buf)? {
|
||||
Some(decoded_pkt) => {
|
||||
count += 1;
|
||||
// Convert packet data back to a string
|
||||
let decoded_str = String::from_utf8_lossy(decoded_pkt.data());
|
||||
info!("Decoded line #{}: {}", decoded_pkt.sn() + 1, decoded_str);
|
||||
|
||||
decoded_lines.push(decoded_pkt.clone_data());
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
for (i, j) in input_lines.iter().zip(decoded_lines.iter()) {
|
||||
assert_eq!(i, j);
|
||||
}
|
||||
|
||||
info!("Decoded {} lines total.", count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
packet 1
|
||||
packet 2
|
||||
packet 3
|
||||
packet 4
|
||||
packet 5
|
||||
packet 6
|
||||
packet 7
|
||||
packet 8
|
||||
packet 9
|
||||
packet 10
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::packet::KcpPacket;
|
||||
|
||||
/// Our codec for encoding/decoding KCP packets
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KcpCodec;
|
||||
|
||||
impl Decoder for KcpCodec {
|
||||
type Item = KcpPacket;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
// We simply delegate to `KcpPacket::decode`
|
||||
KcpPacket::decode(src).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<KcpPacket> for KcpCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, item: KcpPacket, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
// We just call `item.encode` to append the bytes
|
||||
item.encode(dst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use bytes::BytesMut;
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::{error::KcpError, packet::KcpPacket, session::KcpSession};
|
||||
|
||||
pub struct KcpDriver {
|
||||
session: KcpSession,
|
||||
buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl KcpDriver {
|
||||
pub fn conv_id(&self) -> Result<u32, KcpError> {
|
||||
Ok(self.session.conv)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, data: &[u8]) {
|
||||
self.session.send(data);
|
||||
}
|
||||
|
||||
pub fn input(&mut self, data: &[u8]) -> Result<Vec<KcpPacket>, KcpError> {
|
||||
self.buffer.extend_from_slice(data);
|
||||
let mut pkts = Vec::new();
|
||||
while let Ok(Some(pkt)) = KcpPacket::decode(&mut self.buffer) {
|
||||
debug!(
|
||||
"Decoded packet, cmd: {}, sn: {}, frg: {}",
|
||||
pkt.command(),
|
||||
pkt.sn(),
|
||||
pkt.frg()
|
||||
);
|
||||
self._input(&pkt)?;
|
||||
pkts.push(pkt);
|
||||
}
|
||||
Ok(pkts)
|
||||
}
|
||||
|
||||
fn _input(&mut self, pkt: &KcpPacket) -> Result<(), KcpError> {
|
||||
self.session.input(pkt);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_outgoing(&mut self) -> Vec<KcpPacket> {
|
||||
trace!(
|
||||
"ts_flush: {}, ts_current: {}",
|
||||
self.session.ts_flush(),
|
||||
self.session.ts_current()
|
||||
);
|
||||
self.session.fetch_outgoing()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tick: u64) {
|
||||
self.session.update(tick as u32);
|
||||
}
|
||||
|
||||
pub fn new(session: KcpSession) -> Self {
|
||||
KcpDriver {
|
||||
session,
|
||||
buffer: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KcpError {
|
||||
#[error("Invalid KCP command value: {0}")]
|
||||
InvalidCommand(u8),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod codec;
|
||||
pub mod driver;
|
||||
pub mod error;
|
||||
pub mod packet;
|
||||
pub mod session;
|
||||
@@ -1,219 +0,0 @@
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use log::{debug, trace};
|
||||
|
||||
use super::error::KcpError;
|
||||
|
||||
pub const KCP_HEADER: usize = 24;
|
||||
|
||||
/// Typed enumeration for KCP commands.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum KcpCommand {
|
||||
Push = 81, // cmd: push data
|
||||
Ack = 82, // cmd: ack
|
||||
Wask = 83, // cmd: window probe (ask)
|
||||
Wins = 84, // cmd: window size (tell)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KcpCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KcpCommand::Push => write!(f, "Push"),
|
||||
KcpCommand::Ack => write!(f, "Ack"),
|
||||
KcpCommand::Wask => write!(f, "Window Probe (ask)"),
|
||||
KcpCommand::Wins => write!(f, "Window Size (tell)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KcpCommand {
|
||||
type Error = KcpError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
81 => Ok(KcpCommand::Push),
|
||||
82 => Ok(KcpCommand::Ack),
|
||||
83 => Ok(KcpCommand::Wask),
|
||||
84 => Ok(KcpCommand::Wins),
|
||||
_ => Err(KcpError::InvalidCommand(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<u8> for KcpCommand {
|
||||
fn into(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// A single KCP packet (on-wire format).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KcpPacket {
|
||||
conv: u32,
|
||||
cmd: KcpCommand,
|
||||
frg: u8,
|
||||
wnd: u16,
|
||||
ts: u32,
|
||||
sn: u32,
|
||||
una: u32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl KcpPacket {
|
||||
pub fn new(
|
||||
conv: u32,
|
||||
cmd: KcpCommand,
|
||||
frg: u8,
|
||||
wnd: u16,
|
||||
ts: u32,
|
||||
sn: u32,
|
||||
una: u32,
|
||||
data: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
conv,
|
||||
cmd,
|
||||
frg,
|
||||
wnd,
|
||||
ts,
|
||||
sn,
|
||||
una,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command(&self) -> KcpCommand {
|
||||
self.cmd
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn clone_data(&self) -> Vec<u8> {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
pub fn conv(&self) -> u32 {
|
||||
self.conv
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> KcpCommand {
|
||||
self.cmd
|
||||
}
|
||||
|
||||
pub fn frg(&self) -> u8 {
|
||||
self.frg
|
||||
}
|
||||
|
||||
pub fn wnd(&self) -> u16 {
|
||||
self.wnd
|
||||
}
|
||||
|
||||
pub fn ts(&self) -> u32 {
|
||||
self.ts
|
||||
}
|
||||
|
||||
pub fn sn(&self) -> u32 {
|
||||
self.sn
|
||||
}
|
||||
|
||||
pub fn una(&self) -> u32 {
|
||||
self.una
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KcpPacket {
|
||||
fn default() -> Self {
|
||||
// We must pick some default command, e.g. `Push`.
|
||||
// Or omit `Default` if you don't need it.
|
||||
KcpPacket {
|
||||
conv: 0,
|
||||
cmd: KcpCommand::Push,
|
||||
frg: 0,
|
||||
wnd: 0,
|
||||
ts: 0,
|
||||
sn: 0,
|
||||
una: 0,
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KcpPacket {
|
||||
/// Attempt to decode a `KcpPacket` from `src`.
|
||||
/// Returns Ok(Some(pkt)) if fully available, Ok(None) if not enough data,
|
||||
/// or Err(...) if there's an invalid command or other error.
|
||||
pub fn decode(src: &mut BytesMut) -> Result<Option<Self>, KcpError> {
|
||||
trace!("Decoding buffer with len: {}", src.len());
|
||||
if src.len() < KCP_HEADER {
|
||||
// Not enough for even the header, this is usually fine, more data will arrive
|
||||
debug!("Not enough data for header");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Peek into the first 28 bytes
|
||||
let mut header = &src[..KCP_HEADER];
|
||||
|
||||
let conv = header.get_u32_le();
|
||||
let cmd_byte = header.get_u8();
|
||||
let frg = header.get_u8();
|
||||
let wnd = header.get_u16_le();
|
||||
let ts = header.get_u32_le();
|
||||
let sn = header.get_u32_le();
|
||||
let una = header.get_u32_le();
|
||||
let len = header.get_u32_le() as usize;
|
||||
|
||||
let total_needed = KCP_HEADER + len;
|
||||
if src.len() < total_needed {
|
||||
// We don't have the full packet yet
|
||||
debug!(
|
||||
"Not enough data for packet, want {}, have {}",
|
||||
total_needed,
|
||||
src.len()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Convert the raw u8 into our KcpCommand enum
|
||||
let cmd = KcpCommand::try_from(cmd_byte)?;
|
||||
|
||||
// Now we can read out the data portion
|
||||
let data = src[KCP_HEADER..KCP_HEADER + len].to_vec();
|
||||
|
||||
// Advance the buffer so it no longer contains this packet
|
||||
src.advance(total_needed);
|
||||
|
||||
Ok(Some(Self {
|
||||
conv,
|
||||
cmd,
|
||||
frg,
|
||||
wnd,
|
||||
ts,
|
||||
sn,
|
||||
una,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encode this packet into `dst`.
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
let total_len = KCP_HEADER + self.data.len();
|
||||
trace!("Encoding packet: {:?}, len: {}", self, total_len);
|
||||
dst.reserve(total_len);
|
||||
|
||||
dst.put_u32_le(self.conv);
|
||||
dst.put_u8(self.cmd.into()); // Convert enum -> u8
|
||||
dst.put_u8(self.frg);
|
||||
dst.put_u16_le(self.wnd);
|
||||
dst.put_u32_le(self.ts);
|
||||
dst.put_u32_le(self.sn);
|
||||
dst.put_u32_le(self.una);
|
||||
dst.put_u32_le(self.data.len() as u32);
|
||||
dst.extend_from_slice(&self.data);
|
||||
|
||||
trace!("Encoded packet: {:?}, len: {}", dst, dst.len());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "nym-lp-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,28 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Write;
|
||||
|
||||
pub fn format_debug_bytes(bytes: &[u8]) -> Result<String, fmt::Error> {
|
||||
let mut out = String::new();
|
||||
const LINE_LEN: usize = 16;
|
||||
for (i, chunk) in bytes.chunks(LINE_LEN).enumerate() {
|
||||
let line_prefix = format!("[{}:{}]", 1 + i * LINE_LEN, i * LINE_LEN + chunk.len());
|
||||
write!(out, "{line_prefix:12}")?;
|
||||
let mut line = String::new();
|
||||
for b in chunk {
|
||||
line.push_str(format!("{:02x} ", b).as_str());
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{line:48} {}",
|
||||
chunk
|
||||
.iter()
|
||||
.map(|&b| b as char)
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '.' })
|
||||
.collect::<String>()
|
||||
)?;
|
||||
|
||||
writeln!(out)?;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bincode = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ansi_term = { workspace = true }
|
||||
utoipa = { workspace = true, features = ["macros", "non_strict_integers"] }
|
||||
rand = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
harness = false
|
||||
@@ -1,71 +0,0 @@
|
||||
# Nym Lewes Protocol
|
||||
|
||||
The Lewes Protocol (LP) is a secure network communication protocol implemented in Rust. This README provides an overview of the protocol's session management and replay protection mechanisms.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
+-----------------+ +----------------+ +---------------+
|
||||
| Transport Layer |<--->| LP Session |<--->| LP Codec |
|
||||
| (UDP/TCP) | | - Replay prot. | | - Enc/dec only|
|
||||
+-----------------+ | - Crypto state | +---------------+
|
||||
+----------------+
|
||||
```
|
||||
|
||||
## Packet Structure
|
||||
|
||||
The protocol uses a structured packet format:
|
||||
|
||||
```
|
||||
+------------------+-------------------+------------------+
|
||||
| Header (16B) | Message | Trailer (16B) |
|
||||
| - Version (1B) | - Type (2B) | - Authentication |
|
||||
| - Reserved (3B) | - Content | - tag/MAC |
|
||||
| - SenderIdx (4B) | | |
|
||||
| - Counter (8B) | | |
|
||||
+------------------+-------------------+------------------+
|
||||
```
|
||||
|
||||
- Header contains protocol version, sender identification, and counter for replay protection
|
||||
- Message carries the actual payload with a type identifier
|
||||
- Trailer provides authentication and integrity verification (16 bytes)
|
||||
- Total packet size is constrained by MTU (1500 bytes), accounting for network overhead
|
||||
|
||||
## Sessions
|
||||
|
||||
- Sessions are managed by `LPSession` and `SessionManager` classes
|
||||
- Each session has unique receiving and sending indices to identify connections
|
||||
- Sessions maintain:
|
||||
- Cryptographic state (currently mocked implementations)
|
||||
- Counter for outgoing packets
|
||||
- Replay protection mechanism for incoming packets
|
||||
|
||||
## Session Management
|
||||
|
||||
- `SessionManager` handles session lifecycle (creation, retrieval, removal)
|
||||
- Sessions are stored in a thread-safe HashMap indexed by receiving index
|
||||
- The manager generates unique indices for new sessions
|
||||
- Sessions are Arc-wrapped for safe concurrent access
|
||||
|
||||
## Replay Protection
|
||||
|
||||
- Implemented in the `ReceivingKeyCounterValidator` class
|
||||
- Uses a bitmap-based approach to track received packet counters
|
||||
- The bitmap allows reordering of up to 1024 packets (configurable)
|
||||
- SIMD optimizations are used when available for performance
|
||||
|
||||
## Replay Protection Process
|
||||
|
||||
1. Quick validation (`will_accept_branchless`):
|
||||
- Checks if counter is valid before expensive operations
|
||||
- Detects duplicates, out-of-window packets
|
||||
|
||||
2. Marking packets (`mark_did_receive_branchless`):
|
||||
- Updates the bitmap to mark counter as received
|
||||
- Updates statistics and sliding window as needed
|
||||
|
||||
3. Window Sliding:
|
||||
- Automatically slides the acceptance window when new higher counters arrive
|
||||
- Clears bits for old counters that fall outside the window
|
||||
|
||||
This architecture effectively prevents replay attacks while allowing some packet reordering, an essential feature for secure network protocols.
|
||||
@@ -1,238 +0,0 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_sequential");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("sequential_counters", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
let counters: Vec<u64> = (0..size).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut validator = validator.clone();
|
||||
for &counter in &counters {
|
||||
let _ = black_box(validator.will_accept_branchless(counter));
|
||||
let _ = black_box(validator.mark_did_receive_branchless(counter));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_out_of_order");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("out_of_order_counters", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
let mut validator = validator.clone();
|
||||
for &counter in &counters {
|
||||
let _ = black_box(validator.will_accept_branchless(counter));
|
||||
let _ = black_box(validator.mark_did_receive_branchless(counter));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_thread_safety(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_thread_safety");
|
||||
group.sample_size(1000);
|
||||
|
||||
for size in [100, 1000, 10000] {
|
||||
group.throughput(Throughput::Elements(size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("thread_safe_validator", size),
|
||||
&size,
|
||||
|b, &size| {
|
||||
let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default()));
|
||||
let counters: Vec<u64> = (0..size).collect();
|
||||
|
||||
b.iter(|| {
|
||||
for &counter in &counters {
|
||||
let result = {
|
||||
let guard = validator.lock();
|
||||
black_box(guard.will_accept_branchless(counter))
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
let mut guard = validator.lock();
|
||||
let _ = black_box(guard.mark_did_receive_branchless(counter));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_window_sliding(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_window_sliding");
|
||||
group.sample_size(100);
|
||||
|
||||
for window_size in [128, 512, 1024] {
|
||||
group.throughput(Throughput::Elements(window_size));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("window_sliding", window_size),
|
||||
&window_size,
|
||||
|b, &window_size| {
|
||||
b.iter(|| {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// First fill the window with sequential packets
|
||||
for i in 0..window_size {
|
||||
let _ = black_box(validator.mark_did_receive_branchless(i));
|
||||
}
|
||||
|
||||
// Then jump ahead to force window sliding
|
||||
let _ = black_box(validator.mark_did_receive_branchless(window_size * 3));
|
||||
|
||||
// Try some packets in the new window
|
||||
for i in (window_size * 2 + 1)..(window_size * 3) {
|
||||
let _ = black_box(validator.will_accept_branchless(i));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark operations that would benefit from SIMD optimization
|
||||
fn bench_core_operations(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_core_operations");
|
||||
group.sample_size(1000);
|
||||
|
||||
// Create validators with different states
|
||||
let empty_validator = ReceivingKeyCounterValidator::default();
|
||||
let mut half_full_validator = ReceivingKeyCounterValidator::default();
|
||||
let mut full_validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Fill validators with different patterns
|
||||
for i in 0..512 {
|
||||
half_full_validator.mark_did_receive_branchless(i).unwrap();
|
||||
}
|
||||
|
||||
for i in 0..1024 {
|
||||
full_validator.mark_did_receive_branchless(i).unwrap();
|
||||
}
|
||||
|
||||
// Benchmark clearing operations
|
||||
group.bench_function("clear_empty_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = empty_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("clear_half_full_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = half_full_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function("clear_full_window", |b| {
|
||||
b.iter(|| {
|
||||
let mut validator = full_validator.clone();
|
||||
// Force window sliding that will clear bitmap
|
||||
let _: () = validator.mark_did_receive_branchless(2000).unwrap();
|
||||
black_box(());
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark thread safety with different thread counts
|
||||
fn bench_concurrency_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("replay_concurrency_scaling");
|
||||
group.sample_size(50);
|
||||
|
||||
for thread_count in [1, 2, 4, 8] {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("mutex_threads", thread_count),
|
||||
&thread_count,
|
||||
|b, &thread_count| {
|
||||
b.iter(|| {
|
||||
let validator = Arc::new(Mutex::new(ReceivingKeyCounterValidator::default()));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for t in 0..thread_count {
|
||||
let validator_clone = Arc::clone(&validator);
|
||||
let handle = std::thread::spawn(move || {
|
||||
let mut success_count = 0;
|
||||
for i in 0..100 {
|
||||
let counter = t * 1000 + i;
|
||||
let mut guard = validator_clone.lock();
|
||||
if guard.mark_did_receive_branchless(counter as u64).is_ok() {
|
||||
success_count += 1;
|
||||
}
|
||||
}
|
||||
success_count
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
let mut total = 0;
|
||||
for handle in handles {
|
||||
total += handle.join().unwrap();
|
||||
}
|
||||
|
||||
black_box(total)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
replay_benches,
|
||||
bench_sequential_counters,
|
||||
bench_out_of_order_counters,
|
||||
bench_thread_safety,
|
||||
bench_window_sliding,
|
||||
bench_core_operations,
|
||||
bench_concurrency_scaling
|
||||
);
|
||||
criterion_main!(replay_benches);
|
||||
@@ -1,560 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::{ClientHelloData, LpMessage, MessageType};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::LpError;
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload).
|
||||
///
|
||||
/// Assumes the input `src` contains exactly one complete packet. It does not handle
|
||||
/// stream fragmentation or provide replay protection checks (these belong at the session level).
|
||||
pub fn parse_lp_packet(src: &[u8]) -> Result<LpPacket, LpError> {
|
||||
// Minimum size check: LpHeader + Type + Trailer (for 0-payload message)
|
||||
let min_size = LpHeader::SIZE + 2 + TRAILER_LEN;
|
||||
if src.len() < min_size {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
|
||||
// Parse LpHeader
|
||||
let header = LpHeader::parse(&src[..LpHeader::SIZE])?; // Uses the new LpHeader::parse
|
||||
|
||||
// Parse Message Type
|
||||
let type_start = LpHeader::SIZE;
|
||||
let type_end = type_start + 2;
|
||||
let mut message_type_bytes = [0u8; 2];
|
||||
message_type_bytes.copy_from_slice(&src[type_start..type_end]);
|
||||
let message_type_raw = u16::from_le_bytes(message_type_bytes);
|
||||
let message_type = MessageType::from_u16(message_type_raw)
|
||||
.ok_or_else(|| LpError::invalid_message_type(message_type_raw))?;
|
||||
|
||||
// Calculate payload size based on total length
|
||||
let total_size = src.len();
|
||||
let message_size = total_size - min_size; // Size of the payload part
|
||||
|
||||
// Extract payload based on message type
|
||||
let message_start = type_end;
|
||||
let message_end = message_start + message_size;
|
||||
let payload_slice = &src[message_start..message_end]; // Bounds already checked by min_size and total_size calculation
|
||||
|
||||
let message = match message_type {
|
||||
MessageType::Busy => {
|
||||
if message_size != 0 {
|
||||
return Err(LpError::InvalidPayloadSize {
|
||||
expected: 0,
|
||||
actual: message_size,
|
||||
});
|
||||
}
|
||||
LpMessage::Busy
|
||||
}
|
||||
MessageType::Handshake => {
|
||||
// No size validation needed here for Handshake, it's variable
|
||||
LpMessage::Handshake(payload_slice.to_vec())
|
||||
}
|
||||
MessageType::EncryptedData => {
|
||||
// No size validation needed here for EncryptedData, it's variable
|
||||
LpMessage::EncryptedData(payload_slice.to_vec())
|
||||
}
|
||||
MessageType::ClientHello => {
|
||||
// ClientHello has structured data
|
||||
// Deserialize ClientHelloData from payload
|
||||
let data: ClientHelloData = bincode::deserialize(payload_slice)
|
||||
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
|
||||
LpMessage::ClientHello(data)
|
||||
}
|
||||
};
|
||||
|
||||
// Extract trailer
|
||||
let trailer_start = message_end;
|
||||
let trailer_end = trailer_start + TRAILER_LEN;
|
||||
// Check if trailer_end exceeds src length (shouldn't happen if min_size check passed and calculation is correct, but good for safety)
|
||||
if trailer_end > total_size {
|
||||
// This indicates an internal logic error or buffer manipulation issue
|
||||
return Err(LpError::InsufficientBufferSize); // Or a more specific internal error
|
||||
}
|
||||
let trailer_slice = &src[trailer_start..trailer_end];
|
||||
let mut trailer = [0u8; TRAILER_LEN];
|
||||
trailer.copy_from_slice(trailer_slice);
|
||||
|
||||
// Create and return the packet
|
||||
Ok(LpPacket {
|
||||
header,
|
||||
message,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes an LpPacket into the provided BytesMut buffer.
|
||||
pub fn serialize_lp_packet(item: &LpPacket, dst: &mut BytesMut) -> Result<(), LpError> {
|
||||
// Reserve approximate size - consider making this more accurate if needed
|
||||
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
|
||||
item.encode(dst); // Use the existing encode method on LpPacket
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add a new error variant for invalid message types (Moved from previous impl LpError block)
|
||||
impl LpError {
|
||||
pub fn invalid_message_type(message_type: u16) -> Self {
|
||||
LpError::InvalidMessageType(message_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Import standalone functions
|
||||
use super::{parse_lp_packet, serialize_lp_packet};
|
||||
// Keep necessary imports
|
||||
use crate::message::{LpMessage, MessageType};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::LpError;
|
||||
use bytes::BytesMut;
|
||||
|
||||
// === Updated Encode/Decode Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_busy() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create a Busy packet
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify the packet fields
|
||||
assert_eq!(decoded.header.protocol_version, 1);
|
||||
assert_eq!(decoded.header.session_id, 42);
|
||||
assert_eq!(decoded.header.counter, 123);
|
||||
assert!(matches!(decoded.message, LpMessage::Busy));
|
||||
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_handshake() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create a Handshake message packet
|
||||
let payload = vec![42u8; 80]; // Example payload size
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::Handshake(payload.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify the packet fields
|
||||
assert_eq!(decoded.header.protocol_version, 1);
|
||||
assert_eq!(decoded.header.session_id, 42);
|
||||
assert_eq!(decoded.header.counter, 123);
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::Handshake(decoded_payload) => {
|
||||
assert_eq!(decoded_payload, payload);
|
||||
}
|
||||
_ => panic!("Expected Handshake message"),
|
||||
}
|
||||
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_encrypted_data() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create an EncryptedData message packet
|
||||
let payload = vec![43u8; 124]; // Example payload size
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::EncryptedData(payload.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify the packet fields
|
||||
assert_eq!(decoded.header.protocol_version, 1);
|
||||
assert_eq!(decoded.header.session_id, 42);
|
||||
assert_eq!(decoded.header.counter, 123);
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::EncryptedData(decoded_payload) => {
|
||||
assert_eq!(decoded_payload, payload);
|
||||
}
|
||||
_ => panic!("Expected EncryptedData message"),
|
||||
}
|
||||
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
|
||||
}
|
||||
|
||||
// === Updated Incomplete Data Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_parse_incomplete_header() {
|
||||
// Create a buffer with incomplete header
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Only 4 bytes, not enough for LpHeader::SIZE
|
||||
|
||||
// Attempt to parse - expect error
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
LpError::InsufficientBufferSize
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_incomplete_message_type() {
|
||||
// Create a buffer with complete header but incomplete message type
|
||||
let mut buf = BytesMut::new();
|
||||
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(&[0]); // Only 1 byte of message type (need 2)
|
||||
|
||||
// Buffer length = 16 + 1 = 17. Min size = 16 + 2 + 16 = 34.
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
LpError::InsufficientBufferSize
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_incomplete_message_data() {
|
||||
// Create a buffer simulating Handshake but missing trailer and maybe partial payload
|
||||
let mut buf = BytesMut::new();
|
||||
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(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
|
||||
buf.extend_from_slice(&[42; 40]); // 40 bytes of payload data
|
||||
|
||||
// Buffer length = 16 + 2 + 40 = 58. Min size = 16 + 2 + 16 = 34.
|
||||
// Payload size calculated as 58 - 34 = 24.
|
||||
// Trailer expected at index 16 + 2 + 24 = 42.
|
||||
// Trailer read attempts src[42..58].
|
||||
// This *should* parse successfully based on the logic, but the trailer is garbage.
|
||||
// Let's rethink: parse_lp_packet assumes the *entire slice* is the packet.
|
||||
// If the slice doesn't end exactly where the trailer should, it's an error.
|
||||
// In this case, total length is 58. LpHeader(16) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
|
||||
// Trailer starts at 16+2+24 = 42. Ends at 42+16=58. It fits exactly.
|
||||
// This test *still* doesn't test incompleteness correctly for the datagram parser.
|
||||
|
||||
// Let's test a buffer that's *too short* even for header+type+trailer+min_payload
|
||||
let mut buf_too_short = BytesMut::new();
|
||||
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
|
||||
// No payload, no trailer. Length = 16+2=18. Min size = 34.
|
||||
let result_too_short = parse_lp_packet(&buf_too_short);
|
||||
assert!(result_too_short.is_err());
|
||||
assert!(matches!(
|
||||
result_too_short.unwrap_err(),
|
||||
LpError::InsufficientBufferSize
|
||||
));
|
||||
|
||||
// Test a buffer missing PART of the trailer
|
||||
let mut buf_partial_trailer = BytesMut::new();
|
||||
buf_partial_trailer.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf_partial_trailer.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf_partial_trailer.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf_partial_trailer.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
|
||||
let payload = vec![42u8; 20]; // Assume 20 byte payload
|
||||
buf_partial_trailer.extend_from_slice(&payload);
|
||||
buf_partial_trailer.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
|
||||
|
||||
// Total length = 16 + 2 + 20 + 15 = 53. Min size = 34. This passes.
|
||||
// Payload size = 53 - 34 = 19. <--- THIS IS WRONG. The parser assumes the length dictates payload.
|
||||
// Let's fix the parser logic slightly.
|
||||
|
||||
// The point is, parse_lp_packet expects a COMPLETE datagram. Providing less bytes
|
||||
// than LpHeader + Type + Trailer should fail. Providing *more* is also an issue unless
|
||||
// the length calculation works out perfectly. The most direct test is just < min_size.
|
||||
// Renaming test to reflect this.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_buffer_smaller_than_minimum() {
|
||||
// Test a buffer that's smaller than the smallest possible packet (LpHeader+Type+Trailer)
|
||||
let mut buf_too_short = BytesMut::new();
|
||||
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf_too_short.extend_from_slice(&MessageType::Busy.to_u16().to_le_bytes()); // Type
|
||||
buf_too_short.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
|
||||
// Length = 16 + 2 + 15 = 33. Min Size = 34.
|
||||
let result_too_short = parse_lp_packet(&buf_too_short);
|
||||
assert!(
|
||||
result_too_short.is_err(),
|
||||
"Expected error for buffer size 33, min 34"
|
||||
);
|
||||
assert!(matches!(
|
||||
result_too_short.unwrap_err(),
|
||||
LpError::InsufficientBufferSize
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_message_type() {
|
||||
// Create a buffer with invalid message type
|
||||
let mut buf = BytesMut::new();
|
||||
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
|
||||
// Need payload and trailer to meet min_size requirement
|
||||
let payload_size = 10; // Arbitrary
|
||||
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Attempt to parse
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::InvalidMessageType(255)) => {} // Expected error
|
||||
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_incorrect_payload_size_for_busy() {
|
||||
// Create a Busy packet but *with* a payload (which is invalid)
|
||||
let mut buf = BytesMut::new();
|
||||
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(&MessageType::Busy.to_u16().to_le_bytes()); // Busy type
|
||||
buf.extend_from_slice(&[42; 1]); // <<< Invalid 1-byte payload for Busy
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Total size = 16 + 2 + 1 + 16 = 35. Min size = 34.
|
||||
// Calculated payload size = 35 - 34 = 1.
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
LpError::InvalidPayloadSize {
|
||||
expected: 0,
|
||||
actual: 1
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// Test multiple packets simulation isn't relevant for datagram parsing
|
||||
// #[test]
|
||||
// fn test_multiple_packets_in_buffer() { ... }
|
||||
|
||||
// === ClientHello Serialization Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_client_hello() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create ClientHelloData
|
||||
let client_key = [42u8; 32];
|
||||
let protocol_version = 1u8;
|
||||
let salt = [99u8; 32];
|
||||
let hello_data = ClientHelloData {
|
||||
client_lp_public_key: client_key,
|
||||
protocol_version,
|
||||
salt,
|
||||
};
|
||||
|
||||
// Create a ClientHello message packet
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify the packet fields
|
||||
assert_eq!(decoded.header.protocol_version, 1);
|
||||
assert_eq!(decoded.header.session_id, 42);
|
||||
assert_eq!(decoded.header.counter, 123);
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.client_lp_public_key, client_key);
|
||||
assert_eq!(decoded_data.protocol_version, protocol_version);
|
||||
assert_eq!(decoded_data.salt, salt);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message"),
|
||||
}
|
||||
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_client_hello_with_fresh_salt() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create ClientHelloData with fresh salt
|
||||
let client_key = [7u8; 32];
|
||||
let hello_data = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// Create a ClientHello message packet
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 100,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [55; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.client_lp_public_key, client_key);
|
||||
assert_eq!(decoded_data.protocol_version, 1);
|
||||
assert_eq!(decoded_data.salt, hello_data.salt);
|
||||
|
||||
// Verify timestamp can be extracted
|
||||
let timestamp = decoded_data.extract_timestamp();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
// Timestamp should be within 2 seconds of now
|
||||
assert!((timestamp as i64 - now as i64).abs() <= 2);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_client_hello_malformed_bincode() {
|
||||
// Create a buffer with ClientHello message type but invalid bincode data
|
||||
let mut buf = BytesMut::new();
|
||||
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(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
|
||||
|
||||
// Add malformed bincode data (random bytes that won't deserialize to ClientHelloData)
|
||||
buf.extend_from_slice(&[0xFF; 50]); // Invalid bincode data
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Attempt to parse
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::DeserializationError(_)) => {} // Expected error
|
||||
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_client_hello_incomplete_bincode() {
|
||||
// Create a buffer with ClientHello but truncated bincode data
|
||||
let mut buf = BytesMut::new();
|
||||
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(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
|
||||
|
||||
// Add incomplete bincode data (only partial ClientHelloData)
|
||||
buf.extend_from_slice(&[0; 20]); // Too few bytes for full ClientHelloData
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Attempt to parse
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::DeserializationError(_)) => {} // Expected error
|
||||
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_different_protocol_versions() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
for version in [0u8, 1, 2, 255] {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let hello_data = ClientHelloData {
|
||||
client_lp_public_key: [version; 32],
|
||||
protocol_version: version,
|
||||
salt: [version.wrapping_add(1); 32],
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: version as u32,
|
||||
counter: version as u64,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [version; TRAILER_LEN],
|
||||
};
|
||||
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.protocol_version, version);
|
||||
assert_eq!(decoded_data.client_lp_public_key, [version; 32]);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message for version {}", version),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LpError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Snow Error: {0}")]
|
||||
SnowKeyError(#[from] snow::Error),
|
||||
|
||||
#[error("Snow Pattern Error: {0}")]
|
||||
SnowPatternError(String),
|
||||
|
||||
#[error("Noise Protocol Error: {0}")]
|
||||
NoiseError(#[from] NoiseError),
|
||||
|
||||
#[error("Replay detected: {0}")]
|
||||
Replay(#[from] ReplayError),
|
||||
|
||||
#[error("Invalid packet format: {0}")]
|
||||
InvalidPacketFormat(String),
|
||||
|
||||
#[error("Invalid message type: {0}")]
|
||||
InvalidMessageType(u16),
|
||||
|
||||
#[error("Payload too large: {0}")]
|
||||
PayloadTooLarge(usize),
|
||||
|
||||
#[error("Insufficient buffer size provided")]
|
||||
InsufficientBufferSize,
|
||||
|
||||
#[error("Attempted operation on closed session")]
|
||||
SessionClosed,
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Invalid state transition: tried input {input:?} in state {state:?}")]
|
||||
InvalidStateTransition { state: String, input: String },
|
||||
|
||||
#[error("Invalid payload size: expected {expected}, got {actual}")]
|
||||
InvalidPayloadSize { expected: usize, actual: usize },
|
||||
|
||||
#[error("Deserialization error: {0}")]
|
||||
DeserializationError(String),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidBase58String(#[from] bs58::decode::Error),
|
||||
|
||||
/// Session ID from incoming packet does not match any known session.
|
||||
#[error("Received packet with unknown session ID: {0}")]
|
||||
UnknownSessionId(u32),
|
||||
|
||||
/// Invalid state transition attempt in the state machine.
|
||||
#[error("Invalid input '{input}' for current state '{state}'")]
|
||||
InvalidStateTransitionAttempt { state: String, input: String },
|
||||
|
||||
/// Session is closed.
|
||||
#[error("Session is closed")]
|
||||
LpSessionClosed,
|
||||
|
||||
/// Session is processing an input event.
|
||||
#[error("Session is processing an input event")]
|
||||
LpSessionProcessing,
|
||||
|
||||
/// State machine not found.
|
||||
#[error("State machine not found for lp_id: {lp_id}")]
|
||||
StateMachineNotFound { lp_id: u32 },
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use nym_sphinx::{PrivateKey as SphinxPrivateKey, PublicKey as SphinxPublicKey};
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::LpError;
|
||||
|
||||
pub struct PrivateKey(SphinxPrivateKey);
|
||||
|
||||
impl Deref for PrivateKey {
|
||||
type Target = SphinxPrivateKey;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrivateKey {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
pub fn new() -> Self {
|
||||
let private_key = SphinxPrivateKey::random();
|
||||
Self(private_key)
|
||||
}
|
||||
|
||||
pub fn to_base58_string(&self) -> String {
|
||||
bs58::encode(self.0.to_bytes()).into_string()
|
||||
}
|
||||
|
||||
pub fn from_base58_string(s: &str) -> Result<Self, LpError> {
|
||||
let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap();
|
||||
Ok(PrivateKey(SphinxPrivateKey::from(bytes)))
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
let public_key = SphinxPublicKey::from(&self.0);
|
||||
PublicKey(public_key)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PublicKey(SphinxPublicKey);
|
||||
|
||||
impl Deref for PublicKey {
|
||||
type Target = SphinxPublicKey;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn to_base58_string(&self) -> String {
|
||||
bs58::encode(self.0.as_bytes()).into_string()
|
||||
}
|
||||
|
||||
pub fn from_base58_string(s: &str) -> Result<Self, LpError> {
|
||||
let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap();
|
||||
Ok(PublicKey(SphinxPublicKey::from(bytes)))
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, LpError> {
|
||||
Ok(PublicKey(SphinxPublicKey::from(*bytes)))
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PublicKey {
|
||||
fn default() -> Self {
|
||||
let private_key = PrivateKey::default();
|
||||
private_key.public_key()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Keypair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl Default for Keypair {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
pub fn new() -> Self {
|
||||
let private_key = PrivateKey::default();
|
||||
let public_key = private_key.public_key();
|
||||
Self {
|
||||
private_key,
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeypairReadable> for Keypair {
|
||||
fn from(keypair: KeypairReadable) -> Self {
|
||||
Self {
|
||||
private_key: PrivateKey::from_base58_string(&keypair.private).unwrap(),
|
||||
public_key: PublicKey::from_base58_string(&keypair.public).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Keypair> for KeypairReadable {
|
||||
fn from(keypair: &Keypair) -> Self {
|
||||
Self {
|
||||
private: keypair.private_key.to_base58_string(),
|
||||
public: keypair.public_key.to_base58_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for PrivateKey {
|
||||
type Err = LpError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
PrivateKey::from_base58_string(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PrivateKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_base58_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_base58_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, serde::Deserialize, Clone, ToSchema, Debug)]
|
||||
pub struct KeypairReadable {
|
||||
private: String,
|
||||
public: String,
|
||||
}
|
||||
|
||||
impl KeypairReadable {
|
||||
pub fn private_key(&self) -> Result<PrivateKey, LpError> {
|
||||
PrivateKey::from_base58_string(&self.private)
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> Result<PublicKey, LpError> {
|
||||
PublicKey::from_base58_string(&self.public)
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod codec;
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
pub mod message;
|
||||
pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod psk;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
pub mod session_manager;
|
||||
|
||||
use std::hash::{DefaultHasher, Hasher as _};
|
||||
|
||||
pub use error::LpError;
|
||||
use keypair::PublicKey;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::LpPacket;
|
||||
pub use psk::derive_psk;
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
|
||||
// Add the new state machine module
|
||||
pub mod state_machine;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
use crate::{keypair::Keypair, make_lp_id};
|
||||
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let id = make_lp_id(keypair_1.public_key(), keypair_2.public_key());
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// Initiator derives PSK from their perspective
|
||||
let initiator_psk = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
|
||||
let initiator_session = LpSession::new(
|
||||
id,
|
||||
true,
|
||||
&keypair_1.private_key().to_bytes(),
|
||||
&keypair_2.public_key().to_bytes(),
|
||||
&initiator_psk,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
// Responder derives same PSK from their perspective
|
||||
let responder_psk = derive_psk(keypair_2.private_key(), keypair_1.public_key(), &salt);
|
||||
|
||||
let responder_session = LpSession::new(
|
||||
id,
|
||||
false,
|
||||
&keypair_2.private_key().to_bytes(),
|
||||
&keypair_1.public_key().to_bytes(),
|
||||
&responder_psk,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
(initiator_session, responder_session)
|
||||
}
|
||||
|
||||
/// Generates a deterministic u32 session ID for the Lewes Protocol
|
||||
/// based on two public keys. The order of the keys does not matter.
|
||||
///
|
||||
/// Uses a different internal delimiter than `make_conv_id` to avoid
|
||||
/// potential collisions if the same key pairs were used in both contexts.
|
||||
fn make_id(key1_bytes: &[u8], key2_bytes: &[u8], sep: u8) -> u32 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
// Ensure consistent order for hashing to make the ID order-independent.
|
||||
// This guarantees make_lp_id(a, b) == make_lp_id(b, a).
|
||||
if key1_bytes < key2_bytes {
|
||||
hasher.write(key1_bytes);
|
||||
// Use a delimiter specific to Lewes Protocol ID generation
|
||||
// (0xCC chosen arbitrarily, could be any value different from 0xFF)
|
||||
hasher.write_u8(sep);
|
||||
hasher.write(key2_bytes);
|
||||
} else {
|
||||
hasher.write(key2_bytes);
|
||||
hasher.write_u8(sep);
|
||||
hasher.write(key1_bytes);
|
||||
}
|
||||
|
||||
// Truncate the u64 hash result to u32
|
||||
(hasher.finish() & 0xFFFF_FFFF) as u32
|
||||
}
|
||||
|
||||
pub fn make_lp_id(key1_bytes: &PublicKey, key2_bytes: &PublicKey) -> u32 {
|
||||
make_id(key1_bytes.as_bytes(), key2_bytes.as_bytes(), 0xCC)
|
||||
}
|
||||
|
||||
pub fn make_conv_id(src: &[u8], dst: &[u8]) -> u32 {
|
||||
make_id(src, dst, 0xFF)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::keypair::Keypair;
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{make_lp_id, sessions_for_tests, LpError};
|
||||
use bytes::BytesMut;
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let session = sessions_for_tests().0;
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42, // Matches session's sending_index assumption for this test
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet1.header.counter)
|
||||
.expect("Initial packet failed replay check");
|
||||
|
||||
// Mark received (simulating successful processing)
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet1.header.counter)
|
||||
.expect("Failed to mark initial packet received");
|
||||
|
||||
// === Packet 2 (Counter 0 - Replay, should fail check) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 0, // Same counter as before (replay)
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error, got {:?}", e),
|
||||
}
|
||||
// Do not mark received as it failed validation
|
||||
|
||||
// === Packet 3 (Counter 1 - Should succeed) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 1, // Incremented counter
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize packet
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3).unwrap();
|
||||
|
||||
// Parse packet
|
||||
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
|
||||
|
||||
// Perform replay check (should pass)
|
||||
session
|
||||
.receiving_counter_quick_check(parsed_packet3.header.counter)
|
||||
.expect("Packet 3 failed replay check");
|
||||
|
||||
// Mark received
|
||||
session
|
||||
.receiving_counter_mark(parsed_packet3.header.counter)
|
||||
.expect("Failed to mark packet 3 received");
|
||||
|
||||
// Verify validator state directly on the session
|
||||
let state = session.current_packet_cnt();
|
||||
assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
|
||||
assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
// Create session manager
|
||||
let local_manager = SessionManager::new();
|
||||
let remote_manager = SessionManager::new();
|
||||
let local_keypair = Keypair::default();
|
||||
let remote_keypair = Keypair::default();
|
||||
let lp_id = make_lp_id(local_keypair.public_key(), remote_keypair.public_key());
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
&local_keypair,
|
||||
remote_keypair.public_key(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(
|
||||
&remote_keypair,
|
||||
local_keypair.public_key(),
|
||||
false,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: lp_id,
|
||||
counter: 0,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf1 = BytesMut::new();
|
||||
serialize_lp_packet(&packet1, &mut buf1).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
|
||||
|
||||
// Process via SessionManager method (which should handle checks + marking)
|
||||
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
|
||||
// that encapsulates parse -> check -> process_noise -> mark.
|
||||
// For now, we simulate the steps using the retrieved session.
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(lp_id, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(lp_id, parsed_packet1.header.counter)
|
||||
.expect("Packet 1 mark failed");
|
||||
|
||||
// === Packet 2 (Counter 1 - Should succeed on same session) ===
|
||||
let packet2 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: lp_id,
|
||||
counter: 1,
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet2, &mut buf2).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
|
||||
|
||||
// Perform replay check
|
||||
local_manager
|
||||
.receiving_counter_quick_check(lp_id, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 check failed");
|
||||
// Mark received
|
||||
local_manager
|
||||
.receiving_counter_mark(lp_id, parsed_packet2.header.counter)
|
||||
.expect("Packet 2 mark failed");
|
||||
|
||||
// === Packet 3 (Counter 0 - Replay, should fail check) ===
|
||||
let packet3 = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: lp_id,
|
||||
counter: 0, // Replay of first packet
|
||||
},
|
||||
message: LpMessage::Busy,
|
||||
trailer: [0u8; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
let mut buf3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet3, &mut buf3).unwrap();
|
||||
|
||||
// Parse
|
||||
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
|
||||
|
||||
// Perform replay check (should fail)
|
||||
let replay_result =
|
||||
local_manager.receiving_counter_quick_check(lp_id, parsed_packet3.header.counter);
|
||||
assert!(replay_result.is_err());
|
||||
match replay_result.unwrap_err() {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
|
||||
}
|
||||
e => panic!("Expected replay error for packet 3, got {:?}", e),
|
||||
}
|
||||
// Do not mark received
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientHelloData {
|
||||
/// Client's LP x25519 public key (32 bytes)
|
||||
pub client_lp_public_key: [u8; 32],
|
||||
/// Protocol version for future compatibility
|
||||
pub protocol_version: u8,
|
||||
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
|
||||
pub salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl ClientHelloData {
|
||||
/// Generates a new ClientHelloData with fresh salt.
|
||||
///
|
||||
/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_lp_public_key` - Client's x25519 public key
|
||||
/// * `protocol_version` - Protocol version number
|
||||
pub fn new_with_fresh_salt(client_lp_public_key: [u8; 32], protocol_version: u8) -> Self {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Generate salt: timestamp + nonce
|
||||
let mut salt = [0u8; 32];
|
||||
|
||||
// First 8 bytes: current timestamp as u64 little-endian
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
salt[..8].copy_from_slice(×tamp.to_le_bytes());
|
||||
|
||||
// Last 24 bytes: random nonce
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut salt[8..]);
|
||||
|
||||
Self {
|
||||
client_lp_public_key,
|
||||
protocol_version,
|
||||
salt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the timestamp from the salt.
|
||||
///
|
||||
/// # Returns
|
||||
/// Unix timestamp in seconds
|
||||
pub fn extract_timestamp(&self) -> u64 {
|
||||
let mut timestamp_bytes = [0u8; 8];
|
||||
timestamp_bytes.copy_from_slice(&self.salt[..8]);
|
||||
u64::from_le_bytes(timestamp_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum MessageType {
|
||||
Busy = 0x0000,
|
||||
Handshake = 0x0001,
|
||||
EncryptedData = 0x0002,
|
||||
ClientHello = 0x0003,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
pub(crate) fn from_u16(value: u16) -> Option<Self> {
|
||||
match value {
|
||||
0x0000 => Some(MessageType::Busy),
|
||||
0x0001 => Some(MessageType::Handshake),
|
||||
0x0002 => Some(MessageType::EncryptedData),
|
||||
0x0003 => Some(MessageType::ClientHello),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_u16(&self) -> u16 {
|
||||
match self {
|
||||
MessageType::Busy => 0x0000,
|
||||
MessageType::Handshake => 0x0001,
|
||||
MessageType::EncryptedData => 0x0002,
|
||||
MessageType::ClientHello => 0x0003,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LpMessage {
|
||||
Busy,
|
||||
Handshake(Vec<u8>),
|
||||
EncryptedData(Vec<u8>),
|
||||
ClientHello(ClientHelloData),
|
||||
}
|
||||
|
||||
impl Display for LpMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LpMessage::Busy => write!(f, "Busy"),
|
||||
LpMessage::Handshake(_) => write!(f, "Handshake"),
|
||||
LpMessage::EncryptedData(_) => write!(f, "EncryptedData"),
|
||||
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LpMessage {
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
match self {
|
||||
LpMessage::Busy => &[],
|
||||
LpMessage::Handshake(payload) => payload,
|
||||
LpMessage::EncryptedData(payload) => payload,
|
||||
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
LpMessage::Busy => true,
|
||||
LpMessage::Handshake(payload) => payload.is_empty(),
|
||||
LpMessage::EncryptedData(payload) => payload.is_empty(),
|
||||
LpMessage::ClientHello(_) => false, // Always has data
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
LpMessage::Busy => 0,
|
||||
LpMessage::Handshake(payload) => payload.len(),
|
||||
LpMessage::EncryptedData(payload) => payload.len(),
|
||||
LpMessage::ClientHello(_) => 65, // 32 bytes key + 1 byte version + 32 bytes salt
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> MessageType {
|
||||
match self {
|
||||
LpMessage::Busy => MessageType::Busy,
|
||||
LpMessage::Handshake(_) => MessageType::Handshake,
|
||||
LpMessage::EncryptedData(_) => MessageType::EncryptedData,
|
||||
LpMessage::ClientHello(_) => MessageType::ClientHello,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_content(&self, dst: &mut BytesMut) {
|
||||
match self {
|
||||
LpMessage::Busy => { /* No content */ }
|
||||
LpMessage::Handshake(payload) => {
|
||||
dst.put_slice(payload);
|
||||
}
|
||||
LpMessage::EncryptedData(payload) => {
|
||||
dst.put_slice(payload);
|
||||
}
|
||||
LpMessage::ClientHello(data) => {
|
||||
// Serialize ClientHelloData using bincode
|
||||
let serialized =
|
||||
bincode::serialize(data).expect("Failed to serialize ClientHelloData");
|
||||
dst.put_slice(&serialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packet::{LpHeader, TRAILER_LEN};
|
||||
use crate::LpPacket;
|
||||
|
||||
#[test]
|
||||
fn encoding() {
|
||||
let message = LpMessage::EncryptedData(vec![11u8; 124]);
|
||||
|
||||
let resp_header = LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 0,
|
||||
counter: 0,
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: resp_header,
|
||||
message,
|
||||
trailer: [80; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Just print packet for debug, will be captured in test output
|
||||
println!("{packet:?}");
|
||||
|
||||
// Verify message type
|
||||
assert!(matches!(packet.message.typ(), MessageType::EncryptedData));
|
||||
|
||||
// Verify correct data in message
|
||||
match &packet.message {
|
||||
LpMessage::EncryptedData(data) => {
|
||||
assert_eq!(*data, vec![11u8; 124]);
|
||||
}
|
||||
_ => panic!("Wrong message type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_generation() {
|
||||
let client_key = [1u8; 32];
|
||||
let hello1 = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
let hello2 = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// Different salts should be generated
|
||||
assert_ne!(hello1.salt, hello2.salt);
|
||||
|
||||
// But timestamps should be very close (within 1 second)
|
||||
let ts1 = hello1.extract_timestamp();
|
||||
let ts2 = hello2.extract_timestamp();
|
||||
assert!((ts1 as i64 - ts2 as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_timestamp_extraction() {
|
||||
let client_key = [2u8; 32];
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
let timestamp = hello.extract_timestamp();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Timestamp should be within 1 second of now
|
||||
assert!((timestamp as i64 - now as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_format() {
|
||||
let client_key = [3u8; 32];
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// First 8 bytes should be non-zero timestamp
|
||||
let timestamp_bytes = &hello.salt[..8];
|
||||
assert_ne!(timestamp_bytes, &[0u8; 8]);
|
||||
|
||||
// Salt should be 32 bytes total
|
||||
assert_eq!(hello.salt.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Sans-IO Noise protocol state machine, adapted from noise-psq.
|
||||
|
||||
use snow::{params::NoiseParams, TransportState};
|
||||
use thiserror::Error;
|
||||
|
||||
// --- Error Definition ---
|
||||
|
||||
/// Errors related to the Noise protocol state machine.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NoiseError {
|
||||
#[error("encountered a Noise decryption error")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("encountered a Noise Protocol error - {0}")]
|
||||
ProtocolError(snow::Error),
|
||||
|
||||
#[error("operation is invalid in the current protocol state")]
|
||||
IncorrectStateError,
|
||||
|
||||
#[error("Other Noise-related error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<snow::Error> for NoiseError {
|
||||
fn from(err: snow::Error) -> Self {
|
||||
match err {
|
||||
snow::Error::Decrypt => NoiseError::DecryptionError,
|
||||
err => NoiseError::ProtocolError(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Protocol State and Structs ---
|
||||
|
||||
/// Represents the possible states of the Noise protocol machine.
|
||||
#[derive(Debug)]
|
||||
pub enum NoiseProtocolState {
|
||||
/// The protocol is currently performing the handshake.
|
||||
/// Contains the Snow handshake state.
|
||||
Handshaking(Box<snow::HandshakeState>),
|
||||
|
||||
/// The handshake is complete, and the protocol is in transport mode.
|
||||
/// Contains the Snow transport state.
|
||||
Transport(TransportState),
|
||||
|
||||
/// The protocol has encountered an unrecoverable error.
|
||||
/// Stores the error description.
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
/// The core sans-io Noise protocol state machine.
|
||||
#[derive(Debug)]
|
||||
pub struct NoiseProtocol {
|
||||
state: NoiseProtocolState,
|
||||
// We might need buffers for incoming/outgoing data later if we add internal buffering
|
||||
// read_buffer: Vec<u8>,
|
||||
// write_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents the outcome of processing received bytes via `read_message`.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ReadResult {
|
||||
/// A handshake or transport message was successfully processed, but yielded no application data
|
||||
/// and did not complete the handshake.
|
||||
NoOp,
|
||||
/// A complete application data message was decrypted.
|
||||
DecryptedData(Vec<u8>),
|
||||
/// The handshake successfully completed during this read operation.
|
||||
HandshakeComplete,
|
||||
// NOTE: NeedMoreBytes variant removed as read_message expects full frames.
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
impl NoiseProtocol {
|
||||
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
||||
///
|
||||
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
||||
pub fn new(initial_state: snow::HandshakeState) -> Self {
|
||||
NoiseProtocol {
|
||||
state: NoiseProtocolState::Handshaking(Box::new(initial_state)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a single, complete incoming Noise message frame.
|
||||
///
|
||||
/// Assumes the caller handles buffering and framing to provide one full message.
|
||||
/// Returns the result of processing the message.
|
||||
pub fn read_message(&mut self, input: &[u8]) -> Result<ReadResult, NoiseError> {
|
||||
// Allocate a buffer large enough for the maximum possible Noise message size.
|
||||
// TODO: Consider reusing a buffer for efficiency.
|
||||
let mut buffer = vec![0u8; 65535]; // Max Noise message size
|
||||
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Handshaking(handshake_state) => {
|
||||
match handshake_state.read_message(input, &mut buffer) {
|
||||
Ok(_) => {
|
||||
if handshake_state.is_handshake_finished() {
|
||||
// Transition to Transport state.
|
||||
let current_state = std::mem::replace(
|
||||
&mut self.state,
|
||||
// Temporary placeholder needed for mem::replace
|
||||
NoiseProtocolState::Failed(
|
||||
NoiseError::IncorrectStateError.to_string(),
|
||||
),
|
||||
);
|
||||
if let NoiseProtocolState::Handshaking(state_to_convert) = current_state
|
||||
{
|
||||
match state_to_convert.into_transport_mode() {
|
||||
Ok(transport_state) => {
|
||||
self.state = NoiseProtocolState::Transport(transport_state);
|
||||
Ok(ReadResult::HandshakeComplete)
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Should be unreachable
|
||||
let err = NoiseError::IncorrectStateError;
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
} else {
|
||||
// Handshake continues
|
||||
Ok(ReadResult::NoOp)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Transport(transport_state) => {
|
||||
match transport_state.read_message(input, &mut buffer) {
|
||||
Ok(len) => Ok(ReadResult::DecryptedData(buffer[..len].to_vec())),
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Failed(_) => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if there are pending handshake messages to send.
|
||||
///
|
||||
/// If in Handshaking state and it's our turn, generates the message.
|
||||
/// Transitions state to Transport if the handshake completes after this message.
|
||||
/// Returns `None` if not in Handshaking state or not our turn.
|
||||
pub fn get_bytes_to_send(&mut self) -> Option<Result<Vec<u8>, NoiseError>> {
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Handshaking(handshake_state) => {
|
||||
if handshake_state.is_my_turn() {
|
||||
let mut buffer = vec![0u8; 65535];
|
||||
match handshake_state.write_message(&[], &mut buffer) {
|
||||
// Empty payload for handshake msg
|
||||
Ok(len) => {
|
||||
if handshake_state.is_handshake_finished() {
|
||||
// Transition to Transport state.
|
||||
let current_state = std::mem::replace(
|
||||
&mut self.state,
|
||||
NoiseProtocolState::Failed(
|
||||
NoiseError::IncorrectStateError.to_string(),
|
||||
),
|
||||
);
|
||||
if let NoiseProtocolState::Handshaking(state_to_convert) =
|
||||
current_state
|
||||
{
|
||||
match state_to_convert.into_transport_mode() {
|
||||
Ok(transport_state) => {
|
||||
self.state =
|
||||
NoiseProtocolState::Transport(transport_state);
|
||||
Some(Ok(buffer[..len].to_vec())) // Return final handshake msg
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state =
|
||||
NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Should be unreachable
|
||||
let err = NoiseError::IncorrectStateError;
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
} else {
|
||||
// Handshake continues
|
||||
Some(Ok(buffer[..len].to_vec()))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Some(Err(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not our turn
|
||||
None
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Transport(_) | NoiseProtocolState::Failed(_) => {
|
||||
// No handshake messages to send in these states
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypts an application data payload for sending during the Transport phase.
|
||||
///
|
||||
/// Returns the ciphertext (payload + 16-byte tag).
|
||||
/// Errors if not in Transport state or encryption fails.
|
||||
pub fn write_message(&mut self, payload: &[u8]) -> Result<Vec<u8>, NoiseError> {
|
||||
match &mut self.state {
|
||||
NoiseProtocolState::Transport(transport_state) => {
|
||||
let mut buffer = vec![0u8; payload.len() + 16]; // Payload + tag
|
||||
match transport_state.write_message(payload, &mut buffer) {
|
||||
Ok(len) => Ok(buffer[..len].to_vec()),
|
||||
Err(e) => {
|
||||
let err = NoiseError::from(e);
|
||||
self.state = NoiseProtocolState::Failed(err.to_string());
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
NoiseProtocolState::Handshaking(_) | NoiseProtocolState::Failed(_) => {
|
||||
Err(NoiseError::IncorrectStateError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the protocol is in the transport phase (handshake complete).
|
||||
pub fn is_transport(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Transport(_))
|
||||
}
|
||||
|
||||
/// Returns true if the protocol has failed.
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Failed(_))
|
||||
}
|
||||
|
||||
/// Check if the handshake has finished and the protocol is in transport mode.
|
||||
pub fn is_handshake_finished(&self) -> bool {
|
||||
matches!(self.state, NoiseProtocolState::Transport(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_noise_state(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
let psk_index = 3;
|
||||
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 = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
let psk_index = 3;
|
||||
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))
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::LpMessage;
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use crate::LpError;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_lp_common::format_debug_bytes;
|
||||
use parking_lot::Mutex;
|
||||
use std::fmt::Write;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_HEADER_LEN: usize = 8;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const IP_HEADER_LEN: usize = 40; // v4 - 20, v6 - 40
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const MTU: usize = 1500;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_OVERHEAD: usize = UDP_HEADER_LEN + IP_HEADER_LEN;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const TRAILER_LEN: usize = 16;
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const UDP_PAYLOAD_SIZE: usize = MTU - UDP_OVERHEAD - TRAILER_LEN;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LpPacket {
|
||||
pub(crate) header: LpHeader,
|
||||
pub(crate) message: LpMessage,
|
||||
pub(crate) trailer: [u8; TRAILER_LEN],
|
||||
}
|
||||
|
||||
impl Debug for LpPacket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", format_debug_bytes(&self.debug_bytes())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl LpPacket {
|
||||
pub fn new(header: LpHeader, message: LpMessage) -> Self {
|
||||
Self {
|
||||
header,
|
||||
message,
|
||||
trailer: [0; TRAILER_LEN],
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a hash of the message payload
|
||||
///
|
||||
/// This can be used for message integrity verification or deduplication
|
||||
pub fn hash_payload(&self) -> [u8; 32] {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
// Include message type and content in the hash
|
||||
buffer.put_slice(&(self.message.typ() as u16).to_le_bytes());
|
||||
self.message.encode_content(&mut buffer);
|
||||
|
||||
hasher.update(&buffer);
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
pub fn hash_payload_hex(&self) -> String {
|
||||
let hash = self.hash_payload();
|
||||
hash.iter()
|
||||
.fold(String::with_capacity(hash.len() * 2), |mut acc, byte| {
|
||||
let _ = write!(acc, "{:02x}", byte);
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &LpMessage {
|
||||
&self.message
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &LpHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = BytesMut::new();
|
||||
self.encode(&mut bytes);
|
||||
bytes.freeze().to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
self.header.encode(dst);
|
||||
|
||||
dst.put_slice(&(self.message.typ() as u16).to_le_bytes());
|
||||
self.message.encode_content(dst);
|
||||
|
||||
dst.put_slice(&self.trailer)
|
||||
}
|
||||
|
||||
/// Validate packet counter against a replay protection validator
|
||||
///
|
||||
/// This performs a quick check to see if the packet counter is valid before
|
||||
/// any expensive processing is done.
|
||||
pub fn validate_counter(
|
||||
&self,
|
||||
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
|
||||
) -> Result<(), LpError> {
|
||||
let guard = validator.lock();
|
||||
guard.will_accept_branchless(self.header.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark packet as received in the replay protection validator
|
||||
///
|
||||
/// This should be called after a packet has been successfully processed.
|
||||
pub fn mark_received(
|
||||
&self,
|
||||
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
|
||||
) -> Result<(), LpError> {
|
||||
let mut guard = validator.lock();
|
||||
guard.mark_did_receive_branchless(self.header.counter)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// VERSION [1B] || RESERVED [3B] || SENDER_INDEX [4B] || COUNTER [8B]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpHeader {
|
||||
pub protocol_version: u8,
|
||||
|
||||
pub session_id: u32,
|
||||
pub counter: u64,
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub const SIZE: usize = 16;
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub fn new(session_id: u32, counter: u64) -> Self {
|
||||
Self {
|
||||
protocol_version: 1,
|
||||
session_id,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, dst: &mut BytesMut) {
|
||||
// protocol version
|
||||
dst.put_u8(self.protocol_version);
|
||||
|
||||
// reserved
|
||||
dst.put_slice(&[0, 0, 0]);
|
||||
|
||||
// sender index
|
||||
dst.put_slice(&self.session_id.to_le_bytes());
|
||||
|
||||
// counter
|
||||
dst.put_slice(&self.counter.to_le_bytes());
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
|
||||
let protocol_version = src[0];
|
||||
// Skip reserved bytes [1..4]
|
||||
|
||||
let mut session_id_bytes = [0u8; 4];
|
||||
session_id_bytes.copy_from_slice(&src[4..8]);
|
||||
let session_id = u32::from_le_bytes(session_id_bytes);
|
||||
|
||||
let mut counter_bytes = [0u8; 8];
|
||||
counter_bytes.copy_from_slice(&src[8..16]);
|
||||
let counter = u64::from_le_bytes(counter_bytes);
|
||||
|
||||
Ok(LpHeader {
|
||||
protocol_version,
|
||||
session_id,
|
||||
counter,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the counter value from the header
|
||||
pub fn counter(&self) -> u64 {
|
||||
self.counter
|
||||
}
|
||||
|
||||
/// Get the sender index from the header
|
||||
pub fn session_id(&self) -> u32 {
|
||||
self.session_id
|
||||
}
|
||||
}
|
||||
|
||||
// subsequent data: MessageType || Data
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! PSK (Pre-Shared Key) derivation for LP sessions using Blake3 KDF.
|
||||
//!
|
||||
//! This module implements identity-bound PSK derivation where both client and gateway
|
||||
//! derive the same PSK from their LP keypairs using ECDH + Blake3 KDF.
|
||||
|
||||
use crate::keypair::{PrivateKey, PublicKey};
|
||||
|
||||
/// Context string for Blake3 KDF domain separation.
|
||||
const PSK_CONTEXT: &str = "nym-lp-psk-v1";
|
||||
|
||||
/// Derives a PSK using Blake3 KDF from local private key, remote public key, and salt.
|
||||
///
|
||||
/// # Formula
|
||||
/// ```text
|
||||
/// shared_secret = ECDH(local_private, remote_public)
|
||||
/// psk = Blake3_derive_key(context="nym-lp-psk-v1", input=shared_secret || salt)
|
||||
/// ```
|
||||
///
|
||||
/// # Properties
|
||||
/// - **Identity-bound**: PSK is tied to the LP keypairs of both parties
|
||||
/// - **Session-specific**: Different salts produce different PSKs
|
||||
/// - **Symmetric**: Both sides derive the same PSK from their respective keys
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_private` - This side's LP private key
|
||||
/// * `remote_public` - Peer's LP public key
|
||||
/// * `salt` - 32-byte salt (timestamp + nonce from ClientHello)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte PSK suitable for Noise protocol
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Client side
|
||||
/// let client_private = client_keypair.private_key();
|
||||
/// let gateway_public = gateway_keypair.public_key();
|
||||
/// let salt = ClientHelloData::new_with_fresh_salt(...).salt;
|
||||
/// let psk = derive_psk(&client_private, &gateway_public, &salt);
|
||||
///
|
||||
/// // Gateway side (derives same PSK)
|
||||
/// let gateway_private = gateway_keypair.private_key();
|
||||
/// let client_public = /* from ClientHello */;
|
||||
/// let psk = derive_psk(&gateway_private, &client_public, &salt);
|
||||
/// ```
|
||||
pub fn derive_psk(
|
||||
local_private: &PrivateKey,
|
||||
remote_public: &PublicKey,
|
||||
salt: &[u8; 32],
|
||||
) -> [u8; 32] {
|
||||
// Perform ECDH to get shared secret
|
||||
let shared_secret = local_private.diffie_hellman(remote_public);
|
||||
|
||||
// Derive PSK using Blake3 KDF with domain separation
|
||||
nym_crypto::kdf::derive_key_blake3(PSK_CONTEXT, shared_secret.as_bytes(), salt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::keypair::Keypair;
|
||||
|
||||
#[test]
|
||||
fn test_psk_derivation_is_deterministic() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// Derive PSK twice with same inputs
|
||||
let psk1 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
let psk2 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
|
||||
assert_eq!(psk1, psk2, "Same inputs should produce same PSK");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psk_derivation_is_symmetric() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [2u8; 32];
|
||||
|
||||
// Client derives PSK
|
||||
let client_psk = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
|
||||
// Gateway derives PSK from their perspective
|
||||
let gateway_psk = derive_psk(keypair_2.private_key(), keypair_1.public_key(), &salt);
|
||||
|
||||
assert_eq!(
|
||||
client_psk, gateway_psk,
|
||||
"Both sides should derive identical PSK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_psks() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
|
||||
let salt1 = [1u8; 32];
|
||||
let salt2 = [2u8; 32];
|
||||
|
||||
let psk1 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt1);
|
||||
let psk2 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt2);
|
||||
|
||||
assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_keys_produce_different_psks() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let keypair_3 = Keypair::default();
|
||||
let salt = [3u8; 32];
|
||||
|
||||
let psk1 = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
let psk2 = derive_psk(keypair_1.private_key(), keypair_3.public_key(), &salt);
|
||||
|
||||
assert_ne!(
|
||||
psk1, psk2,
|
||||
"Different remote keys should produce different PSKs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psk_output_length() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [4u8; 32];
|
||||
|
||||
let psk = derive_psk(keypair_1.private_key(), keypair_2.public_key(), &salt);
|
||||
|
||||
assert_eq!(psk.len(), 32, "PSK should be exactly 32 bytes");
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Error types for replay protection.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur during replay protection validation.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ReplayError {
|
||||
/// The counter value is invalid (e.g., too far in the future)
|
||||
#[error("Invalid counter value")]
|
||||
InvalidCounter,
|
||||
|
||||
/// The packet has already been received (replay attack)
|
||||
#[error("Duplicate counter value")]
|
||||
DuplicateCounter,
|
||||
|
||||
/// The packet is outside the replay window
|
||||
#[error("Packet outside replay window")]
|
||||
OutOfWindow,
|
||||
}
|
||||
|
||||
/// Result type for replay protection operations
|
||||
pub type ReplayResult<T> = Result<T, ReplayError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::LpError;
|
||||
|
||||
#[test]
|
||||
fn test_replay_error_variants() {
|
||||
let invalid = ReplayError::InvalidCounter;
|
||||
let duplicate = ReplayError::DuplicateCounter;
|
||||
let out_of_window = ReplayError::OutOfWindow;
|
||||
|
||||
assert_eq!(invalid.to_string(), "Invalid counter value");
|
||||
assert_eq!(duplicate.to_string(), "Duplicate counter value");
|
||||
assert_eq!(out_of_window.to_string(), "Packet outside replay window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_error_conversion() {
|
||||
let replay_error = ReplayError::InvalidCounter;
|
||||
let lp_error: LpError = replay_error.into();
|
||||
|
||||
match lp_error {
|
||||
LpError::Replay(e) => {
|
||||
assert!(matches!(e, ReplayError::InvalidCounter));
|
||||
}
|
||||
_ => panic!("Expected Replay variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_result() {
|
||||
let ok_result: ReplayResult<()> = Ok(());
|
||||
let err_result: ReplayResult<()> = Err(ReplayError::InvalidCounter);
|
||||
|
||||
assert!(ok_result.is_ok());
|
||||
assert!(err_result.is_err());
|
||||
assert!(matches!(
|
||||
err_result.unwrap_err(),
|
||||
ReplayError::InvalidCounter
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Replay protection module for the Lewes Protocol.
|
||||
//!
|
||||
//! This module implements BoringTun-style replay protection to prevent
|
||||
//! replay attacks and ensure packet ordering. It uses a bitmap-based
|
||||
//! approach to track received packets and validate their sequence.
|
||||
|
||||
pub mod error;
|
||||
pub mod simd;
|
||||
pub mod validator;
|
||||
|
||||
pub use error::ReplayError;
|
||||
pub use validator::ReceivingKeyCounterValidator;
|
||||
@@ -1,278 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! ARM NEON implementation of bitmap operations.
|
||||
|
||||
use super::BitmapOps;
|
||||
|
||||
#[cfg(target_feature = "neon")]
|
||||
use std::arch::aarch64::{vceqq_u64, vdupq_n_u64, vgetq_lane_u64, vld1q_u64, vst1q_u64};
|
||||
|
||||
/// ARM NEON bitmap operations implementation
|
||||
pub struct ArmBitmapOps;
|
||||
|
||||
impl BitmapOps for ArmBitmapOps {
|
||||
#[inline(always)]
|
||||
fn clear_words(bitmap: &mut [u64], start_idx: usize, num_words: usize) {
|
||||
debug_assert!(start_idx + num_words <= bitmap.len());
|
||||
|
||||
#[cfg(target_feature = "neon")]
|
||||
unsafe {
|
||||
// Process 2 words at a time with NEON
|
||||
// Safety:
|
||||
// - vdupq_n_u64 is safe to call with any u64 value
|
||||
let zero_vec = vdupq_n_u64(0);
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 2 words
|
||||
while idx + 2 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads/writes of at least 2 u64 words (16 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 2 <= end_idx to ensure we have 2 complete words
|
||||
vst1q_u64(bitmap[idx..].as_mut_ptr(), zero_vec);
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle remaining words (0 or 1)
|
||||
while idx < end_idx {
|
||||
bitmap[idx] = 0;
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "neon"))]
|
||||
{
|
||||
// Fallback to scalar implementation
|
||||
for i in start_idx..(start_idx + num_words) {
|
||||
bitmap[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_range_zero(bitmap: &[u64], start_idx: usize, num_words: usize) -> bool {
|
||||
debug_assert!(start_idx + num_words <= bitmap.len());
|
||||
|
||||
#[cfg(target_feature = "neon")]
|
||||
unsafe {
|
||||
// Process 2 words at a time with NEON
|
||||
// Safety:
|
||||
// - vdupq_n_u64 is safe to call with any u64 value
|
||||
let zero_vec = vdupq_n_u64(0);
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 2 words
|
||||
while idx + 2 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads of at least 2 u64 words (16 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 2 <= end_idx to ensure we have 2 complete words
|
||||
let data_vec = vld1q_u64(bitmap[idx..].as_ptr());
|
||||
|
||||
// Safety:
|
||||
// - vceqq_u64 is safe when given valid vector values from vld1q_u64 and vdupq_n_u64
|
||||
// - vgetq_lane_u64 is safe with valid indices (0 and 1) for a 2-lane vector
|
||||
let cmp_result = vceqq_u64(data_vec, zero_vec);
|
||||
let mask1 = vgetq_lane_u64(cmp_result, 0);
|
||||
let mask2 = vgetq_lane_u64(cmp_result, 1);
|
||||
|
||||
if (mask1 & mask2) != u64::MAX {
|
||||
return false;
|
||||
}
|
||||
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle remaining words (0 or 1)
|
||||
while idx < end_idx {
|
||||
if bitmap[idx] != 0 {
|
||||
return false;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "neon"))]
|
||||
{
|
||||
// Fallback to scalar implementation
|
||||
bitmap[start_idx..(start_idx + num_words)]
|
||||
.iter()
|
||||
.all(|&w| w == 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn set_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = bit_idx % 64;
|
||||
bitmap[word_idx] |= 1u64 << bit_pos;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn clear_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = bit_idx % 64;
|
||||
bitmap[word_idx] &= !(1u64 << bit_pos);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn check_bit(bitmap: &[u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = bit_idx % 64;
|
||||
(bitmap[word_idx] & (1u64 << bit_pos)) != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// We also implement optimized versions for specific operations that could
|
||||
/// benefit from NEON but don't fit the general trait pattern
|
||||
///
|
||||
/// Atomic operations for the bitmap
|
||||
pub mod atomic {
|
||||
#[cfg(target_feature = "neon")]
|
||||
use std::arch::aarch64::{vdupq_n_u64, vld1q_u64, vorrq_u64, vst1q_u64};
|
||||
|
||||
/// Check and set bit, returning the previous state
|
||||
/// This function is not actually atomic! It's just a non-atomic optimization
|
||||
/// For actual atomic operations, the caller must provide proper synchronization
|
||||
#[inline(always)]
|
||||
pub fn check_and_set_bit(bitmap: &mut [u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = bit_idx % 64;
|
||||
let mask = 1u64 << bit_pos;
|
||||
|
||||
// Get old value
|
||||
let old_word = bitmap[word_idx];
|
||||
|
||||
// Set bit regardless of current state
|
||||
bitmap[word_idx] |= mask;
|
||||
|
||||
// Return true if bit was already set (duplicate)
|
||||
(old_word & mask) != 0
|
||||
}
|
||||
|
||||
/// Set a range of bits efficiently using NEON
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because it:
|
||||
/// - Uses SIMD intrinsics that require the NEON CPU feature to be available
|
||||
/// - Accesses bitmap memory through raw pointers
|
||||
/// - Does not perform bounds checking beyond what's required for SIMD operations
|
||||
///
|
||||
/// Caller must ensure:
|
||||
/// - The NEON feature is available on the current CPU
|
||||
/// - `bitmap` has sufficient size to hold indices up to `end_bit/64`
|
||||
/// - `start_bit` and `end_bit` are valid bit indices within the bitmap
|
||||
/// - No other thread is concurrently modifying the same memory
|
||||
#[inline(always)]
|
||||
#[cfg(target_feature = "neon")]
|
||||
pub unsafe fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
if start_word == end_word {
|
||||
// Special case: all bits in the same word
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle using NEON
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
if first_full_word <= last_full_word {
|
||||
// Use NEON to set words faster
|
||||
// Safety: vdupq_n_u64 is safe to call with any u64 value
|
||||
let ones_vec = vdupq_n_u64(u64::MAX);
|
||||
let mut idx = first_full_word;
|
||||
|
||||
while idx + 2 <= last_full_word + 1 {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads/writes of at least 2 u64 words (16 bytes)
|
||||
// - We check that idx + 2 <= last_full_word + 1 to ensure we have 2 complete words
|
||||
let current_vec = vld1q_u64(bitmap[idx..].as_ptr());
|
||||
// Safety: vorrq_u64 is safe when given valid vector values
|
||||
let result_vec = vorrq_u64(current_vec, ones_vec);
|
||||
vst1q_u64(bitmap[idx..].as_mut_ptr(), result_vec);
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle remaining words
|
||||
while idx <= last_full_word {
|
||||
bitmap[idx] = u64::MAX;
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a range of bits efficiently (scalar fallback)
|
||||
#[inline(always)]
|
||||
#[cfg(not(target_feature = "neon"))]
|
||||
pub fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
if start_word == end_word {
|
||||
// Special case: all bits in the same word
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
for word_idx in first_full_word..=last_full_word {
|
||||
bitmap[word_idx] = u64::MAX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! SIMD optimizations for the replay protection bitmap operations.
|
||||
//!
|
||||
//! This module provides architecture-specific SIMD implementations with a common interface.
|
||||
|
||||
// Re-export the appropriate implementation
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
mod x86;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub use self::x86::*;
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod arm;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
pub use self::arm::*;
|
||||
|
||||
// Fallback scalar implementation for all other architectures
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
mod scalar;
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
pub use self::scalar::*;
|
||||
|
||||
/// Trait defining SIMD operations for bitmap manipulation
|
||||
pub trait BitmapOps {
|
||||
/// Clear a range of words in the bitmap
|
||||
fn clear_words(bitmap: &mut [u64], start_idx: usize, num_words: usize);
|
||||
|
||||
/// Check if a range of words in the bitmap is all zeros
|
||||
fn is_range_zero(bitmap: &[u64], start_idx: usize, num_words: usize) -> bool;
|
||||
|
||||
/// Set a specific bit in the bitmap
|
||||
fn set_bit(bitmap: &mut [u64], bit_idx: u64);
|
||||
|
||||
/// Clear a specific bit in the bitmap
|
||||
fn clear_bit(bitmap: &mut [u64], bit_idx: u64);
|
||||
|
||||
/// Check if a specific bit is set in the bitmap
|
||||
fn check_bit(bitmap: &[u64], bit_idx: u64) -> bool;
|
||||
}
|
||||
|
||||
/// Get the optimal number of words to process in a SIMD operation
|
||||
/// for the current architecture
|
||||
#[inline(always)]
|
||||
pub fn optimal_simd_width() -> usize {
|
||||
// This value is specialized for each architecture in their respective modules
|
||||
OPTIMAL_SIMD_WIDTH
|
||||
}
|
||||
|
||||
/// Constant indicating the optimal SIMD processing width in number of u64 words
|
||||
/// for the current architecture
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[cfg(target_feature = "avx2")]
|
||||
pub const OPTIMAL_SIMD_WIDTH: usize = 4; // 256 bits = 4 u64 words
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[cfg(all(not(target_feature = "avx2"), target_feature = "sse2"))]
|
||||
pub const OPTIMAL_SIMD_WIDTH: usize = 2; // 128 bits = 2 u64 words
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(target_feature = "neon")]
|
||||
pub const OPTIMAL_SIMD_WIDTH: usize = 2; // 128 bits = 2 u64 words
|
||||
|
||||
// Fallback for non-SIMD platforms or when features aren't available
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "x86_64", target_feature = "avx2"),
|
||||
all(target_arch = "x86_64", target_feature = "sse2"),
|
||||
all(target_arch = "aarch64", target_feature = "neon")
|
||||
)))]
|
||||
pub const OPTIMAL_SIMD_WIDTH: usize = 1; // Scalar fallback
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Scalar (non-SIMD) implementation of bitmap operations.
|
||||
//! Used as a fallback when SIMD instructions are unavailable.
|
||||
|
||||
use super::BitmapOps;
|
||||
|
||||
/// Scalar (non-SIMD) bitmap operations implementation
|
||||
pub struct ScalarBitmapOps;
|
||||
|
||||
impl BitmapOps for ScalarBitmapOps {
|
||||
#[inline(always)]
|
||||
fn clear_words(bitmap: &mut [u64], start_idx: usize, num_words: usize) {
|
||||
for i in start_idx..(start_idx + num_words) {
|
||||
bitmap[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_range_zero(bitmap: &[u64], start_idx: usize, num_words: usize) -> bool {
|
||||
for i in start_idx..(start_idx + num_words) {
|
||||
if bitmap[i] != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn set_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
bitmap[word_idx] |= 1u64 << bit_pos;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn clear_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
bitmap[word_idx] &= !(1u64 << bit_pos);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn check_bit(bitmap: &[u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
(bitmap[word_idx] & (1u64 << bit_pos)) != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Scalar implementations of other bitmap utilities
|
||||
pub mod atomic {
|
||||
/// Check and set bit, returning the previous state
|
||||
/// This function is not actually atomic! It's just a normal operation
|
||||
#[inline(always)]
|
||||
pub fn check_and_set_bit(bitmap: &mut [u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
let mask = 1u64 << bit_pos;
|
||||
|
||||
// Get old value
|
||||
let old_word = bitmap[word_idx];
|
||||
|
||||
// Set bit regardless of current state
|
||||
bitmap[word_idx] |= mask;
|
||||
|
||||
// Return true if bit was already set (duplicate)
|
||||
(old_word & mask) != 0
|
||||
}
|
||||
|
||||
/// Set a range of bits efficiently
|
||||
#[inline(always)]
|
||||
pub fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
if start_word == end_word {
|
||||
// Special case: all bits in the same word
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
for word_idx in first_full_word..=last_full_word {
|
||||
bitmap[word_idx] = u64::MAX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! x86/x86_64 SIMD implementation of bitmap operations.
|
||||
//! Provides optimized implementations using SSE2 and AVX2 intrinsics.
|
||||
|
||||
use super::BitmapOps;
|
||||
|
||||
// Track execution counts for debugging
|
||||
static mut AVX2_CLEAR_COUNT: usize = 0;
|
||||
static mut SSE2_CLEAR_COUNT: usize = 0;
|
||||
static mut SCALAR_CLEAR_COUNT: usize = 0;
|
||||
|
||||
// Import the appropriate SIMD intrinsics
|
||||
#[cfg(target_feature = "avx2")]
|
||||
use std::arch::x86_64::{
|
||||
__m256i, _mm256_cmpeq_epi64, _mm256_load_si256, _mm256_loadu_si256, _mm256_movemask_epi8,
|
||||
_mm256_or_si256, _mm256_set1_epi64x, _mm256_setzero_si256, _mm256_store_si256,
|
||||
_mm256_storeu_si256, _mm256_testz_si256,
|
||||
};
|
||||
|
||||
#[cfg(target_feature = "sse2")]
|
||||
use std::arch::x86_64::{
|
||||
__m128i, _mm_cmpeq_epi64, _mm_load_si128, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128,
|
||||
_mm_set1_epi64x, _mm_setzero_si128, _mm_store_si128, _mm_storeu_si128, _mm_testz_si128,
|
||||
};
|
||||
|
||||
/// x86/x86_64 SIMD bitmap operations implementation
|
||||
pub struct X86BitmapOps;
|
||||
|
||||
impl BitmapOps for X86BitmapOps {
|
||||
#[inline(always)]
|
||||
fn clear_words(bitmap: &mut [u64], start_idx: usize, num_words: usize) {
|
||||
debug_assert!(start_idx + num_words <= bitmap.len());
|
||||
|
||||
// First try AVX2 (256-bit, 4 words at a time)
|
||||
#[cfg(target_feature = "avx2")]
|
||||
unsafe {
|
||||
// Track execution count
|
||||
AVX2_CLEAR_COUNT += 1;
|
||||
|
||||
// Process 4 words at a time with AVX2
|
||||
let zero_vec = _mm256_setzero_si256();
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 4 words
|
||||
while idx + 4 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads/writes of at least 4 u64 words (32 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 4 <= end_idx to ensure we have 4 complete words
|
||||
// - The unaligned _storeu_ variant is used to handle any alignment
|
||||
_mm256_storeu_si256(bitmap[idx..].as_mut_ptr() as *mut __m256i, zero_vec);
|
||||
idx += 4;
|
||||
}
|
||||
|
||||
// Handle remaining words with SSE2 or scalar ops
|
||||
if idx < end_idx {
|
||||
if idx + 2 <= end_idx {
|
||||
// Use SSE2 for 2 words
|
||||
// Safety: Same as above, but for 2 words (16 bytes) instead of 4
|
||||
let sse_zero = _mm_setzero_si128();
|
||||
_mm_storeu_si128(bitmap[idx..].as_mut_ptr() as *mut __m128i, sse_zero);
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle any remaining words
|
||||
while idx < end_idx {
|
||||
bitmap[idx] = 0;
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If AVX2 is unavailable, try SSE2 (128-bit, 2 words at a time)
|
||||
#[cfg(all(target_feature = "sse2", not(target_feature = "avx2")))]
|
||||
unsafe {
|
||||
// Track execution count
|
||||
SSE2_CLEAR_COUNT += 1;
|
||||
|
||||
// Process 2 words at a time with SSE2
|
||||
let zero_vec = _mm_setzero_si128();
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 2 words
|
||||
while idx + 2 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads/writes of at least 2 u64 words (16 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 2 <= end_idx to ensure we have 2 complete words
|
||||
// - The unaligned _storeu_ variant is used to handle any alignment
|
||||
_mm_storeu_si128(bitmap[idx..].as_mut_ptr() as *mut __m128i, zero_vec);
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle remaining word (if any)
|
||||
if idx < end_idx {
|
||||
bitmap[idx] = 0;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to scalar implementation if no SIMD features available
|
||||
unsafe {
|
||||
// Safety: Just increments a static counter, with no possibility of data races
|
||||
// as long as this function isn't called concurrently
|
||||
SCALAR_CLEAR_COUNT += 1;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
for i in start_idx..(start_idx + num_words) {
|
||||
bitmap[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_range_zero(bitmap: &[u64], start_idx: usize, num_words: usize) -> bool {
|
||||
debug_assert!(start_idx + num_words <= bitmap.len());
|
||||
|
||||
// First try AVX2 (256-bit, 4 words at a time)
|
||||
#[cfg(target_feature = "avx2")]
|
||||
unsafe {
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 4 words
|
||||
while idx + 4 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads of at least 4 u64 words (32 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 4 <= end_idx to ensure we have 4 complete words
|
||||
// - The unaligned _loadu_ variant is used to handle any alignment
|
||||
let data_vec = _mm256_loadu_si256(bitmap[idx..].as_ptr() as *const __m256i);
|
||||
|
||||
// Check if any bits are non-zero
|
||||
// Safety: _mm256_testz_si256 is safe when given valid __m256i values,
|
||||
// which data_vec is guaranteed to be
|
||||
if !_mm256_testz_si256(data_vec, data_vec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
idx += 4;
|
||||
}
|
||||
|
||||
// Handle remaining words with SSE2 or scalar ops
|
||||
if idx < end_idx {
|
||||
if idx + 2 <= end_idx {
|
||||
// Use SSE2 for 2 words
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads of at least 2 u64 words (16 bytes)
|
||||
// - We check that idx + 2 <= end_idx to ensure we have 2 complete words
|
||||
let data_vec = _mm_loadu_si128(bitmap[idx..].as_ptr() as *const __m128i);
|
||||
|
||||
// Safety: _mm_testz_si128 is safe when given valid __m128i values
|
||||
if !_mm_testz_si128(data_vec, data_vec) {
|
||||
return false;
|
||||
}
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle any remaining words
|
||||
while idx < end_idx {
|
||||
if bitmap[idx] != 0 {
|
||||
return false;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If AVX2 is unavailable, try SSE2 (128-bit, 2 words at a time)
|
||||
#[cfg(all(target_feature = "sse2", not(target_feature = "avx2")))]
|
||||
unsafe {
|
||||
let mut idx = start_idx;
|
||||
let end_idx = start_idx + num_words;
|
||||
|
||||
// Process aligned blocks of 2 words
|
||||
while idx + 2 <= end_idx {
|
||||
// Safety:
|
||||
// - bitmap[idx..] is valid for reads of at least 2 u64 words (16 bytes)
|
||||
// - We've validated with the debug_assert that start_idx + num_words <= bitmap.len()
|
||||
// - We check that idx + 2 <= end_idx to ensure we have 2 complete words
|
||||
// - The unaligned _loadu_ variant is used to handle any alignment
|
||||
let data_vec = _mm_loadu_si128(bitmap[idx..].as_ptr() as *const __m128i);
|
||||
|
||||
// Check if any bits are non-zero (SSE4.1 would have _mm_testz_si128,
|
||||
// but for SSE2 compatibility we need to use a different approach)
|
||||
#[cfg(target_feature = "sse4.1")]
|
||||
{
|
||||
// Safety: _mm_testz_si128 is safe when given valid __m128i values
|
||||
if !_mm_testz_si128(data_vec, data_vec) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "sse4.1"))]
|
||||
{
|
||||
// Compare with zero vector using SSE2 only
|
||||
// Safety: All operations are valid with the data_vec value
|
||||
let zero_vec = _mm_setzero_si128();
|
||||
let cmp = _mm_cmpeq_epi64(data_vec, zero_vec);
|
||||
|
||||
// The movemask gives us a bit for each byte, set if the high bit of the byte is set
|
||||
// For all-zero comparison, all 16 bits should be set (0xFFFF)
|
||||
let mask = _mm_movemask_epi8(cmp);
|
||||
if mask != 0xFFFF {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Handle remaining word (if any)
|
||||
if idx < end_idx && bitmap[idx] != 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
bitmap[start_idx..(start_idx + num_words)]
|
||||
.iter()
|
||||
.all(|&word| word == 0)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn set_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
bitmap[word_idx] |= 1u64 << bit_pos;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn clear_bit(bitmap: &mut [u64], bit_idx: u64) {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
bitmap[word_idx] &= !(1u64 << bit_pos);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn check_bit(bitmap: &[u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
(bitmap[word_idx] & (1u64 << bit_pos)) != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional x86 optimized operations not covered by the trait
|
||||
pub mod atomic {
|
||||
use super::*;
|
||||
|
||||
/// Check and set bit, returning the previous state
|
||||
/// This function is not actually atomic! It's just a non-atomic optimization
|
||||
#[inline(always)]
|
||||
pub fn check_and_set_bit(bitmap: &mut [u64], bit_idx: u64) -> bool {
|
||||
let word_idx = (bit_idx / 64) as usize;
|
||||
let bit_pos = (bit_idx % 64) as u64;
|
||||
let mask = 1u64 << bit_pos;
|
||||
|
||||
// Get old value
|
||||
let old_word = bitmap[word_idx];
|
||||
|
||||
// Set bit regardless of current state
|
||||
bitmap[word_idx] |= mask;
|
||||
|
||||
// Return true if bit was already set (duplicate)
|
||||
(old_word & mask) != 0
|
||||
}
|
||||
|
||||
/// Set multiple bits at once using SIMD when possible
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because it:
|
||||
/// - Uses SIMD intrinsics that require the AVX2 CPU feature to be available
|
||||
/// - Accesses bitmap memory through raw pointers
|
||||
/// - Does not perform bounds checking beyond what's required for SIMD operations
|
||||
///
|
||||
/// Caller must ensure:
|
||||
/// - The AVX2 feature is available on the current CPU
|
||||
/// - `bitmap` has sufficient size to hold indices up to `end_bit/64`
|
||||
/// - `start_bit` and `end_bit` are valid bit indices within the bitmap
|
||||
/// - No other thread is concurrently modifying the same memory
|
||||
#[inline(always)]
|
||||
#[cfg(target_feature = "avx2")]
|
||||
pub unsafe fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
// Special case: all bits in the same word
|
||||
if start_word == end_word {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle using AVX2
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
if first_full_word <= last_full_word {
|
||||
// Use AVX2 to set multiple words at once
|
||||
// Safety: _mm256_set1_epi64x is safe to call with any i64 value
|
||||
let ones = _mm256_set1_epi64x(-1); // All bits set to 1
|
||||
|
||||
let mut i = first_full_word;
|
||||
while i + 4 <= last_full_word + 1 {
|
||||
// Safety:
|
||||
// - bitmap[i..] is valid for reads/writes of at least 4 u64 words (32 bytes)
|
||||
// - We check that i + 4 <= last_full_word + 1 to ensure we have 4 complete words
|
||||
// - The unaligned _loadu/_storeu variants are used to handle any alignment
|
||||
let current = _mm256_loadu_si256(bitmap[i..].as_ptr() as *const __m256i);
|
||||
let result = _mm256_or_si256(current, ones);
|
||||
_mm256_storeu_si256(bitmap[i..].as_mut_ptr() as *mut __m256i, result);
|
||||
i += 4;
|
||||
}
|
||||
|
||||
// Use SSE2 for remaining pairs of words
|
||||
if i + 2 <= last_full_word + 1 {
|
||||
// Safety:
|
||||
// - bitmap[i..] is valid for reads/writes of at least 2 u64 words (16 bytes)
|
||||
// - We check that i + 2 <= last_full_word + 1 to ensure we have 2 complete words
|
||||
// - The unaligned _loadu/_storeu variants are used to handle any alignment
|
||||
let sse_ones = _mm_set1_epi64x(-1);
|
||||
let current = _mm_loadu_si128(bitmap[i..].as_ptr() as *const __m128i);
|
||||
let result = _mm_or_si128(current, sse_ones);
|
||||
_mm_storeu_si128(bitmap[i..].as_mut_ptr() as *mut __m128i, result);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
// Handle any remaining words
|
||||
while i <= last_full_word {
|
||||
bitmap[i] = u64::MAX;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set multiple bits at once using SSE2 (when AVX2 not available)
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because it:
|
||||
/// - Uses SIMD intrinsics that require the SSE2 CPU feature to be available
|
||||
/// - Accesses bitmap memory through raw pointers
|
||||
/// - Does not perform bounds checking beyond what's required for SIMD operations
|
||||
///
|
||||
/// Caller must ensure:
|
||||
/// - The SSE2 feature is available on the current CPU
|
||||
/// - `bitmap` has sufficient size to hold indices up to `end_bit/64`
|
||||
/// - `start_bit` and `end_bit` are valid bit indices within the bitmap
|
||||
/// - No other thread is concurrently modifying the same memory
|
||||
#[inline(always)]
|
||||
#[cfg(all(target_feature = "sse2", not(target_feature = "avx2")))]
|
||||
pub unsafe fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
// Special case: all bits in the same word
|
||||
if start_word == end_word {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle using SSE2
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
if first_full_word <= last_full_word {
|
||||
// Use SSE2 to set multiple words at once
|
||||
// Safety: _mm_set1_epi64x is safe to call with any i64 value
|
||||
let ones = _mm_set1_epi64x(-1); // All bits set to 1
|
||||
|
||||
let mut i = first_full_word;
|
||||
while i + 2 <= last_full_word + 1 {
|
||||
// Safety:
|
||||
// - bitmap[i..] is valid for reads/writes of at least 2 u64 words (16 bytes)
|
||||
// - We check that i + 2 <= last_full_word + 1 to ensure we have 2 complete words
|
||||
// - The unaligned _loadu/_storeu variants are used to handle any alignment
|
||||
let current = _mm_loadu_si128(bitmap[i..].as_ptr() as *const __m128i);
|
||||
let result = _mm_or_si128(current, ones);
|
||||
_mm_storeu_si128(bitmap[i..].as_mut_ptr() as *mut __m128i, result);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
// Handle any remaining words
|
||||
while i <= last_full_word {
|
||||
bitmap[i] = u64::MAX;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set multiple bits at once using scalar operations (fallback)
|
||||
#[inline(always)]
|
||||
#[cfg(not(any(target_feature = "avx2", target_feature = "sse2")))]
|
||||
pub fn set_bits_range(bitmap: &mut [u64], start_bit: u64, end_bit: u64) {
|
||||
// Process whole words where possible
|
||||
let start_word = (start_bit / 64) as usize;
|
||||
let end_word = (end_bit / 64) as usize;
|
||||
|
||||
// Special case: all bits in the same word
|
||||
if start_word == end_word {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[start_word] |= start_mask & end_mask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial words at the beginning and end
|
||||
if start_bit % 64 != 0 {
|
||||
let start_mask = u64::MAX << (start_bit % 64);
|
||||
bitmap[start_word] |= start_mask;
|
||||
}
|
||||
|
||||
if (end_bit + 1) % 64 != 0 {
|
||||
let end_mask = u64::MAX >> (63 - (end_bit % 64));
|
||||
bitmap[end_word] |= end_mask;
|
||||
}
|
||||
|
||||
// Handle complete words in the middle
|
||||
let first_full_word = if start_bit % 64 == 0 {
|
||||
start_word
|
||||
} else {
|
||||
start_word + 1
|
||||
};
|
||||
let last_full_word = if (end_bit + 1) % 64 == 0 {
|
||||
end_word
|
||||
} else {
|
||||
end_word - 1
|
||||
};
|
||||
|
||||
for i in first_full_word..=last_full_word {
|
||||
bitmap[i] = u64::MAX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,876 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Replay protection validator implementation.
|
||||
//!
|
||||
//! This module implements the core replay protection logic using a bitmap-based
|
||||
//! approach to track received packets and validate their sequence.
|
||||
|
||||
use crate::replay::error::{ReplayError, ReplayResult};
|
||||
use crate::replay::simd::{self, BitmapOps};
|
||||
|
||||
// Determine the appropriate SIMD implementation at compile time
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
#[cfg(target_feature = "neon")]
|
||||
use crate::replay::simd::ArmBitmapOps as SimdImpl;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[cfg(target_feature = "avx2")]
|
||||
use crate::replay::simd::X86BitmapOps as SimdImpl;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[cfg(all(not(target_feature = "avx2"), target_feature = "sse2"))]
|
||||
use crate::replay::simd::X86BitmapOps as SimdImpl;
|
||||
|
||||
#[cfg(not(any(
|
||||
all(target_arch = "x86_64", target_feature = "avx2"),
|
||||
all(target_arch = "x86_64", target_feature = "sse2"),
|
||||
all(target_arch = "aarch64", target_feature = "neon")
|
||||
)))]
|
||||
use crate::replay::simd::ScalarBitmapOps as SimdImpl;
|
||||
|
||||
/// Size of a word in the bitmap (64 bits)
|
||||
const WORD_SIZE: usize = 64;
|
||||
|
||||
/// Number of words in the bitmap (allows reordering of 64*16 = 1024 packets)
|
||||
const N_WORDS: usize = 16;
|
||||
|
||||
/// Total number of bits in the bitmap
|
||||
const N_BITS: usize = WORD_SIZE * N_WORDS;
|
||||
|
||||
/// Validator for receiving key counters to prevent replay attacks.
|
||||
///
|
||||
/// This structure maintains a bitmap of received packets and validates
|
||||
/// incoming packet counters to ensure they are not replayed.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ReceivingKeyCounterValidator {
|
||||
/// Next expected counter value
|
||||
next: u64,
|
||||
|
||||
/// Total number of received packets
|
||||
receive_cnt: u64,
|
||||
|
||||
/// Bitmap for tracking received packets
|
||||
bitmap: [u64; N_WORDS],
|
||||
}
|
||||
|
||||
impl ReceivingKeyCounterValidator {
|
||||
/// Creates a new validator with the given initial counter value.
|
||||
pub fn new(initial_counter: u64) -> Self {
|
||||
Self {
|
||||
next: initial_counter,
|
||||
receive_cnt: 0,
|
||||
bitmap: [0; N_WORDS],
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a bit in the bitmap to mark a counter as received.
|
||||
#[inline(always)]
|
||||
fn set_bit(&mut self, idx: u64) {
|
||||
SimdImpl::set_bit(&mut self.bitmap, idx % (N_BITS as u64));
|
||||
}
|
||||
|
||||
/// Clears a bit in the bitmap.
|
||||
#[inline(always)]
|
||||
fn clear_bit(&mut self, idx: u64) {
|
||||
SimdImpl::clear_bit(&mut self.bitmap, idx % (N_BITS as u64));
|
||||
}
|
||||
|
||||
/// Clears the word that contains the given index.
|
||||
#[inline(always)]
|
||||
#[allow(dead_code)]
|
||||
fn clear_word(&mut self, idx: u64) {
|
||||
let bit_idx = idx % (N_BITS as u64);
|
||||
let word = (bit_idx / (WORD_SIZE as u64)) as usize;
|
||||
SimdImpl::clear_words(&mut self.bitmap, word, 1);
|
||||
}
|
||||
|
||||
/// Returns true if the bit is set, false otherwise.
|
||||
#[inline(always)]
|
||||
fn check_bit_branchless(&self, idx: u64) -> bool {
|
||||
SimdImpl::check_bit(&self.bitmap, idx % (N_BITS as u64))
|
||||
}
|
||||
|
||||
/// Performs a quick check to determine if a counter will be accepted.
|
||||
///
|
||||
/// This is a fast check that can be done before more expensive operations.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(())` if the counter is acceptable
|
||||
/// - `Err(ReplayError::InvalidCounter)` if the counter is invalid (too far back)
|
||||
/// - `Err(ReplayError::DuplicateCounter)` if the counter has already been received
|
||||
#[inline(always)]
|
||||
pub fn will_accept_branchless(&self, counter: u64) -> ReplayResult<()> {
|
||||
// Calculate conditions
|
||||
let is_growing = counter >= self.next;
|
||||
|
||||
// Handle potential overflow when adding N_BITS to counter
|
||||
let too_far_back = if counter > u64::MAX - (N_BITS as u64) {
|
||||
// If adding N_BITS would overflow, it can't be too far back
|
||||
false
|
||||
} else {
|
||||
counter + (N_BITS as u64) < self.next
|
||||
};
|
||||
|
||||
let duplicate = self.check_bit_branchless(counter);
|
||||
|
||||
// Using Option to avoid early returns
|
||||
let result = if is_growing {
|
||||
Some(Ok(()))
|
||||
} else if too_far_back {
|
||||
Some(Err(ReplayError::OutOfWindow))
|
||||
} else if duplicate {
|
||||
Some(Err(ReplayError::DuplicateCounter))
|
||||
} else {
|
||||
Some(Ok(()))
|
||||
};
|
||||
|
||||
// Unwrap the option (always Some)
|
||||
result.unwrap()
|
||||
}
|
||||
|
||||
/// Special case function for clearing the entire bitmap
|
||||
/// Used for the fast path when we know the bitmap must be entirely cleared
|
||||
#[inline(always)]
|
||||
fn clear_window_fast(&mut self) {
|
||||
SimdImpl::clear_words(&mut self.bitmap, 0, N_WORDS);
|
||||
}
|
||||
|
||||
/// Checks if the bitmap is completely empty (all zeros)
|
||||
/// This is used for fast path optimization
|
||||
#[inline(always)]
|
||||
fn is_bitmap_empty(&self) -> bool {
|
||||
SimdImpl::is_range_zero(&self.bitmap, 0, N_WORDS)
|
||||
}
|
||||
|
||||
/// Marks a counter as received and updates internal state.
|
||||
///
|
||||
/// This method should be called after a packet has been validated
|
||||
/// and processed successfully.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(())` if the counter was successfully marked
|
||||
/// - `Err(ReplayError::InvalidCounter)` if the counter is invalid (too far back)
|
||||
/// - `Err(ReplayError::DuplicateCounter)` if the counter has already been received
|
||||
#[inline(always)]
|
||||
pub fn mark_did_receive_branchless(&mut self, counter: u64) -> ReplayResult<()> {
|
||||
// Calculate conditions once - using saturating operations to prevent overflow
|
||||
// For the too_far_back check, we need to avoid overflowing when adding N_BITS to counter
|
||||
let too_far_back = if counter > u64::MAX - (N_BITS as u64) {
|
||||
// If adding N_BITS would overflow, it can't be too far back
|
||||
false
|
||||
} else {
|
||||
counter + (N_BITS as u64) < self.next
|
||||
};
|
||||
|
||||
let is_sequential = counter == self.next;
|
||||
let is_out_of_order = counter < self.next;
|
||||
|
||||
// Early return for out-of-window condition
|
||||
if too_far_back {
|
||||
return Err(ReplayError::OutOfWindow);
|
||||
}
|
||||
|
||||
// Check for duplicate (only matters for out-of-order packets)
|
||||
let duplicate = is_out_of_order && self.check_bit_branchless(counter);
|
||||
if duplicate {
|
||||
return Err(ReplayError::DuplicateCounter);
|
||||
}
|
||||
|
||||
// Fast path for far ahead counters with empty bitmap
|
||||
let far_ahead = counter.saturating_sub(self.next) >= (N_BITS as u64);
|
||||
if far_ahead && self.is_bitmap_empty() {
|
||||
// No need to clear anything, just set the new bit
|
||||
self.set_bit(counter);
|
||||
self.next = counter.saturating_add(1);
|
||||
self.receive_cnt += 1;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle bitmap clearing for ahead counters that aren't sequential
|
||||
if !is_sequential && !is_out_of_order {
|
||||
self.clear_window(counter);
|
||||
}
|
||||
|
||||
// Set the bit and update counters
|
||||
self.set_bit(counter);
|
||||
|
||||
// Update next counter safely - avoid overflow
|
||||
self.next = if is_sequential {
|
||||
counter.saturating_add(1)
|
||||
} else {
|
||||
self.next.max(counter.saturating_add(1))
|
||||
};
|
||||
|
||||
self.receive_cnt += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the current packet count statistics.
|
||||
///
|
||||
/// Returns a tuple of `(next, receive_cnt)` where:
|
||||
/// - `next` is the next expected counter value
|
||||
/// - `receive_cnt` is the total number of received packets
|
||||
pub fn current_packet_cnt(&self) -> (u64, u64) {
|
||||
(self.next, self.receive_cnt)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(dead_code)]
|
||||
fn check_and_set_bit_branchless(&mut self, idx: u64) -> bool {
|
||||
let bit_idx = idx % (N_BITS as u64);
|
||||
simd::atomic::check_and_set_bit(&mut self.bitmap, bit_idx)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(dead_code)]
|
||||
fn increment_counter_branchless(&mut self, condition: bool) {
|
||||
// Add either 1 or 0 based on condition
|
||||
self.receive_cnt += condition as u64;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn mark_sequential_branchless(&mut self, counter: u64) -> ReplayResult<()> {
|
||||
// Check if sequential
|
||||
let is_sequential = counter == self.next;
|
||||
|
||||
// Set the bit
|
||||
self.set_bit(counter);
|
||||
|
||||
// Conditionally update next counter using saturating add to prevent overflow
|
||||
self.next = self.next.saturating_add(is_sequential as u64);
|
||||
|
||||
// Always increment receive count if we got here
|
||||
self.receive_cnt += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function for window clearing with SIMD optimization
|
||||
#[inline(always)]
|
||||
fn clear_window(&mut self, counter: u64) {
|
||||
// Handle potential overflow safely
|
||||
// If counter is very large (close to u64::MAX), we need special handling
|
||||
let counter_distance = counter.saturating_sub(self.next);
|
||||
let far_ahead = counter_distance >= (N_BITS as u64);
|
||||
|
||||
// Fast path: Complete window clearing for far ahead counters
|
||||
if far_ahead {
|
||||
// Check if window is already clear for fast path optimization
|
||||
if !self.is_bitmap_empty() {
|
||||
// Use SIMD to clear the entire bitmap at once
|
||||
self.clear_window_fast();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare for partial window clearing
|
||||
let mut i = self.next;
|
||||
|
||||
// Get SIMD processing width (platform optimized)
|
||||
let simd_width = simd::optimal_simd_width();
|
||||
|
||||
// Pre-alignment clearing
|
||||
if i % (WORD_SIZE as u64) != 0 {
|
||||
let current_word = (i % (N_BITS as u64) / (WORD_SIZE as u64)) as usize;
|
||||
|
||||
// Check if we need to clear this word
|
||||
if self.bitmap[current_word] != 0 {
|
||||
// Safely handle potential overflow by checking before each increment
|
||||
while i % (WORD_SIZE as u64) != 0 && i < counter {
|
||||
self.clear_bit(i);
|
||||
|
||||
// Prevent overflow on increment
|
||||
if i == u64::MAX {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
// Fast forward to the next word boundary
|
||||
let words_to_skip = (WORD_SIZE as u64) - (i % (WORD_SIZE as u64));
|
||||
if words_to_skip > u64::MAX - i {
|
||||
// Would overflow, just set to MAX
|
||||
i = u64::MAX;
|
||||
} else {
|
||||
i += words_to_skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Word-aligned clearing with SIMD where possible
|
||||
while i <= counter.saturating_sub(WORD_SIZE as u64) {
|
||||
let current_word = (i % (N_BITS as u64) / (WORD_SIZE as u64)) as usize;
|
||||
|
||||
// Check if we have enough consecutive words to use SIMD
|
||||
if current_word + simd_width <= N_WORDS
|
||||
&& i % (simd_width as u64 * WORD_SIZE as u64) == 0
|
||||
{
|
||||
// Use SIMD to clear multiple words at once if any need clearing
|
||||
let needs_clearing =
|
||||
!SimdImpl::is_range_zero(&self.bitmap, current_word, simd_width);
|
||||
if needs_clearing {
|
||||
SimdImpl::clear_words(&mut self.bitmap, current_word, simd_width);
|
||||
}
|
||||
|
||||
// Skip the words we just processed
|
||||
let words_to_skip = simd_width as u64 * WORD_SIZE as u64;
|
||||
if words_to_skip > u64::MAX - i {
|
||||
i = u64::MAX;
|
||||
break;
|
||||
}
|
||||
i += words_to_skip;
|
||||
} else {
|
||||
// Process single word
|
||||
if self.bitmap[current_word] != 0 {
|
||||
self.bitmap[current_word] = 0;
|
||||
}
|
||||
|
||||
// Check for potential overflow before incrementing
|
||||
if i > u64::MAX - (WORD_SIZE as u64) {
|
||||
i = u64::MAX;
|
||||
break;
|
||||
}
|
||||
i += WORD_SIZE as u64;
|
||||
}
|
||||
}
|
||||
|
||||
// Post-alignment clearing (bit by bit for remaining bits)
|
||||
if i < counter {
|
||||
let final_word = (i % (N_BITS as u64) / (WORD_SIZE as u64)) as usize;
|
||||
let is_final_word_empty = self.bitmap[final_word] == 0;
|
||||
|
||||
// Skip clearing if word is already empty
|
||||
if !is_final_word_empty {
|
||||
while i < counter {
|
||||
self.clear_bit(i);
|
||||
|
||||
// Prevent overflow on increment
|
||||
if i == u64::MAX {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_replay_counter_basic() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Check initial state
|
||||
assert_eq!(validator.next, 0);
|
||||
assert_eq!(validator.receive_cnt, 0);
|
||||
|
||||
// Test sequential counters
|
||||
assert!(validator.mark_did_receive_branchless(0).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(0).is_err());
|
||||
assert!(validator.mark_did_receive_branchless(1).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_counter_out_of_order() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Process some sequential packets
|
||||
assert!(validator.mark_did_receive_branchless(0).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(2).is_ok());
|
||||
|
||||
// Out-of-order packet that hasn't been seen yet
|
||||
assert!(validator.mark_did_receive_branchless(1).is_err()); // Already seen
|
||||
assert!(validator.mark_did_receive_branchless(10).is_ok()); // New packet, ahead of next
|
||||
|
||||
// Next should now be 11
|
||||
assert_eq!(validator.next, 11);
|
||||
|
||||
// Can still accept packets in the valid window
|
||||
assert!(validator.will_accept_branchless(9).is_ok());
|
||||
assert!(validator.will_accept_branchless(8).is_ok());
|
||||
|
||||
// But duplicates are rejected
|
||||
assert!(validator.will_accept_branchless(10).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_counter_full() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Process a bunch of sequential packets
|
||||
for i in 0..64 {
|
||||
assert!(validator.mark_did_receive_branchless(i).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(i).is_err());
|
||||
}
|
||||
|
||||
// Test out of order within window
|
||||
assert!(validator.mark_did_receive_branchless(15).is_err()); // Already seen
|
||||
assert!(validator.mark_did_receive_branchless(63).is_err()); // Already seen
|
||||
|
||||
// Test for packets within bitmap range
|
||||
for i in 64..(N_BITS as u64) + 128 {
|
||||
assert!(validator.mark_did_receive_branchless(i).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(i).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_counter_window_sliding() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Jump far ahead to force window sliding
|
||||
let far_ahead = (N_BITS as u64) * 3;
|
||||
assert!(validator.mark_did_receive_branchless(far_ahead).is_ok());
|
||||
|
||||
// Everything too far back should be rejected
|
||||
for i in 0..=(N_BITS as u64) * 2 {
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(i),
|
||||
Err(ReplayError::OutOfWindow)
|
||||
));
|
||||
assert!(validator.mark_did_receive_branchless(i).is_err());
|
||||
}
|
||||
|
||||
// Values in window but less than far_ahead should be accepted
|
||||
for i in (N_BITS as u64) * 2 + 1..far_ahead {
|
||||
assert!(validator.will_accept_branchless(i).is_ok());
|
||||
}
|
||||
|
||||
// The far_ahead value itself should be rejected now (duplicate)
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(far_ahead),
|
||||
Err(ReplayError::DuplicateCounter)
|
||||
));
|
||||
|
||||
// Test receiving packets in reverse order within window
|
||||
for i in ((N_BITS as u64) * 2 + 1..far_ahead).rev() {
|
||||
assert!(validator.mark_did_receive_branchless(i).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(i).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_out_of_order_tracking() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Jump ahead
|
||||
assert!(validator.mark_did_receive_branchless(1000).is_ok());
|
||||
|
||||
// Test some more additions
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 70).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 71).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 72).is_ok());
|
||||
assert!(validator
|
||||
.mark_did_receive_branchless(1000 + 72 + 125)
|
||||
.is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 63).is_ok());
|
||||
|
||||
// Check duplicates
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 70).is_err());
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 71).is_err());
|
||||
assert!(validator.mark_did_receive_branchless(1000 + 72).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_counter_stats() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Initial state
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
assert_eq!(next, 0);
|
||||
assert_eq!(count, 0);
|
||||
|
||||
// After receiving some packets
|
||||
assert!(validator.mark_did_receive_branchless(0).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(1).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(2).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
assert_eq!(next, 3);
|
||||
assert_eq!(count, 3);
|
||||
|
||||
// After an out of order packet
|
||||
assert!(validator.mark_did_receive_branchless(10).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
assert_eq!(next, 11);
|
||||
assert_eq!(count, 4);
|
||||
|
||||
// After a packet from the past (within window)
|
||||
assert!(validator.mark_did_receive_branchless(5).is_ok());
|
||||
|
||||
let (next, count) = validator.current_packet_cnt();
|
||||
assert_eq!(next, 11); // Next doesn't change
|
||||
assert_eq!(count, 5); // Count increases
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_boundary_edge_cases() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// First process a sequence of packets
|
||||
for i in 0..100 {
|
||||
assert!(validator.mark_did_receive_branchless(i).is_ok());
|
||||
}
|
||||
|
||||
// The window should now span from 100 to 100+N_BITS
|
||||
|
||||
// Test packet near the upper edge of the window
|
||||
let upper_edge = 100 + (N_BITS as u64) - 1;
|
||||
assert!(validator.will_accept_branchless(upper_edge).is_ok());
|
||||
assert!(validator.mark_did_receive_branchless(upper_edge).is_ok());
|
||||
|
||||
// Test packet just outside the upper edge (should be accepted)
|
||||
let just_outside_upper = 100 + (N_BITS as u64);
|
||||
assert!(validator.will_accept_branchless(just_outside_upper).is_ok());
|
||||
|
||||
// Test packet near the lower edge of the window
|
||||
let lower_edge = 100 + 1; // +1 because we've already processed 100
|
||||
assert!(validator.will_accept_branchless(lower_edge).is_ok());
|
||||
|
||||
// Test packet just outside the lower edge (should be rejected)
|
||||
if upper_edge >= (N_BITS as u64) * 2 {
|
||||
// Only test this if we're far enough along to have a lower bound
|
||||
let just_outside_lower = 100 - (N_BITS as u64);
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(just_outside_lower),
|
||||
Err(ReplayError::OutOfWindow)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_window_shifts() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// First jump - process packet far ahead
|
||||
let first_jump = 2000;
|
||||
assert!(validator.mark_did_receive_branchless(first_jump).is_ok());
|
||||
|
||||
// Verify next counter is updated
|
||||
let (next, _) = validator.current_packet_cnt();
|
||||
assert_eq!(next, first_jump + 1);
|
||||
|
||||
// Second large jump, even further ahead
|
||||
let second_jump = first_jump + 5000;
|
||||
assert!(validator.mark_did_receive_branchless(second_jump).is_ok());
|
||||
|
||||
// Verify next counter is updated again
|
||||
let (next, _) = validator.current_packet_cnt();
|
||||
assert_eq!(next, second_jump + 1);
|
||||
|
||||
// Test packets within the new window
|
||||
let mid_window = second_jump - 500;
|
||||
assert!(validator.will_accept_branchless(mid_window).is_ok());
|
||||
|
||||
// Test packets outside the new window
|
||||
let outside_window = first_jump + 100;
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(outside_window),
|
||||
Err(ReplayError::OutOfWindow)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interleaved_packets_at_boundaries() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Jump ahead to establish a large window
|
||||
let jump = 2000;
|
||||
assert!(validator.mark_did_receive_branchless(jump).is_ok());
|
||||
|
||||
// Process a sequence at the upper boundary
|
||||
for i in 0..10 {
|
||||
let upper_packet = jump + 100 + i;
|
||||
assert!(validator.mark_did_receive_branchless(upper_packet).is_ok());
|
||||
}
|
||||
|
||||
// Process a sequence at the lower boundary
|
||||
for i in 0..10 {
|
||||
let lower_packet = jump - (N_BITS as u64) + 100 + i;
|
||||
// These might fail if they're outside the window, that's ok
|
||||
let _ = validator.mark_did_receive_branchless(lower_packet);
|
||||
}
|
||||
|
||||
// Process alternating packets at both ends
|
||||
for i in 0..5 {
|
||||
let upper = jump + 200 + i;
|
||||
let lower = jump - (N_BITS as u64) + 200 + i;
|
||||
|
||||
assert!(validator.will_accept_branchless(upper).is_ok());
|
||||
let lower_result = validator.will_accept_branchless(lower);
|
||||
|
||||
// Lower might be accepted or rejected, depending on exactly where the window is
|
||||
if lower_result.is_ok() {
|
||||
assert!(validator.mark_did_receive_branchless(lower).is_ok());
|
||||
}
|
||||
|
||||
assert!(validator.mark_did_receive_branchless(upper).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_window_size_with_full_bitmap() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Fill the entire bitmap with non-sequential packets
|
||||
// This tests both window size and bitmap capacity
|
||||
|
||||
// Generate a random but reproducible pattern
|
||||
let mut positions = Vec::new();
|
||||
for i in 0..N_BITS {
|
||||
positions.push((i * 7) % N_BITS);
|
||||
}
|
||||
|
||||
// Mark packets in this pattern
|
||||
for pos in &positions {
|
||||
assert!(validator.mark_did_receive_branchless(*pos as u64).is_ok());
|
||||
}
|
||||
|
||||
// Try to mark them again (should all fail as duplicates)
|
||||
for pos in &positions {
|
||||
assert!(matches!(
|
||||
validator.mark_did_receive_branchless(*pos as u64),
|
||||
Err(ReplayError::DuplicateCounter)
|
||||
));
|
||||
}
|
||||
|
||||
// Force window to slide
|
||||
let far_ahead = (N_BITS as u64) * 2;
|
||||
assert!(validator.mark_did_receive_branchless(far_ahead).is_ok());
|
||||
|
||||
// Old packets should now be outside the window
|
||||
for pos in &positions {
|
||||
if *pos as u64 + (N_BITS as u64) < far_ahead {
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(*pos as u64),
|
||||
Err(ReplayError::OutOfWindow)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_access() {
|
||||
let validator = Arc::new(std::sync::Mutex::new(
|
||||
ReceivingKeyCounterValidator::default(),
|
||||
));
|
||||
let num_threads = 8;
|
||||
let operations_per_thread = 1000;
|
||||
let barrier = Arc::new(Barrier::new(num_threads));
|
||||
|
||||
// Create thread handles
|
||||
let mut handles = vec![];
|
||||
|
||||
for thread_id in 0..num_threads {
|
||||
let validator_clone = Arc::clone(&validator);
|
||||
let barrier_clone = Arc::clone(&barrier);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
// Wait for all threads to be ready
|
||||
barrier_clone.wait();
|
||||
|
||||
let mut successes = 0;
|
||||
let mut duplicates = 0;
|
||||
let mut out_of_window = 0;
|
||||
|
||||
for i in 0..operations_per_thread {
|
||||
// Generate a somewhat random but reproducible counter value
|
||||
// Different threads will sometimes try to insert the same value
|
||||
let counter = (i * 7 + thread_id * 13) as u64;
|
||||
|
||||
let mut guard = validator_clone.lock().unwrap();
|
||||
match guard.mark_did_receive_branchless(counter) {
|
||||
Ok(()) => successes += 1,
|
||||
Err(ReplayError::DuplicateCounter) => duplicates += 1,
|
||||
Err(ReplayError::OutOfWindow) => out_of_window += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(successes, duplicates, out_of_window)
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Collect results
|
||||
let mut total_successes = 0;
|
||||
let mut total_duplicates = 0;
|
||||
let mut total_out_of_window = 0;
|
||||
|
||||
for handle in handles {
|
||||
let (successes, duplicates, out_of_window) = handle.join().unwrap();
|
||||
total_successes += successes;
|
||||
total_duplicates += duplicates;
|
||||
total_out_of_window += out_of_window;
|
||||
}
|
||||
|
||||
// Verify that all operations were accounted for
|
||||
assert_eq!(
|
||||
total_successes + total_duplicates + total_out_of_window,
|
||||
num_threads * operations_per_thread
|
||||
);
|
||||
|
||||
// Verify that some operations were successful and some were duplicates
|
||||
assert!(total_successes > 0);
|
||||
assert!(total_duplicates > 0);
|
||||
|
||||
// Check final state of the validator
|
||||
let final_state = validator.lock().unwrap();
|
||||
let (_next, receive_cnt) = final_state.current_packet_cnt();
|
||||
|
||||
// Verify that the received count matches our successful operations
|
||||
assert_eq!(receive_cnt, total_successes as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_usage() {
|
||||
use std::mem::{size_of, size_of_val};
|
||||
|
||||
// Test small validator
|
||||
let validator_default = ReceivingKeyCounterValidator::default();
|
||||
let size_default = size_of_val(&validator_default);
|
||||
|
||||
// Expected size calculation
|
||||
let expected_size = size_of::<u64>() * 2 + // next + receive_cnt
|
||||
size_of::<u64>() * N_WORDS; // bitmap
|
||||
|
||||
assert_eq!(size_default, expected_size);
|
||||
println!("Default validator size: {} bytes", size_default);
|
||||
|
||||
// Memory efficiency calculation (bits tracked per byte of memory)
|
||||
let bits_per_byte = N_BITS as f64 / size_default as f64;
|
||||
println!(
|
||||
"Memory efficiency: {:.2} bits tracked per byte of memory",
|
||||
bits_per_byte
|
||||
);
|
||||
|
||||
// Verify minimum memory needed for different window sizes
|
||||
for window_size in [64, 128, 256, 512, 1024, 2048] {
|
||||
let words_needed = (window_size + WORD_SIZE - 1) / WORD_SIZE; // Ceiling division
|
||||
let memory_needed = size_of::<u64>() * 2 + size_of::<u64>() * words_needed;
|
||||
println!(
|
||||
"Window size {}: {} bytes minimum",
|
||||
window_size, memory_needed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
target_feature = "sse2",
|
||||
target_feature = "avx2",
|
||||
target_feature = "neon"
|
||||
))]
|
||||
fn test_simd_operations() {
|
||||
// This test verifies that SIMD-optimized operations would produce
|
||||
// the same results as the scalar implementation
|
||||
|
||||
// Create a validator with a known state
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Fill bitmap with a pattern
|
||||
for i in 0..64 {
|
||||
validator.set_bit(i);
|
||||
}
|
||||
|
||||
// Create a copy for comparison
|
||||
let _original_bitmap = validator.bitmap;
|
||||
|
||||
// Simulate SIMD clear (4 words at a time)
|
||||
#[cfg(target_feature = "avx2")]
|
||||
{
|
||||
use std::arch::x86_64::{_mm256_setzero_si256, _mm256_storeu_si256};
|
||||
|
||||
// Clear words 0-3 using AVX2
|
||||
unsafe {
|
||||
let zero_vec = _mm256_setzero_si256();
|
||||
_mm256_storeu_si256(validator.bitmap.as_mut_ptr() as *mut _, zero_vec);
|
||||
}
|
||||
|
||||
// Verify first 4 words are cleared
|
||||
assert_eq!(validator.bitmap[0], 0);
|
||||
assert_eq!(validator.bitmap[1], 0);
|
||||
assert_eq!(validator.bitmap[2], 0);
|
||||
assert_eq!(validator.bitmap[3], 0);
|
||||
|
||||
// Verify other words are unchanged
|
||||
for i in 4..N_WORDS {
|
||||
assert_eq!(validator.bitmap[i], original_bitmap[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_feature = "sse2")]
|
||||
{
|
||||
use std::arch::x86_64::{_mm_setzero_si128, _mm_storeu_si128};
|
||||
|
||||
// Reset validator
|
||||
validator.bitmap = original_bitmap;
|
||||
|
||||
// Clear words 0-1 using SSE2
|
||||
unsafe {
|
||||
let zero_vec = _mm_setzero_si128();
|
||||
_mm_storeu_si128(validator.bitmap.as_mut_ptr() as *mut _, zero_vec);
|
||||
}
|
||||
|
||||
// Verify first 2 words are cleared
|
||||
assert_eq!(validator.bitmap[0], 0);
|
||||
assert_eq!(validator.bitmap[1], 0);
|
||||
|
||||
// Verify other words are unchanged
|
||||
for i in 2..N_WORDS {
|
||||
assert_eq!(validator.bitmap[i], original_bitmap[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// No SIMD available, make this test a no-op
|
||||
#[cfg(not(any(
|
||||
target_feature = "sse2",
|
||||
target_feature = "avx2",
|
||||
target_feature = "neon"
|
||||
)))]
|
||||
{
|
||||
println!("No SIMD features available, skipping SIMD test");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_window_overflow() {
|
||||
let mut validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Set a very large next value, close to u64::MAX
|
||||
validator.next = u64::MAX - 1000;
|
||||
|
||||
// Try to clear window with an even higher counter
|
||||
// This should exercise the potentially problematic code
|
||||
let counter = u64::MAX - 500;
|
||||
|
||||
// Call clear_window directly (this is what we suspect has issues)
|
||||
validator.clear_window(counter);
|
||||
|
||||
// If we got here without a panic, at least it's not crashing
|
||||
// Let's verify the bitmap state is reasonable
|
||||
let any_non_zero = validator.bitmap.iter().any(|&word| word != 0);
|
||||
assert!(!any_non_zero, "Bitmap should be cleared");
|
||||
|
||||
// Try the full function which uses clear_window internally
|
||||
assert!(validator.mark_did_receive_branchless(counter).is_ok());
|
||||
|
||||
// Verify it was marked
|
||||
assert!(matches!(
|
||||
validator.will_accept_branchless(counter),
|
||||
Err(ReplayError::DuplicateCounter)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,658 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Session management for the Lewes Protocol.
|
||||
//!
|
||||
//! This module implements session management functionality, including replay protection
|
||||
//! and Noise protocol state handling.
|
||||
|
||||
use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult};
|
||||
use crate::packet::LpHeader;
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use parking_lot::Mutex;
|
||||
use snow::Builder;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// A session in the Lewes Protocol, handling connection state with Noise.
|
||||
///
|
||||
/// Sessions manage connection state, including LP replay protection and Noise cryptography.
|
||||
/// Each session has a unique receiving index and sending index for connection identification.
|
||||
#[derive(Debug)]
|
||||
pub struct LpSession {
|
||||
id: u32,
|
||||
|
||||
/// Flag indicating if this session acts as the Noise protocol initiator.
|
||||
is_initiator: bool,
|
||||
|
||||
/// Noise protocol state machine
|
||||
noise_state: Mutex<NoiseProtocol>,
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: AtomicU64,
|
||||
|
||||
/// Validator for incoming packet counters to prevent replay attacks
|
||||
receiving_counter: Mutex<ReceivingKeyCounterValidator>,
|
||||
}
|
||||
|
||||
impl LpSession {
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn noise_state(&self) -> &Mutex<NoiseProtocol> {
|
||||
&self.noise_state
|
||||
}
|
||||
|
||||
/// Returns true if this session was created as the initiator.
|
||||
pub fn is_initiator(&self) -> bool {
|
||||
self.is_initiator
|
||||
}
|
||||
|
||||
/// Creates a new session and initializes the Noise protocol state.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `receiving_index` - Index used for receiving packets (becomes session ID).
|
||||
/// * `sending_index` - Index used for sending packets to the peer.
|
||||
/// * `is_initiator` - True if this side initiates the Noise handshake.
|
||||
/// * `local_static_key` - This side's static private key (e.g., X25519).
|
||||
/// * `remote_static_key` - The peer's static public key (required for initiator in some patterns like XK).
|
||||
/// * `psk` - The pre-shared key established out-of-band.
|
||||
/// * `pattern_name` - The Noise protocol pattern string (e.g., "Noise_XKpsk3_25519_ChaChaPoly_SHA256").
|
||||
/// * `psk_index` - The index/position where the PSK is mixed in according to the pattern.
|
||||
pub fn new(
|
||||
id: u32,
|
||||
is_initiator: bool,
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<Self, LpError> {
|
||||
// XKpsk3 pattern requires remote static key known upfront (XK)
|
||||
// and PSK mixed at position 3. This provides forward secrecy with PSK authentication.
|
||||
let pattern_name = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
let psk_index = 3;
|
||||
|
||||
let params = pattern_name.parse()?;
|
||||
let builder = Builder::new(params);
|
||||
|
||||
let builder = builder.local_private_key(local_private_key);
|
||||
|
||||
let builder = builder.remote_public_key(remote_public_key);
|
||||
|
||||
let builder = builder.psk(psk_index, psk);
|
||||
|
||||
let initial_state = if is_initiator {
|
||||
builder.build_initiator().map_err(LpError::SnowKeyError)?
|
||||
} else {
|
||||
builder.build_responder().map_err(LpError::SnowKeyError)?
|
||||
};
|
||||
|
||||
let noise_protocol = NoiseProtocol::new(initial_state);
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
is_initiator,
|
||||
noise_state: Mutex::new(noise_protocol),
|
||||
sending_counter: AtomicU64::new(0),
|
||||
receiving_counter: Mutex::new(ReceivingKeyCounterValidator::default()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_packet(&self, message: LpMessage) -> Result<LpPacket, LpError> {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(self.id(), counter);
|
||||
let packet = LpPacket::new(header, message);
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
/// Generates the next counter value for outgoing packets.
|
||||
pub fn next_counter(&self) -> u64 {
|
||||
self.sending_counter.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Performs a quick validation check for an incoming packet counter.
|
||||
///
|
||||
/// This should be called before performing any expensive operations like
|
||||
/// decryption/Noise processing to efficiently filter out potential replay attacks.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `counter` - The counter value to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the counter is likely valid
|
||||
/// * `Err(LpError::Replay)` if the counter is invalid or a potential replay
|
||||
pub fn receiving_counter_quick_check(&self, counter: u64) -> Result<(), LpError> {
|
||||
// Branchless implementation uses SIMD when available for constant-time
|
||||
// operations, preventing timing attacks. Check before crypto to save CPU cycles.
|
||||
let counter_validator = self.receiving_counter.lock();
|
||||
counter_validator
|
||||
.will_accept_branchless(counter)
|
||||
.map_err(LpError::Replay)
|
||||
}
|
||||
|
||||
/// Marks a counter as received after successful packet processing.
|
||||
///
|
||||
/// This should be called after a packet has been successfully decoded and processed
|
||||
/// (including Noise decryption/handshake step) to update the replay protection state.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `counter` - The counter value to mark as received
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the counter was successfully marked
|
||||
/// * `Err(LpError::Replay)` if the counter cannot be marked (duplicate, too old, etc.)
|
||||
pub fn receiving_counter_mark(&self, counter: u64) -> Result<(), LpError> {
|
||||
let mut counter_validator = self.receiving_counter.lock();
|
||||
counter_validator
|
||||
.mark_did_receive_branchless(counter)
|
||||
.map_err(LpError::Replay)
|
||||
}
|
||||
|
||||
/// Returns current packet statistics for monitoring.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple containing:
|
||||
/// * The next expected counter value for incoming packets
|
||||
/// * The total number of received packets
|
||||
pub fn current_packet_cnt(&self) -> (u64, u64) {
|
||||
let counter_validator = self.receiving_counter.lock();
|
||||
counter_validator.current_packet_cnt()
|
||||
}
|
||||
|
||||
/// Prepares the next handshake message to be sent, if any.
|
||||
///
|
||||
/// This should be called by the driver/IO layer to check if the Noise protocol
|
||||
/// state machine requires a message to be sent to the peer.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(None)` if no message needs to be sent currently (e.g., waiting for peer, or handshake complete).
|
||||
/// * `Err(NoiseError)` if there's an error within the Noise protocol state.
|
||||
pub fn prepare_handshake_message(&self) -> Option<Result<LpMessage, LpError>> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
if let Some(message) = noise_state.get_bytes_to_send() {
|
||||
match message {
|
||||
Ok(message) => Some(Ok(LpMessage::Handshake(message))),
|
||||
Err(e) => Some(Err(LpError::NoiseError(e))),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a received handshake message from the peer.
|
||||
///
|
||||
/// This should be called by the driver/IO layer after receiving a potential
|
||||
/// handshake message payload from an LP packet.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `noise_payload` - The raw bytes received from the peer, purported to be a Noise handshake message.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(ReadResult)` detailing the outcome (e.g., handshake complete, no-op).
|
||||
/// * `Err(NoiseError)` if the message is invalid or causes a Noise protocol error.
|
||||
pub fn process_handshake_message(&self, message: &LpMessage) -> Result<ReadResult, NoiseError> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
|
||||
match message {
|
||||
LpMessage::Handshake(payload) => {
|
||||
// The sans-io NoiseProtocol::read_message expects only the payload.
|
||||
noise_state.read_message(payload)
|
||||
}
|
||||
_ => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the Noise handshake phase is complete.
|
||||
pub fn is_handshake_complete(&self) -> bool {
|
||||
self.noise_state.lock().is_handshake_finished()
|
||||
}
|
||||
|
||||
/// Encrypts application data payload using the established Noise transport session.
|
||||
///
|
||||
/// This should only be called after the handshake is complete (`is_handshake_complete` returns true).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payload` - The application data to encrypt.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the encrypted Noise message ciphertext.
|
||||
/// * `Err(NoiseError)` if the session is not in transport mode or encryption fails.
|
||||
pub fn encrypt_data(&self, payload: &[u8]) -> Result<LpMessage, NoiseError> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
// Explicitly check if handshake is finished before trying to write
|
||||
if !noise_state.is_handshake_finished() {
|
||||
return Err(NoiseError::IncorrectStateError);
|
||||
}
|
||||
let payload = noise_state.write_message(payload)?;
|
||||
Ok(LpMessage::EncryptedData(payload))
|
||||
}
|
||||
|
||||
/// Decrypts an incoming Noise message containing application data.
|
||||
///
|
||||
/// This should only be called after the handshake is complete (`is_handshake_complete` returns true)
|
||||
/// and when an `LPMessage::EncryptedData` is received.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `noise_ciphertext` - The encrypted Noise message received from the peer.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the decrypted application data payload.
|
||||
/// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data.
|
||||
pub fn decrypt_data(&self, noise_ciphertext: &LpMessage) -> Result<Vec<u8>, NoiseError> {
|
||||
let mut noise_state = self.noise_state.lock();
|
||||
// Explicitly check if handshake is finished before trying to read
|
||||
if !noise_state.is_handshake_finished() {
|
||||
return Err(NoiseError::IncorrectStateError);
|
||||
}
|
||||
|
||||
let payload = noise_ciphertext.payload();
|
||||
|
||||
match noise_state.read_message(payload)? {
|
||||
ReadResult::DecryptedData(data) => Ok(data),
|
||||
_ => Err(NoiseError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use snow::{params::NoiseParams, Keypair};
|
||||
|
||||
use super::*;
|
||||
use crate::{replay::ReplayError, sessions_for_tests, NOISE_PATTERN};
|
||||
|
||||
// Helper function to generate keypairs for tests
|
||||
fn generate_keypair() -> Keypair {
|
||||
let params: NoiseParams = NOISE_PATTERN.parse().unwrap();
|
||||
snow::Builder::new(params).generate_keypair().unwrap()
|
||||
}
|
||||
|
||||
// Helper function to create a session with real keys for handshake tests
|
||||
fn create_handshake_test_session(
|
||||
is_initiator: bool,
|
||||
local_keys: &Keypair,
|
||||
remote_pub_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> LpSession {
|
||||
// Use a dummy ID for testing, the important part is is_initiator
|
||||
let test_id = if is_initiator { 1 } else { 2 };
|
||||
LpSession::new(
|
||||
test_id,
|
||||
is_initiator,
|
||||
&local_keys.private,
|
||||
remote_pub_key,
|
||||
psk,
|
||||
)
|
||||
.expect("Test session creation failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_creation() {
|
||||
let session = sessions_for_tests().0;
|
||||
|
||||
// Initial counter should be zero
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 0);
|
||||
|
||||
// Counter should increment
|
||||
let counter = session.next_counter();
|
||||
assert_eq!(counter, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_sequential() {
|
||||
let session = sessions_for_tests().1;
|
||||
|
||||
// Sequential counters should be accepted
|
||||
assert!(session.receiving_counter_quick_check(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
|
||||
assert!(session.receiving_counter_quick_check(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
|
||||
// Duplicates should be rejected
|
||||
assert!(session.receiving_counter_quick_check(0).is_err());
|
||||
let err = session.receiving_counter_mark(0).unwrap_err();
|
||||
match err {
|
||||
LpError::Replay(replay_error) => {
|
||||
assert!(matches!(replay_error, ReplayError::DuplicateCounter));
|
||||
}
|
||||
_ => panic!("Expected replay error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_out_of_order() {
|
||||
let session = sessions_for_tests().1;
|
||||
|
||||
// Receive packets in order
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
assert!(session.receiving_counter_mark(2).is_ok());
|
||||
|
||||
// Skip ahead
|
||||
assert!(session.receiving_counter_mark(10).is_ok());
|
||||
|
||||
// Can still receive out-of-order packets within window
|
||||
assert!(session.receiving_counter_quick_check(5).is_ok());
|
||||
assert!(session.receiving_counter_mark(5).is_ok());
|
||||
|
||||
// But duplicates are still rejected
|
||||
assert!(session.receiving_counter_quick_check(5).is_err());
|
||||
assert!(session.receiving_counter_mark(5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packet_stats() {
|
||||
let session = sessions_for_tests().1;
|
||||
|
||||
// Initial stats
|
||||
let (next, received) = session.current_packet_cnt();
|
||||
assert_eq!(next, 0);
|
||||
assert_eq!(received, 0);
|
||||
|
||||
// After receiving packets
|
||||
assert!(session.receiving_counter_mark(0).is_ok());
|
||||
assert!(session.receiving_counter_mark(1).is_ok());
|
||||
|
||||
let (next, received) = session.current_packet_cnt();
|
||||
assert_eq!(next, 2);
|
||||
assert_eq!(received, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_handshake_message_initial_state() {
|
||||
let initiator_keys = generate_keypair();
|
||||
let responder_keys = generate_keypair();
|
||||
let psk = [3u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk);
|
||||
let responder_session = create_handshake_test_session(
|
||||
false,
|
||||
&responder_keys,
|
||||
&initiator_keys.public, // Responder also needs initiator's key for XK
|
||||
&psk,
|
||||
);
|
||||
|
||||
// Initiator should have a message to send immediately (-> e)
|
||||
let initiator_msg_result = initiator_session.prepare_handshake_message();
|
||||
assert!(initiator_msg_result.is_some());
|
||||
let initiator_msg = initiator_msg_result
|
||||
.unwrap()
|
||||
.expect("Initiator msg prep failed");
|
||||
assert!(!initiator_msg.is_empty());
|
||||
|
||||
// Responder should have nothing to send initially (waits for <- e)
|
||||
let responder_msg_result = responder_session.prepare_handshake_message();
|
||||
assert!(responder_msg_result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_handshake_message_first_step() {
|
||||
let initiator_keys = generate_keypair();
|
||||
let responder_keys = generate_keypair();
|
||||
let psk = [4u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk);
|
||||
let responder_session =
|
||||
create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk);
|
||||
|
||||
// 1. Initiator prepares the first message (-> e)
|
||||
let initiator_msg_result = initiator_session.prepare_handshake_message();
|
||||
let initiator_msg = initiator_msg_result
|
||||
.unwrap()
|
||||
.expect("Initiator msg prep failed");
|
||||
|
||||
// 2. Responder processes the message (<- e)
|
||||
let process_result = responder_session.process_handshake_message(&initiator_msg);
|
||||
|
||||
// Check the result of processing
|
||||
match process_result {
|
||||
Ok(ReadResult::NoOp) => {
|
||||
// Expected for XK first message, responder doesn't decrypt data yet
|
||||
}
|
||||
Ok(other) => panic!("Unexpected process result: {:?}", other),
|
||||
Err(e) => panic!("Responder processing failed: {:?}", e),
|
||||
}
|
||||
|
||||
// 3. After processing, responder should now have a message to send (-> e, es)
|
||||
let responder_response_result = responder_session.prepare_handshake_message();
|
||||
assert!(responder_response_result.is_some());
|
||||
let responder_response = responder_response_result
|
||||
.unwrap()
|
||||
.expect("Responder response prep failed");
|
||||
assert!(!responder_response.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handshake_driver_simulation() {
|
||||
let initiator_keys = generate_keypair();
|
||||
let responder_keys = generate_keypair();
|
||||
let psk = [5u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk);
|
||||
let responder_session =
|
||||
create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk);
|
||||
|
||||
let mut responder_to_initiator_msg = None;
|
||||
let mut rounds = 0;
|
||||
const MAX_ROUNDS: usize = 10; // Safety break for the loop
|
||||
|
||||
// Start by priming the initiator message
|
||||
let mut initiator_to_responder_msg =
|
||||
initiator_session.prepare_handshake_message().unwrap().ok();
|
||||
assert!(
|
||||
initiator_to_responder_msg.is_some(),
|
||||
"Initiator did not produce initial message"
|
||||
);
|
||||
|
||||
while rounds < MAX_ROUNDS {
|
||||
rounds += 1;
|
||||
|
||||
// === Initiator -> Responder ===
|
||||
if let Some(msg) = initiator_to_responder_msg.take() {
|
||||
// Process message
|
||||
match responder_session.process_handshake_message(&msg) {
|
||||
Ok(_) => {}
|
||||
Err(e) => panic!("Responder processing failed: {:?}", e),
|
||||
}
|
||||
|
||||
// Check if responder needs to send a reply
|
||||
responder_to_initiator_msg = responder_session
|
||||
.prepare_handshake_message()
|
||||
.transpose()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Check completion after potentially processing responder's message below
|
||||
if initiator_session.is_handshake_complete()
|
||||
&& responder_session.is_handshake_complete()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// === Responder -> Initiator ===
|
||||
if let Some(msg) = responder_to_initiator_msg.take() {
|
||||
// Process message
|
||||
match initiator_session.process_handshake_message(&msg) {
|
||||
Ok(_) => {}
|
||||
Err(e) => panic!("Initiator processing failed: {:?}", e),
|
||||
}
|
||||
|
||||
// Check if initiator needs to send a reply (should be last message in XK)
|
||||
initiator_to_responder_msg = initiator_session
|
||||
.prepare_handshake_message()
|
||||
.transpose()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Check completion again after potentially processing initiator's message above
|
||||
if initiator_session.is_handshake_complete()
|
||||
&& responder_session.is_handshake_complete()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
rounds < MAX_ROUNDS,
|
||||
"Handshake did not complete within max rounds"
|
||||
);
|
||||
assert!(
|
||||
initiator_session.is_handshake_complete(),
|
||||
"Initiator handshake did not complete"
|
||||
);
|
||||
assert!(
|
||||
responder_session.is_handshake_complete(),
|
||||
"Responder handshake did not complete"
|
||||
);
|
||||
|
||||
println!("Handshake completed in {} rounds.", rounds);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_after_handshake() {
|
||||
// --- Setup Handshake ---
|
||||
let initiator_keys = generate_keypair();
|
||||
let responder_keys = generate_keypair();
|
||||
let psk = [6u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk);
|
||||
let responder_session =
|
||||
create_handshake_test_session(false, &responder_keys, &initiator_keys.public, &psk);
|
||||
|
||||
// Drive handshake to completion (simplified loop from previous test)
|
||||
let mut i_msg = initiator_session
|
||||
.prepare_handshake_message()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
responder_session.process_handshake_message(&i_msg).unwrap();
|
||||
let r_msg = responder_session
|
||||
.prepare_handshake_message()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
initiator_session.process_handshake_message(&r_msg).unwrap();
|
||||
i_msg = initiator_session
|
||||
.prepare_handshake_message()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
responder_session.process_handshake_message(&i_msg).unwrap();
|
||||
|
||||
assert!(initiator_session.is_handshake_complete());
|
||||
assert!(responder_session.is_handshake_complete());
|
||||
|
||||
// --- Test Encryption/Decryption ---
|
||||
let plaintext = b"Hello, Lewes Protocol!";
|
||||
|
||||
// Initiator encrypts
|
||||
let ciphertext = initiator_session
|
||||
.encrypt_data(plaintext)
|
||||
.expect("Initiator encryption failed");
|
||||
assert_ne!(ciphertext.payload(), plaintext); // Ensure it's actually encrypted
|
||||
|
||||
// Responder decrypts
|
||||
let decrypted = responder_session
|
||||
.decrypt_data(&ciphertext)
|
||||
.expect("Responder decryption failed");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// --- Test other direction ---
|
||||
let plaintext2 = b"Response from responder.";
|
||||
|
||||
// Responder encrypts
|
||||
let ciphertext2 = responder_session
|
||||
.encrypt_data(plaintext2)
|
||||
.expect("Responder encryption failed");
|
||||
assert_ne!(ciphertext2.payload(), plaintext2);
|
||||
|
||||
// Initiator decrypts
|
||||
let decrypted2 = initiator_session
|
||||
.decrypt_data(&ciphertext2)
|
||||
.expect("Initiator decryption failed");
|
||||
assert_eq!(decrypted2, plaintext2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_before_handshake() {
|
||||
let initiator_keys = generate_keypair();
|
||||
let responder_keys = generate_keypair();
|
||||
let psk = [7u8; 32];
|
||||
|
||||
let initiator_session =
|
||||
create_handshake_test_session(true, &initiator_keys, &responder_keys.public, &psk);
|
||||
|
||||
assert!(!initiator_session.is_handshake_complete());
|
||||
|
||||
// Attempt to encrypt before handshake
|
||||
let plaintext = b"This should fail";
|
||||
let result = initiator_session.encrypt_data(plaintext);
|
||||
assert!(result.is_err());
|
||||
match result.unwrap_err() {
|
||||
NoiseError::IncorrectStateError => {} // Expected error
|
||||
e => panic!("Expected IncorrectStateError, got {:?}", e),
|
||||
}
|
||||
|
||||
// Attempt to decrypt before handshake (using dummy ciphertext)
|
||||
let dummy_ciphertext = vec![0u8; 32];
|
||||
let result_decrypt =
|
||||
initiator_session.decrypt_data(&LpMessage::EncryptedData(dummy_ciphertext));
|
||||
assert!(result_decrypt.is_err());
|
||||
match result_decrypt.unwrap_err() {
|
||||
NoiseError::IncorrectStateError => {} // Expected error
|
||||
e => panic!("Expected IncorrectStateError, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// These tests remain commented as they rely on the old mock crypto functions
|
||||
#[test]
|
||||
fn test_mock_crypto() {
|
||||
let session = create_test_session(true);
|
||||
let data = [1, 2, 3, 4, 5];
|
||||
let mut encrypted = [0; 5];
|
||||
let mut decrypted = [0; 5];
|
||||
|
||||
// Mock encrypt should copy the data
|
||||
// let encrypted_len = session.encrypt_packet(&data, &mut encrypted).unwrap(); // Removed method
|
||||
// assert_eq!(encrypted_len, 5);
|
||||
// assert_eq!(encrypted, data);
|
||||
|
||||
// Mock decrypt should copy the data
|
||||
// let decrypted_len = session.decrypt_packet(&encrypted, &mut decrypted).unwrap(); // Removed method
|
||||
// assert_eq!(decrypted_len, 5);
|
||||
// assert_eq!(decrypted, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_crypto_buffer_too_small() {
|
||||
let session = create_test_session(true);
|
||||
let data = [1, 2, 3, 4, 5];
|
||||
let mut too_small = [0; 3];
|
||||
|
||||
// Should fail with buffer too small
|
||||
// let result = session.encrypt_packet(&data, &mut too_small); // Removed method
|
||||
// assert!(result.is_err());
|
||||
// match result.unwrap_err() {
|
||||
// LpError::InsufficientBufferSize => {} // Error type might change
|
||||
// _ => panic!("Expected InsufficientBufferSize error"),
|
||||
// }
|
||||
}
|
||||
*/
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,296 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Session management for the Lewes Protocol.
|
||||
//!
|
||||
//! This module implements session lifecycle management functionality, handling
|
||||
//! creation, retrieval, and storage of sessions.
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
use crate::keypair::{Keypair, PublicKey};
|
||||
use crate::noise_protocol::ReadResult;
|
||||
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
|
||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions,
|
||||
/// ensuring proper thread-safety for concurrent access.
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
state_machines: DashMap<u32, LpStateMachine>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
/// Creates a new session manager with empty session storage.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_machines: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_input(&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 },
|
||||
};
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn transport(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Transport)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_state_machine_id(&self, lp_id: u32) -> Result<u32, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.id())?
|
||||
}
|
||||
|
||||
pub fn get_state(&self, lp_id: u32) -> Result<LpStateBare, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.bare_state()))?
|
||||
}
|
||||
|
||||
pub fn receiving_counter_quick_check(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.receiving_counter_quick_check(counter)
|
||||
})?
|
||||
}
|
||||
|
||||
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 start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.prepare_handshake_message(lp_id)
|
||||
}
|
||||
|
||||
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()?
|
||||
.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()?
|
||||
.encrypt_data(message)
|
||||
.map_err(LpError::NoiseError)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn current_packet_cnt(&self, lp_id: u32) -> Result<(u64, u64), LpError> {
|
||||
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| {
|
||||
Ok(sm.session()?.process_handshake_message(message)?)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.state_machines.len()
|
||||
}
|
||||
|
||||
pub fn state_machine_exists(&self, lp_id: u32) -> bool {
|
||||
self.state_machines.contains_key(&lp_id)
|
||||
}
|
||||
|
||||
pub fn with_state_machine<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&LpStateMachine) -> R,
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
Ok(f(&sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
// self.state_machines.get(&lp_id).map(|sm_ref| f(&*sm_ref)) // Lock held only during closure execution
|
||||
}
|
||||
|
||||
// For mutable access (like running process_input)
|
||||
pub fn with_state_machine_mut<F, R>(&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))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session_state_machine(
|
||||
&self,
|
||||
local_keypair: &Keypair,
|
||||
remote_public_key: &PublicKey,
|
||||
is_initiator: bool,
|
||||
psk: &[u8],
|
||||
) -> Result<u32, LpError> {
|
||||
let sm = LpStateMachine::new(is_initiator, local_keypair, remote_public_key, psk)?;
|
||||
let sm_id = sm.id()?;
|
||||
|
||||
self.state_machines.insert(sm_id, sm);
|
||||
Ok(sm_id)
|
||||
}
|
||||
|
||||
/// Method to remove a state machine
|
||||
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
|
||||
let removed = self.state_machines.remove(&lp_id);
|
||||
|
||||
removed.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_get() {
|
||||
let manager = SessionManager::new();
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
||||
assert!(retrieved);
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
assert!(!not_found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_remove() {
|
||||
let manager = SessionManager::new();
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
assert_eq!(manager.session_count(), 0);
|
||||
|
||||
let removed_again = manager.remove_state_machine(sm_1_id);
|
||||
assert!(!removed_again);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sessions() {
|
||||
let manager = SessionManager::new();
|
||||
|
||||
let sm_1 = manager
|
||||
.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_2 = manager
|
||||
.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_3 = manager
|
||||
.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
|
||||
let retrieved1 = manager.get_state_machine_id(sm_1).unwrap();
|
||||
let retrieved2 = manager.get_state_machine_id(sm_2).unwrap();
|
||||
let retrieved3 = manager.get_state_machine_id(sm_3).unwrap();
|
||||
|
||||
assert_eq!(retrieved1, sm_1);
|
||||
assert_eq!(retrieved2, sm_2);
|
||||
assert_eq!(retrieved3, sm_3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_create_session() {
|
||||
let manager = SessionManager::new();
|
||||
|
||||
let sm = manager.create_session_state_machine(
|
||||
&Keypair::default(),
|
||||
&PublicKey::default(),
|
||||
true,
|
||||
&[2u8; 32],
|
||||
);
|
||||
|
||||
assert!(sm.is_ok());
|
||||
let sm = sm.unwrap();
|
||||
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
assert!(retrieved.is_ok());
|
||||
assert_eq!(retrieved.unwrap(), sm);
|
||||
}
|
||||
}
|
||||
@@ -1,649 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Lewes Protocol State Machine for managing connection lifecycle.
|
||||
|
||||
use crate::{
|
||||
keypair::{Keypair, PublicKey},
|
||||
make_lp_id,
|
||||
noise_protocol::NoiseError,
|
||||
packet::LpPacket,
|
||||
session::LpSession,
|
||||
LpError,
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
use std::mem;
|
||||
|
||||
/// Represents the possible states of the Lewes Protocol connection.
|
||||
#[derive(Debug, Default)]
|
||||
pub enum LpState {
|
||||
/// Initial state: Ready to start the handshake.
|
||||
/// State machine is created with keys, lp_id is derived, session is ready.
|
||||
ReadyToHandshake { session: LpSession },
|
||||
|
||||
/// Actively performing the Noise handshake.
|
||||
/// (We might be able to merge this with ReadyToHandshake if the first step always happens)
|
||||
Handshaking { session: LpSession }, // Kept for now, logic might merge later
|
||||
|
||||
/// Handshake complete, ready for data transport.
|
||||
Transport { session: LpSession },
|
||||
/// An error occurred, or the connection was intentionally closed.
|
||||
Closed { reason: String },
|
||||
/// Processing an input event.
|
||||
#[default]
|
||||
Processing,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LpStateBare {
|
||||
ReadyToHandshake,
|
||||
Handshaking,
|
||||
Transport,
|
||||
Closed,
|
||||
Processing,
|
||||
}
|
||||
|
||||
impl From<&LpState> for LpStateBare {
|
||||
fn from(state: &LpState) -> Self {
|
||||
match state {
|
||||
LpState::ReadyToHandshake { .. } => LpStateBare::ReadyToHandshake,
|
||||
LpState::Handshaking { .. } => LpStateBare::Handshaking,
|
||||
LpState::Transport { .. } => LpStateBare::Transport,
|
||||
LpState::Closed { .. } => LpStateBare::Closed,
|
||||
LpState::Processing => LpStateBare::Processing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents inputs that drive the state machine transitions.
|
||||
#[derive(Debug)]
|
||||
pub enum LpInput {
|
||||
/// Explicitly trigger the start of the handshake (optional, could be implicit on creation)
|
||||
StartHandshake,
|
||||
/// Received an LP Packet from the network.
|
||||
ReceivePacket(LpPacket),
|
||||
/// Application wants to send data (only valid in Transport state).
|
||||
SendData(Vec<u8>), // Using Bytes for efficiency
|
||||
/// Close the connection.
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Represents actions the state machine requests the environment to perform.
|
||||
#[derive(Debug)]
|
||||
pub enum LpAction {
|
||||
/// Send an LP Packet over the network.
|
||||
SendPacket(LpPacket),
|
||||
/// Deliver decrypted application data received from the peer.
|
||||
DeliverData(BytesMut),
|
||||
/// Inform the environment that the handshake is complete.
|
||||
HandshakeComplete,
|
||||
/// Inform the environment that the connection is closed.
|
||||
ConnectionClosed,
|
||||
}
|
||||
|
||||
/// The Lewes Protocol State Machine.
|
||||
pub struct LpStateMachine {
|
||||
pub state: LpState,
|
||||
}
|
||||
|
||||
impl LpStateMachine {
|
||||
pub fn bare_state(&self) -> LpStateBare {
|
||||
LpStateBare::from(&self.state)
|
||||
}
|
||||
|
||||
pub fn session(&self) -> Result<&LpSession, LpError> {
|
||||
match &self.state {
|
||||
LpState::ReadyToHandshake { session }
|
||||
| LpState::Handshaking { session }
|
||||
| LpState::Transport { session } => Ok(session),
|
||||
LpState::Closed { .. } => Err(LpError::LpSessionClosed),
|
||||
LpState::Processing => Err(LpError::LpSessionProcessing),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the state machine and return the session with ownership.
|
||||
/// This is useful when the handshake is complete and you want to transfer
|
||||
/// ownership of the session to the caller.
|
||||
pub fn into_session(self) -> Result<LpSession, LpError> {
|
||||
match self.state {
|
||||
LpState::ReadyToHandshake { session }
|
||||
| LpState::Handshaking { session }
|
||||
| LpState::Transport { session } => Ok(session),
|
||||
LpState::Closed { .. } => Err(LpError::LpSessionClosed),
|
||||
LpState::Processing => Err(LpError::LpSessionProcessing),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Result<u32, LpError> {
|
||||
Ok(self.session()?.id())
|
||||
}
|
||||
|
||||
/// Creates a new state machine, calculates the lp_id, creates the session,
|
||||
/// and sets the initial state to ReadyToHandshake.
|
||||
///
|
||||
/// Requires the local *full* keypair to get the public key for lp_id calculation.
|
||||
pub fn new(
|
||||
is_initiator: bool,
|
||||
local_keypair: &Keypair, // Use Keypair
|
||||
remote_public_key: &PublicKey,
|
||||
psk: &[u8],
|
||||
// session_manager: Arc<SessionManager> // Optional
|
||||
) -> Result<Self, LpError> {
|
||||
// Calculate the shared lp_id// Calculate the shared lp_id
|
||||
let lp_id = make_lp_id(local_keypair.public_key(), remote_public_key);
|
||||
|
||||
let local_private_key = local_keypair.private_key().to_bytes();
|
||||
let remote_public_key = remote_public_key.as_bytes();
|
||||
|
||||
// Create the session immediately
|
||||
let session = LpSession::new(
|
||||
lp_id,
|
||||
is_initiator,
|
||||
&local_private_key,
|
||||
remote_public_key,
|
||||
psk,
|
||||
)?;
|
||||
|
||||
// TODO: Register the session with the SessionManager if applicable
|
||||
// if let Some(manager) = session_manager {
|
||||
// manager.insert_session(lp_id, session.clone())?; // Assuming insert_session exists
|
||||
// }
|
||||
|
||||
Ok(LpStateMachine {
|
||||
state: LpState::ReadyToHandshake { session },
|
||||
// Store necessary info if needed for recreation, otherwise remove
|
||||
// is_initiator,
|
||||
// local_private_key: local_private_key.to_vec(),
|
||||
// remote_public_key: remote_public_key.to_vec(),
|
||||
// psk: psk.to_vec(),
|
||||
})
|
||||
}
|
||||
/// Processes an input event and returns a list of actions to perform.
|
||||
pub fn process_input(&mut self, input: LpInput) -> Option<Result<LpAction, LpError>> {
|
||||
// 1. Replace current state with a placeholder, taking ownership of the real current state.
|
||||
let current_state = mem::take(&mut self.state);
|
||||
|
||||
let mut result_action: Option<Result<LpAction, LpError>> = None;
|
||||
|
||||
// 2. Match on the owned current_state. Each arm calculates and returns the NEXT state.
|
||||
let next_state = match (current_state, input) {
|
||||
// --- ReadyToHandshake State ---
|
||||
(LpState::ReadyToHandshake { session }, LpInput::StartHandshake) => {
|
||||
if session.is_initiator() {
|
||||
// Initiator sends the first message
|
||||
match self.start_handshake(&session) {
|
||||
Some(Ok(action)) => {
|
||||
result_action = Some(Ok(action));
|
||||
LpState::Handshaking { session } // Transition state
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
// Error occurred, move to Closed state
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
None => {
|
||||
// Should not happen, treat as internal error
|
||||
let err = LpError::Internal(
|
||||
"start_handshake returned None unexpectedly".to_string(),
|
||||
);
|
||||
let reason = err.to_string();
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Responder waits for the first message, transition to Handshaking to wait.
|
||||
LpState::Handshaking { session }
|
||||
// No action needed yet, result_action remains None.
|
||||
}
|
||||
}
|
||||
|
||||
// --- Handshaking State ---
|
||||
(LpState::Handshaking { session }, LpInput::ReceivePacket(packet)) => {
|
||||
// Check if packet lp_id matches our session
|
||||
if packet.header.session_id() != session.id() {
|
||||
result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id())));
|
||||
// Don't change state, return the original state variant
|
||||
LpState::Handshaking { session }
|
||||
} else {
|
||||
// --- Inline handle_handshake_packet logic ---
|
||||
// 1. Check replay protection *before* processing
|
||||
if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
|
||||
let _reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Handshaking { session }
|
||||
// LpState::Closed { reason }
|
||||
} else {
|
||||
// 2. Process the handshake message
|
||||
match session.process_handshake_message(&packet.message) {
|
||||
Ok(_) => {
|
||||
// 3. Mark counter as received *after* successful processing
|
||||
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
|
||||
let _reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
// LpState::Closed { reason }
|
||||
LpState::Handshaking { session }
|
||||
} else {
|
||||
// 4. Check if handshake is now complete
|
||||
if session.is_handshake_complete() {
|
||||
result_action = Some(Ok(LpAction::HandshakeComplete));
|
||||
LpState::Transport { session } // Transition to Transport
|
||||
} else {
|
||||
// 5. Check if we need to send the next handshake message
|
||||
match session.prepare_handshake_message() {
|
||||
Some(Ok(message)) => {
|
||||
match session.next_packet(message) {
|
||||
Ok(response_packet) => {
|
||||
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
|
||||
// Check AGAIN if handshake became complete *after preparing*
|
||||
if session.is_handshake_complete() {
|
||||
LpState::Transport { session } // Transition to Transport
|
||||
} else {
|
||||
LpState::Handshaking { session } // Remain Handshaking
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
None => {
|
||||
// Handshake stalled unexpectedly
|
||||
let err = LpError::NoiseError(NoiseError::Other(
|
||||
"Handshake stalled unexpectedly".to_string(),
|
||||
));
|
||||
let reason = err.to_string();
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => { // Error from process_handshake_message
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e.into()));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- End inline handle_handshake_packet logic ---
|
||||
}
|
||||
}
|
||||
// Reject SendData during handshake
|
||||
(LpState::Handshaking { session }, LpInput::SendData(_)) => { // Keep session if returning to this state
|
||||
result_action = Some(Err(LpError::InvalidStateTransition {
|
||||
state: "Handshaking".to_string(),
|
||||
input: "SendData".to_string(),
|
||||
}));
|
||||
// Invalid input, remain in Handshaking state
|
||||
LpState::Handshaking { session }
|
||||
}
|
||||
// Reject StartHandshake if already handshaking
|
||||
(LpState::Handshaking { session }, LpInput::StartHandshake) => { // Keep session
|
||||
result_action = Some(Err(LpError::InvalidStateTransition {
|
||||
state: "Handshaking".to_string(),
|
||||
input: "StartHandshake".to_string(),
|
||||
}));
|
||||
// Invalid input, remain in Handshaking state
|
||||
LpState::Handshaking { session }
|
||||
}
|
||||
|
||||
// --- Transport State ---
|
||||
(LpState::Transport { session }, LpInput::ReceivePacket(packet)) => { // Needs mut session for marking counter
|
||||
// Check if packet lp_id matches our session
|
||||
if packet.header.session_id() != session.id() {
|
||||
result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id())));
|
||||
// Remain in transport state
|
||||
LpState::Transport { session }
|
||||
} else {
|
||||
// --- Inline handle_data_packet logic ---
|
||||
// 1. Check replay protection
|
||||
if let Err(e) = session.receiving_counter_quick_check(packet.header.counter) {
|
||||
let _reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Transport { session }
|
||||
} else {
|
||||
// 2. Decrypt data
|
||||
match session.decrypt_data(&packet.message) {
|
||||
Ok(plaintext) => {
|
||||
// 3. Mark counter as received
|
||||
if let Err(e) = session.receiving_counter_mark(packet.header.counter) {
|
||||
let _reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Transport{ session }
|
||||
} else {
|
||||
// 4. Deliver data
|
||||
result_action = Some(Ok(LpAction::DeliverData(BytesMut::from(plaintext.as_slice()))));
|
||||
// Remain in transport state
|
||||
LpState::Transport { session }
|
||||
}
|
||||
}
|
||||
Err(e) => { // Error decrypting data
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e.into()));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- End inline handle_data_packet logic ---
|
||||
}
|
||||
}
|
||||
(LpState::Transport { session }, LpInput::SendData(data)) => {
|
||||
// Encrypt and send application data
|
||||
match self.prepare_data_packet(&session, &data) {
|
||||
Ok(packet) => result_action = Some(Ok(LpAction::SendPacket(packet))),
|
||||
Err(e) => {
|
||||
// If prepare fails, should we close? Let's report error and stay Transport for now.
|
||||
// Alternative: transition to Closed state.
|
||||
result_action = Some(Err(e.into()));
|
||||
}
|
||||
}
|
||||
// Remain in transport state
|
||||
LpState::Transport { session }
|
||||
}
|
||||
// Reject StartHandshake if already in transport
|
||||
(LpState::Transport { session }, LpInput::StartHandshake) => { // Keep session
|
||||
result_action = Some(Err(LpError::InvalidStateTransition {
|
||||
state: "Transport".to_string(),
|
||||
input: "StartHandshake".to_string(),
|
||||
}));
|
||||
// Invalid input, remain in Transport state
|
||||
LpState::Transport { session }
|
||||
}
|
||||
|
||||
// --- Close Transition (applies to ReadyToHandshake, Handshaking, Transport) ---
|
||||
(
|
||||
LpState::ReadyToHandshake { .. } // We consume the session here
|
||||
| LpState::Handshaking { .. }
|
||||
| LpState::Transport { .. },
|
||||
LpInput::Close,
|
||||
) => {
|
||||
result_action = Some(Ok(LpAction::ConnectionClosed));
|
||||
// Transition to Closed state
|
||||
LpState::Closed { reason: "Closed by user".to_string() }
|
||||
}
|
||||
// Ignore Close if already Closed
|
||||
(closed_state @ LpState::Closed { .. }, LpInput::Close) => {
|
||||
// result_action remains None
|
||||
// Return the original closed state
|
||||
closed_state
|
||||
}
|
||||
// Ignore StartHandshake if Closed
|
||||
// (closed_state @ LpState::Closed { .. }, LpInput::StartHandshake) => {
|
||||
// result_action = Some(Err(LpError::LpSessionClosed));
|
||||
// closed_state
|
||||
// }
|
||||
// Ignore ReceivePacket if Closed
|
||||
(closed_state @ LpState::Closed { .. }, LpInput::ReceivePacket(_)) => {
|
||||
result_action = Some(Err(LpError::LpSessionClosed));
|
||||
closed_state
|
||||
}
|
||||
// Ignore SendData if Closed
|
||||
(closed_state @ LpState::Closed { .. }, LpInput::SendData(_)) => {
|
||||
result_action = Some(Err(LpError::LpSessionClosed));
|
||||
closed_state
|
||||
}
|
||||
// Processing state should not be matched directly if using replace
|
||||
(LpState::Processing, _) => {
|
||||
// This case should ideally be unreachable if placeholder logic is correct
|
||||
let err = LpError::Internal("Reached Processing state unexpectedly".to_string());
|
||||
let reason = err.to_string();
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
|
||||
// --- Default: Invalid input for current state (if any combinations missed) ---
|
||||
// Consider if this should transition to Closed state. For now, just report error
|
||||
// and transition to Closed as a safety measure.
|
||||
(invalid_state, input) => {
|
||||
let err = LpError::InvalidStateTransition {
|
||||
state: format!("{:?}", invalid_state), // Use owned state for debug info
|
||||
input: format!("{:?}", input),
|
||||
};
|
||||
let reason = err.to_string();
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Put the calculated next state back into the machine.
|
||||
self.state = next_state;
|
||||
|
||||
result_action // Return the determined action (or None)
|
||||
}
|
||||
|
||||
// Helper to start the handshake (sends first message if initiator)
|
||||
// Kept as it doesn't mutate self.state
|
||||
fn start_handshake(&self, session: &LpSession) -> Option<Result<LpAction, LpError>> {
|
||||
session
|
||||
.prepare_handshake_message()
|
||||
.map(|result| match result {
|
||||
Ok(message) => match session.next_packet(message) {
|
||||
Ok(packet) => Ok(LpAction::SendPacket(packet)),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to prepare an outgoing data packet
|
||||
// Kept as it doesn't mutate self.state
|
||||
fn prepare_data_packet(
|
||||
&self,
|
||||
session: &LpSession,
|
||||
data: &[u8],
|
||||
) -> Result<LpPacket, NoiseError> {
|
||||
let encrypted_message = session.encrypt_data(data)?;
|
||||
session
|
||||
.next_packet(encrypted_message)
|
||||
.map_err(|e| NoiseError::Other(e.to_string())) // Improve error conversion?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::keypair::Keypair;
|
||||
use bytes::Bytes;
|
||||
|
||||
#[test]
|
||||
fn test_state_machine_init() {
|
||||
let init_key = Keypair::new();
|
||||
let resp_key = Keypair::new();
|
||||
let psk = vec![0u8; 32];
|
||||
let remote_pub_key = resp_key.public_key();
|
||||
|
||||
let initiator_sm = LpStateMachine::new(true, &init_key, remote_pub_key, &psk);
|
||||
assert!(initiator_sm.is_ok());
|
||||
let initiator_sm = initiator_sm.unwrap();
|
||||
assert!(matches!(
|
||||
initiator_sm.state,
|
||||
LpState::ReadyToHandshake { .. }
|
||||
));
|
||||
let init_session = initiator_sm.session().unwrap();
|
||||
assert!(init_session.is_initiator());
|
||||
|
||||
let responder_sm = LpStateMachine::new(false, &resp_key, init_key.public_key(), &psk);
|
||||
assert!(responder_sm.is_ok());
|
||||
let responder_sm = responder_sm.unwrap();
|
||||
assert!(matches!(
|
||||
responder_sm.state,
|
||||
LpState::ReadyToHandshake { .. }
|
||||
));
|
||||
let resp_session = responder_sm.session().unwrap();
|
||||
assert!(!resp_session.is_initiator());
|
||||
|
||||
// Check lp_id is the same
|
||||
let expected_lp_id = make_lp_id(init_key.public_key(), remote_pub_key);
|
||||
assert_eq!(init_session.id(), expected_lp_id);
|
||||
assert_eq!(resp_session.id(), expected_lp_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state_machine_simplified_flow() {
|
||||
// Create test keys
|
||||
let init_key = Keypair::new();
|
||||
let resp_key = Keypair::new();
|
||||
let psk = vec![0u8; 32];
|
||||
|
||||
// Create state machines (already in ReadyToHandshake)
|
||||
let mut initiator = LpStateMachine::new(
|
||||
true, // is_initiator
|
||||
&init_key,
|
||||
resp_key.public_key(),
|
||||
&psk.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut responder = LpStateMachine::new(
|
||||
false, // is_initiator
|
||||
&resp_key,
|
||||
init_key.public_key(),
|
||||
&psk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let lp_id = initiator.id().unwrap();
|
||||
assert_eq!(lp_id, responder.id().unwrap());
|
||||
|
||||
// --- Start Handshake --- (No index exchange needed)
|
||||
println!("--- Step 1: Initiator starts handshake ---");
|
||||
let init_actions_1 = initiator.process_input(LpInput::StartHandshake);
|
||||
let init_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_1 {
|
||||
packet.clone()
|
||||
} else {
|
||||
panic!("Initiator should produce 1 action");
|
||||
};
|
||||
|
||||
assert!(
|
||||
matches!(initiator.state, LpState::Handshaking { .. }),
|
||||
"Initiator should be Handshaking"
|
||||
);
|
||||
assert_eq!(
|
||||
init_packet_1.header.session_id(),
|
||||
lp_id,
|
||||
"Packet 1 has wrong lp_id"
|
||||
);
|
||||
|
||||
println!("--- Step 2: Responder starts handshake (waits) ---");
|
||||
let resp_actions_1 = responder.process_input(LpInput::StartHandshake);
|
||||
assert!(
|
||||
resp_actions_1.is_none(),
|
||||
"Responder should produce 0 actions initially"
|
||||
);
|
||||
assert!(
|
||||
matches!(responder.state, LpState::Handshaking { .. }),
|
||||
"Responder should be Handshaking"
|
||||
);
|
||||
|
||||
// --- Handshake Message Exchange ---
|
||||
println!("--- Step 3: Responder receives packet 1, sends packet 2 ---");
|
||||
let resp_actions_2 = responder.process_input(LpInput::ReceivePacket(init_packet_1));
|
||||
let resp_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_2 {
|
||||
packet.clone()
|
||||
} else {
|
||||
panic!("Responder should send packet 2");
|
||||
};
|
||||
assert!(
|
||||
matches!(responder.state, LpState::Handshaking { .. }),
|
||||
"Responder still Handshaking"
|
||||
);
|
||||
assert_eq!(
|
||||
resp_packet_2.header.session_id(),
|
||||
lp_id,
|
||||
"Packet 2 has wrong lp_id"
|
||||
);
|
||||
|
||||
println!("--- Step 4: Initiator receives packet 2, sends packet 3 ---");
|
||||
let init_actions_2 = initiator.process_input(LpInput::ReceivePacket(resp_packet_2));
|
||||
let init_packet_3 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_2 {
|
||||
packet.clone()
|
||||
} else {
|
||||
panic!("Initiator should send packet 3");
|
||||
};
|
||||
assert!(
|
||||
matches!(initiator.state, LpState::Transport { .. }),
|
||||
"Initiator should be Transport"
|
||||
);
|
||||
assert_eq!(
|
||||
init_packet_3.header.session_id(),
|
||||
lp_id,
|
||||
"Packet 3 has wrong lp_id"
|
||||
);
|
||||
|
||||
println!("--- Step 5: Responder receives packet 3, completes handshake ---");
|
||||
let resp_actions_3 = responder.process_input(LpInput::ReceivePacket(init_packet_3));
|
||||
assert!(
|
||||
matches!(resp_actions_3, Some(Ok(LpAction::HandshakeComplete))),
|
||||
"Responder should complete handshake"
|
||||
);
|
||||
assert!(
|
||||
matches!(responder.state, LpState::Transport { .. }),
|
||||
"Responder should be Transport"
|
||||
);
|
||||
|
||||
// --- Transport Phase ---
|
||||
println!("--- Step 6: Initiator sends data ---");
|
||||
let data_to_send_1 = b"hello responder";
|
||||
let init_actions_3 = initiator.process_input(LpInput::SendData(data_to_send_1.to_vec()));
|
||||
let data_packet_1 = if let Some(Ok(LpAction::SendPacket(packet))) = init_actions_3 {
|
||||
packet.clone()
|
||||
} else {
|
||||
panic!("Initiator should send data packet");
|
||||
};
|
||||
assert_eq!(data_packet_1.header.session_id(), lp_id);
|
||||
|
||||
println!("--- Step 7: Responder receives data ---");
|
||||
let resp_actions_4 = responder.process_input(LpInput::ReceivePacket(data_packet_1));
|
||||
let resp_data_1 = if let Some(Ok(LpAction::DeliverData(data))) = resp_actions_4 {
|
||||
data
|
||||
} else {
|
||||
panic!("Responder should deliver data");
|
||||
};
|
||||
assert_eq!(resp_data_1, Bytes::copy_from_slice(data_to_send_1));
|
||||
|
||||
println!("--- Step 8: Responder sends data ---");
|
||||
let data_to_send_2 = b"hello initiator";
|
||||
let resp_actions_5 = responder.process_input(LpInput::SendData(data_to_send_2.to_vec()));
|
||||
let data_packet_2 = if let Some(Ok(LpAction::SendPacket(packet))) = resp_actions_5 {
|
||||
packet.clone()
|
||||
} else {
|
||||
panic!("Responder should send data packet");
|
||||
};
|
||||
assert_eq!(data_packet_2.header.session_id(), lp_id);
|
||||
|
||||
println!("--- Step 9: Initiator receives data ---");
|
||||
let init_actions_4 = initiator.process_input(LpInput::ReceivePacket(data_packet_2));
|
||||
if let Some(Ok(LpAction::DeliverData(data))) = init_actions_4 {
|
||||
assert_eq!(data, Bytes::copy_from_slice(data_to_send_2));
|
||||
} else {
|
||||
panic!("Initiator should deliver data");
|
||||
}
|
||||
|
||||
// --- Close ---
|
||||
println!("--- Step 10: Initiator closes ---");
|
||||
let init_actions_5 = initiator.process_input(LpInput::Close);
|
||||
assert!(matches!(
|
||||
init_actions_5,
|
||||
Some(Ok(LpAction::ConnectionClosed))
|
||||
));
|
||||
assert!(matches!(initiator.state, LpState::Closed { .. }));
|
||||
|
||||
println!("--- Step 11: Responder closes ---");
|
||||
let resp_actions_6 = responder.process_input(LpInput::Close);
|
||||
assert!(matches!(
|
||||
resp_actions_6,
|
||||
Some(Ok(LpAction::ConnectionClosed))
|
||||
));
|
||||
assert!(matches!(responder.state, LpState::Closed { .. }));
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,9 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio-util.workspace = true
|
||||
|
||||
nym-authenticator-requests = { path = "../authenticator-requests" }
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-ip-packet-requests = { path = "../ip-packet-requests" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-wireguard-types = { path = "../wireguard-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
bincode.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod lp_messages;
|
||||
|
||||
pub use lp_messages::{LpRegistrationRequest, LpRegistrationResponse, RegistrationMode};
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
use nym_authenticator_requests::AuthenticatorVersion;
|
||||
use nym_crypto::asymmetric::x25519::PublicKey;
|
||||
use nym_ip_packet_requests::IpPair;
|
||||
use nym_sphinx::addressing::{NodeIdentity, Recipient};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NymNode {
|
||||
@@ -19,11 +14,10 @@ pub struct NymNode {
|
||||
pub ip_address: IpAddr,
|
||||
pub ipr_address: Option<Recipient>,
|
||||
pub authenticator_address: Option<Recipient>,
|
||||
pub lp_address: Option<SocketAddr>,
|
||||
pub version: AuthenticatorVersion,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GatewayData {
|
||||
pub public_key: PublicKey,
|
||||
pub endpoint: SocketAddr,
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! LP (Lewes Protocol) registration message types shared between client and gateway.
|
||||
|
||||
use nym_credentials_interface::{CredentialSpendingData, TicketType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::GatewayData;
|
||||
|
||||
/// Registration request sent by client after LP handshake
|
||||
/// Aligned with existing authenticator registration flow
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LpRegistrationRequest {
|
||||
/// Client's WireGuard public key (for dVPN mode)
|
||||
pub wg_public_key: nym_wireguard_types::PeerPublicKey,
|
||||
|
||||
/// Bandwidth credential for payment
|
||||
pub credential: CredentialSpendingData,
|
||||
|
||||
/// Ticket type for bandwidth allocation
|
||||
pub ticket_type: TicketType,
|
||||
|
||||
/// Registration mode
|
||||
pub mode: RegistrationMode,
|
||||
|
||||
/// Client's IP address (for tracking/metrics)
|
||||
pub client_ip: IpAddr,
|
||||
|
||||
/// Unix timestamp for replay protection
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RegistrationMode {
|
||||
/// dVPN mode - register as WireGuard peer (most common)
|
||||
Dvpn,
|
||||
|
||||
/// Mixnet mode - register for mixnet usage (future)
|
||||
Mixnet {
|
||||
/// Client identifier for mixnet mode
|
||||
client_id: [u8; 32],
|
||||
},
|
||||
}
|
||||
|
||||
/// Registration response from gateway
|
||||
/// Contains GatewayData for compatibility with existing client code
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LpRegistrationResponse {
|
||||
/// Whether registration succeeded
|
||||
pub success: bool,
|
||||
|
||||
/// Error message if registration failed
|
||||
pub error: Option<String>,
|
||||
|
||||
/// Gateway configuration data (same as returned by authenticator)
|
||||
/// This matches what WireguardRegistrationResult expects
|
||||
pub gateway_data: Option<GatewayData>,
|
||||
|
||||
/// Allocated bandwidth in bytes
|
||||
pub allocated_bandwidth: i64,
|
||||
|
||||
/// Session identifier for future reference
|
||||
pub session_id: u32,
|
||||
}
|
||||
|
||||
impl LpRegistrationRequest {
|
||||
/// Create a new dVPN registration request
|
||||
pub fn new_dvpn(
|
||||
wg_public_key: nym_wireguard_types::PeerPublicKey,
|
||||
credential: CredentialSpendingData,
|
||||
ticket_type: TicketType,
|
||||
client_ip: IpAddr,
|
||||
) -> Self {
|
||||
Self {
|
||||
wg_public_key,
|
||||
credential,
|
||||
ticket_type,
|
||||
mode: RegistrationMode::Dvpn,
|
||||
client_ip,
|
||||
#[allow(clippy::expect_used)]
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the request timestamp is within acceptable bounds
|
||||
pub fn validate_timestamp(&self, max_skew_secs: u64) -> bool {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
(now as i64 - self.timestamp as i64).abs() <= max_skew_secs as i64
|
||||
}
|
||||
}
|
||||
|
||||
impl LpRegistrationResponse {
|
||||
/// Create a success response with GatewayData
|
||||
pub fn success(session_id: u32, allocated_bandwidth: i64, gateway_data: GatewayData) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
error: None,
|
||||
gateway_data: Some(gateway_data),
|
||||
allocated_bandwidth,
|
||||
session_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an error response
|
||||
pub fn error(session_id: u32, error: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error: Some(error),
|
||||
gateway_data: None,
|
||||
allocated_bandwidth: 0,
|
||||
session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
// ==================== Helper Functions ====================
|
||||
|
||||
fn create_test_gateway_data() -> GatewayData {
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
GatewayData {
|
||||
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(
|
||||
nym_sphinx::PublicKey::from([1u8; 32]),
|
||||
),
|
||||
private_ipv4: Ipv4Addr::new(10, 0, 0, 1),
|
||||
private_ipv6: Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1),
|
||||
endpoint: "192.168.1.1:8080".parse().expect("Valid test endpoint"),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== LpRegistrationRequest Tests ====================
|
||||
|
||||
// ==================== LpRegistrationResponse Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_success() {
|
||||
let gateway_data = create_test_gateway_data();
|
||||
let session_id = 12345;
|
||||
let allocated_bandwidth = 1_000_000_000;
|
||||
|
||||
let response =
|
||||
LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data.clone());
|
||||
|
||||
assert!(response.success);
|
||||
assert!(response.error.is_none());
|
||||
assert!(response.gateway_data.is_some());
|
||||
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
|
||||
assert_eq!(response.session_id, session_id);
|
||||
|
||||
let returned_gw_data = response
|
||||
.gateway_data
|
||||
.expect("Gateway data should be present in success response");
|
||||
assert_eq!(returned_gw_data.public_key, gateway_data.public_key);
|
||||
assert_eq!(returned_gw_data.private_ipv4, gateway_data.private_ipv4);
|
||||
assert_eq!(returned_gw_data.private_ipv6, gateway_data.private_ipv6);
|
||||
assert_eq!(returned_gw_data.endpoint, gateway_data.endpoint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_error() {
|
||||
let session_id = 54321;
|
||||
let error_msg = String::from("Insufficient bandwidth");
|
||||
|
||||
let response = LpRegistrationResponse::error(session_id, error_msg.clone());
|
||||
|
||||
assert!(!response.success);
|
||||
assert_eq!(response.error, Some(error_msg));
|
||||
assert!(response.gateway_data.is_none());
|
||||
assert_eq!(response.allocated_bandwidth, 0);
|
||||
assert_eq!(response.session_id, session_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_serialize_deserialize_success() {
|
||||
let gateway_data = create_test_gateway_data();
|
||||
let original = LpRegistrationResponse::success(999, 5_000_000_000, gateway_data);
|
||||
|
||||
// Serialize
|
||||
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
|
||||
|
||||
// Deserialize
|
||||
let deserialized: LpRegistrationResponse =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize response");
|
||||
|
||||
assert_eq!(deserialized.success, original.success);
|
||||
assert_eq!(deserialized.error, original.error);
|
||||
assert_eq!(
|
||||
deserialized.allocated_bandwidth,
|
||||
original.allocated_bandwidth
|
||||
);
|
||||
assert_eq!(deserialized.session_id, original.session_id);
|
||||
assert!(deserialized.gateway_data.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_serialize_deserialize_error() {
|
||||
let original = LpRegistrationResponse::error(777, String::from("Test error message"));
|
||||
|
||||
// Serialize
|
||||
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
|
||||
|
||||
// Deserialize
|
||||
let deserialized: LpRegistrationResponse =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize response");
|
||||
|
||||
assert_eq!(deserialized.success, original.success);
|
||||
assert_eq!(deserialized.error, original.error);
|
||||
assert_eq!(deserialized.allocated_bandwidth, 0);
|
||||
assert_eq!(deserialized.session_id, original.session_id);
|
||||
assert!(deserialized.gateway_data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_malformed_deserialize() {
|
||||
// Create invalid bincode data
|
||||
let invalid_data = vec![0xFF; 100];
|
||||
|
||||
// Attempt to deserialize
|
||||
let result: Result<LpRegistrationResponse, _> = bincode::deserialize(&invalid_data);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected deserialization to fail for malformed data"
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== RegistrationMode Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_registration_mode_serialize_dvpn() {
|
||||
let mode = RegistrationMode::Dvpn;
|
||||
|
||||
let serialized = bincode::serialize(&mode).expect("Failed to serialize mode");
|
||||
let deserialized: RegistrationMode =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize mode");
|
||||
|
||||
assert!(matches!(deserialized, RegistrationMode::Dvpn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_mode_serialize_mixnet() {
|
||||
let client_id = [99u8; 32];
|
||||
let mode = RegistrationMode::Mixnet { client_id };
|
||||
|
||||
let serialized = bincode::serialize(&mode).expect("Failed to serialize mode");
|
||||
let deserialized: RegistrationMode =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize mode");
|
||||
|
||||
match deserialized {
|
||||
RegistrationMode::Mixnet { client_id: id } => {
|
||||
assert_eq!(id, client_id);
|
||||
}
|
||||
_ => panic!("Expected Mixnet mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@ pub use crate::runtime_registry::RegistryAccessError;
|
||||
|
||||
/// Get or create a ShutdownTracker for SDK use.
|
||||
/// This provides automatic task management without requiring manual setup.
|
||||
pub fn get_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
|
||||
Ok(runtime_registry::RuntimeRegistry::get_or_create_sdk()?.shutdown_tracker_owned())
|
||||
pub fn create_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
|
||||
Ok(runtime_registry::RuntimeRegistry::create_sdk()?.shutdown_tracker_owned())
|
||||
}
|
||||
|
||||
@@ -19,30 +19,45 @@ pub(crate) struct RuntimeRegistry {
|
||||
pub enum RegistryAccessError {
|
||||
#[error("the runtime registry is poisoned")]
|
||||
Poisoned,
|
||||
|
||||
#[error("The SDK ShutdownManager already exists")]
|
||||
ExistingShutdownManager,
|
||||
|
||||
#[error("No existing SDK ShutdownManager")]
|
||||
MissingShutdownManager,
|
||||
}
|
||||
|
||||
impl RuntimeRegistry {
|
||||
/// Get or create a ShutdownManager for SDK use.
|
||||
/// Create a ShutdownManager for SDK use.
|
||||
/// This manager doesn't listen to OS signals, making it suitable for library use.
|
||||
pub(crate) fn get_or_create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
|
||||
/// This function overwrite any existing manager!
|
||||
pub(crate) fn create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
|
||||
let mut guard = REGISTRY
|
||||
.sdk_manager
|
||||
.write()
|
||||
.map_err(|_| RegistryAccessError::Poisoned)?;
|
||||
|
||||
Ok(guard
|
||||
.insert(Arc::new(
|
||||
ShutdownManager::new_without_signals().with_cancel_on_panic(),
|
||||
))
|
||||
.clone())
|
||||
}
|
||||
|
||||
/// Get the ShutdownManager for SDK use.
|
||||
/// This manager doesn't listen to OS signals, making it suitable for library use.
|
||||
/// Not yet used, but maybe in the future
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
|
||||
let guard = REGISTRY
|
||||
.sdk_manager
|
||||
.read()
|
||||
.map_err(|_| RegistryAccessError::Poisoned)?;
|
||||
if let Some(manager) = guard.as_ref() {
|
||||
return Ok(manager.clone());
|
||||
Ok(manager.clone())
|
||||
} else {
|
||||
Err(RegistryAccessError::MissingShutdownManager)
|
||||
}
|
||||
drop(guard);
|
||||
|
||||
let mut guard = REGISTRY
|
||||
.sdk_manager
|
||||
.write()
|
||||
.map_err(|_| RegistryAccessError::Poisoned)?;
|
||||
Ok(guard
|
||||
.get_or_insert_with(|| {
|
||||
Arc::new(ShutdownManager::new_without_signals().with_cancel_on_panic())
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
|
||||
/// Check if an SDK manager has been created.
|
||||
@@ -85,10 +100,13 @@ mod tests {
|
||||
|
||||
assert!(!RuntimeRegistry::has_sdk_manager().unwrap());
|
||||
|
||||
let manager1 = RuntimeRegistry::get_or_create_sdk().unwrap();
|
||||
// Error if nothing was created
|
||||
assert!(RuntimeRegistry::get_sdk().is_err());
|
||||
|
||||
let manager1 = RuntimeRegistry::create_sdk().unwrap();
|
||||
assert!(RuntimeRegistry::has_sdk_manager().unwrap());
|
||||
|
||||
let manager2 = RuntimeRegistry::get_or_create_sdk().unwrap();
|
||||
let manager2 = RuntimeRegistry::get_sdk().unwrap();
|
||||
// Should return the same instance
|
||||
assert!(Arc::ptr_eq(&manager1, &manager2));
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ jwt-simple = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde"] }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
nym-http-api-client = { path = "../http-api-client", default-features = false }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] }
|
||||
@@ -25,6 +26,12 @@ nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
time = { workspace = true, features = ["macros"] }
|
||||
nym-test-utils = { path = "../test-utils" }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
|
||||
|
||||
[features]
|
||||
openapi = ["utoipa"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::UpgradeModeCheckError;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_http_api_client::generate_user_agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct UpgradeModeAttestation {
|
||||
#[serde(flatten)]
|
||||
pub content: UpgradeModeAttestationContent,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_signature")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub signature: ed25519::Signature,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
impl UpgradeModeAttestation {
|
||||
pub fn authorised_to_issue_jwt(&self, key: &ed25519::PublicKey) -> bool {
|
||||
self.content.authorised_jwt_issuers.contains(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename = "upgrade_mode")]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct UpgradeModeAttestationContent {
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub starting_time: OffsetDateTime,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub attester_public_key: ed25519::PublicKey,
|
||||
|
||||
#[serde(with = "ed25519::vec_bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
|
||||
pub authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
}
|
||||
|
||||
impl UpgradeModeAttestation {
|
||||
@@ -45,17 +57,26 @@ impl UpgradeModeAttestationContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation {
|
||||
generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc())
|
||||
pub fn generate_new_attestation(
|
||||
key: &ed25519::PrivateKey,
|
||||
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
) -> UpgradeModeAttestation {
|
||||
generate_new_attestation_with_starting_time(
|
||||
key,
|
||||
authorised_jwt_issuers,
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation_with_starting_time(
|
||||
key: &ed25519::PrivateKey,
|
||||
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
starting_time: OffsetDateTime,
|
||||
) -> UpgradeModeAttestation {
|
||||
let content = UpgradeModeAttestationContent {
|
||||
starting_time,
|
||||
attester_public_key: key.into(),
|
||||
authorised_jwt_issuers,
|
||||
};
|
||||
UpgradeModeAttestation {
|
||||
signature: key.sign(content.as_json()),
|
||||
@@ -63,17 +84,19 @@ pub fn generate_new_attestation_with_starting_time(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn attempt_retrieve(
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn attempt_retrieve_attestation(
|
||||
url: &str,
|
||||
) -> Result<Option<UpgradeModeAttestation>, UpgradeModeCheckError> {
|
||||
let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure {
|
||||
user_agent: Option<nym_http_api_client::UserAgent>,
|
||||
) -> Result<Option<UpgradeModeAttestation>, crate::UpgradeModeCheckError> {
|
||||
let retrieval_failure = |source| crate::UpgradeModeCheckError::AttestationRetrievalFailure {
|
||||
url: url.to_string(),
|
||||
source,
|
||||
};
|
||||
|
||||
let attestation = reqwest::ClientBuilder::new()
|
||||
.user_agent(generate_user_agent!())
|
||||
.timeout(Duration::from_secs(5))
|
||||
.user_agent(user_agent.unwrap_or_else(|| nym_http_api_client::generate_user_agent!()))
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(retrieval_failure)?
|
||||
.get(url)
|
||||
@@ -101,13 +124,20 @@ mod tests {
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
])?;
|
||||
|
||||
let attestation = generate_new_attestation_with_starting_time(&key, starting_time);
|
||||
let authorised_jwt_issuers = vec![ed25519::PublicKey::from_base58_string(
|
||||
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
|
||||
)?];
|
||||
let attestation = generate_new_attestation_with_starting_time(
|
||||
&key,
|
||||
authorised_jwt_issuers,
|
||||
starting_time,
|
||||
);
|
||||
|
||||
let attestation_json = serde_json::to_string(&attestation)?;
|
||||
let attestation_content_json = attestation.content.as_json();
|
||||
|
||||
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#;
|
||||
let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#;
|
||||
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"],"signature":"5Kt9dfwvnkdnDcENbwNyitrxghyckWUYycBv8jUUn7hJUMohWEMc6otb3scXQfCrAGSE7FD5m7kr6auBmkAmfczY"}"#;
|
||||
let expected_content = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]}"#;
|
||||
|
||||
assert_eq!(attestation_content_json, expected_content);
|
||||
assert_eq!(attestation_json, expected_attestation);
|
||||
|
||||
@@ -12,6 +12,9 @@ pub enum UpgradeModeCheckError {
|
||||
#[error("the jwt metadata didn't contain explicit public key")]
|
||||
MissingTokenPublicKey,
|
||||
|
||||
#[error("the jwt signer does not appear in the authorised attestation set")]
|
||||
UnauthorisedIssuer,
|
||||
|
||||
#[error("the attached public key was not valid ed25519 public key")]
|
||||
MalformedEd25519PublicKey { source: Ed25519RecoveryError },
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ pub fn validate_upgrade_mode_jwt(
|
||||
.map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })?
|
||||
.custom;
|
||||
|
||||
// jwt itself is cryptographically valid,
|
||||
// but let's see if this entity has been permitted to issue the token in the first place
|
||||
if !attestation.authorised_to_issue_jwt(&ed25519_pub_key) {
|
||||
return Err(UpgradeModeCheckError::UnauthorisedIssuer);
|
||||
}
|
||||
|
||||
Ok(attestation)
|
||||
}
|
||||
|
||||
@@ -73,6 +79,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::generate_new_attestation;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
|
||||
#[test]
|
||||
fn generate_and_validate_jwt() {
|
||||
@@ -86,15 +93,25 @@ mod tests {
|
||||
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
|
||||
])
|
||||
.unwrap();
|
||||
let keys = ed25519::KeyPair::from(jwt_key);
|
||||
let jwt_keys = ed25519::KeyPair::from(jwt_key);
|
||||
|
||||
let attestation = generate_new_attestation(&attestation_key);
|
||||
let mut rng = deterministic_rng();
|
||||
let unauthorised_jwt_keys = ed25519::KeyPair::new(&mut rng);
|
||||
|
||||
let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]);
|
||||
let jwt_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
attestation.clone(),
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
&jwt_keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
let unauthorised_jwt = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation.clone(),
|
||||
Duration::from_secs(60 * 60),
|
||||
&unauthorised_jwt_keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
|
||||
|
||||
@@ -104,10 +121,15 @@ mod tests {
|
||||
// we expect another-issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err());
|
||||
|
||||
// the key is not in the authorised set inside the attestation
|
||||
assert!(
|
||||
validate_upgrade_mode_jwt(&unauthorised_jwt, Some("nym-credential-proxy")).is_err()
|
||||
);
|
||||
|
||||
let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
&jwt_keys,
|
||||
None,
|
||||
);
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
|
||||
@@ -6,8 +6,10 @@ pub(crate) mod error;
|
||||
pub(crate) mod jwt;
|
||||
|
||||
pub use attestation::{
|
||||
UpgradeModeAttestation, attempt_retrieve, generate_new_attestation,
|
||||
generate_new_attestation_with_starting_time,
|
||||
UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time,
|
||||
};
|
||||
pub use error::UpgradeModeCheckError;
|
||||
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use attestation::attempt_retrieve_attestation;
|
||||
|
||||
@@ -37,7 +37,6 @@ nym-credential-verification = { path = "../credential-verification" }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
|
||||
nym-gateway-storage = { path = "../gateway-storage" }
|
||||
nym-gateway-requests = { path = "../gateway-requests" }
|
||||
nym-metrics = { path = "../nym-metrics" }
|
||||
nym-network-defaults = { path = "../network-defaults" }
|
||||
nym-task = { path = "../task" }
|
||||
nym-wireguard-types = { path = "../wireguard-types" }
|
||||
|
||||
@@ -11,6 +11,7 @@ use defguard_wireguard_rs::{WGApi, WireguardInterfaceApi, host::Peer, key::Key,
|
||||
use nym_credential_verification::ecash::EcashManager;
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_wireguard_types::Config;
|
||||
use peer_controller::PeerControlRequest;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
@@ -22,8 +23,6 @@ pub mod peer_controller;
|
||||
pub mod peer_handle;
|
||||
pub mod peer_storage_manager;
|
||||
|
||||
pub use peer_controller::PeerControlRequest;
|
||||
|
||||
pub const CONTROL_CHANNEL_SIZE: usize = 256;
|
||||
|
||||
pub struct WgApiWrapper {
|
||||
|
||||
@@ -138,8 +138,6 @@ impl PeerController {
|
||||
|
||||
// Function that should be used for peer removal, to handle both storage and kernel interaction
|
||||
pub async fn remove_peer(&mut self, key: &Key) -> Result<()> {
|
||||
nym_metrics::inc!("wg_peer_removal_attempts");
|
||||
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.remove_wireguard_peer(&key.to_string())
|
||||
@@ -147,12 +145,9 @@ impl PeerController {
|
||||
self.bw_storage_managers.remove(key);
|
||||
let ret = self.wg_api.remove_peer(key);
|
||||
if ret.is_err() {
|
||||
nym_metrics::inc!("wg_peer_removal_failed");
|
||||
log::error!(
|
||||
"Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset."
|
||||
);
|
||||
} else {
|
||||
nym_metrics::inc!("wg_peer_removal_success");
|
||||
}
|
||||
Ok(ret?)
|
||||
}
|
||||
@@ -182,15 +177,7 @@ impl PeerController {
|
||||
}
|
||||
|
||||
async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> {
|
||||
nym_metrics::inc!("wg_peer_addition_attempts");
|
||||
|
||||
// Try to configure WireGuard peer
|
||||
if let Err(e) = self.wg_api.configure_peer(peer) {
|
||||
nym_metrics::inc!("wg_peer_addition_failed");
|
||||
nym_metrics::inc!("wg_config_errors_total");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
self.wg_api.configure_peer(peer)?;
|
||||
let bandwidth_storage_manager = SharedBandwidthStorageManager::new(
|
||||
Arc::new(RwLock::new(
|
||||
Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key)
|
||||
@@ -218,8 +205,6 @@ impl PeerController {
|
||||
handle.run().await;
|
||||
log::debug!("Peer handle shut down for {public_key}");
|
||||
});
|
||||
|
||||
nym_metrics::inc!("wg_peer_addition_success");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Single-stage Dockerfile for Nym localnet
|
||||
# Builds: nym-node, nym-network-requester, nym-socks5-client
|
||||
# Target: Apple Container Runtime with host networking
|
||||
|
||||
FROM rust:latest
|
||||
|
||||
WORKDIR /usr/src/nym
|
||||
COPY ./ ./
|
||||
|
||||
ENV CARGO_BUILD_JOBS=8
|
||||
|
||||
# Build all required binaries in release mode
|
||||
RUN cargo build --release --locked \
|
||||
-p nym-node \
|
||||
-p nym-network-requester \
|
||||
-p nym-socks5-client
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt update && apt install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
netcat-openbsd \
|
||||
jq \
|
||||
iproute2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies for build_topology.py
|
||||
RUN pip3 install --break-system-packages base58
|
||||
|
||||
# Move binaries to /usr/local/bin for easy access
|
||||
RUN cp target/release/nym-node /usr/local/bin/ && \
|
||||
cp target/release/nym-network-requester /usr/local/bin/ && \
|
||||
cp target/release/nym-socks5-client /usr/local/bin/
|
||||
|
||||
# Copy supporting scripts
|
||||
COPY ./docker/localnet/build_topology.py /usr/local/bin/
|
||||
|
||||
WORKDIR /nym
|
||||
|
||||
# Default command
|
||||
CMD ["nym-node", "--help"]
|
||||
@@ -1,608 +0,0 @@
|
||||
# Nym Localnet for Kata Container Runtimes
|
||||
|
||||
A complete Nym mixnet test environment running on Apple's container runtime for macOS (for now).
|
||||
|
||||
## Overview
|
||||
|
||||
This localnet setup provides a fully functional Nym mixnet for local development and testing:
|
||||
- **3 mixnodes** (layer 1, 2, 3)
|
||||
- **1 gateway** (entry + exit mode)
|
||||
- **1 network-requester** (service provider)
|
||||
- **1 SOCKS5 client**
|
||||
|
||||
All components run in isolated containers with proper networking and dynamic IP resolution.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
- **macOS** (tested on macOS Sequoia 15.0+)
|
||||
- **Apple Container Runtime** - Built into macOS
|
||||
- **Docker Desktop** (for building images only)
|
||||
- **Python 3** with `base58` library
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Install Python dependencies
|
||||
pip3 install --break-system-packages base58
|
||||
|
||||
# Verify container runtime is available
|
||||
container --version
|
||||
|
||||
# Verify Docker is installed (for building)
|
||||
docker --version
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Navigate to the localnet directory
|
||||
cd docker/localnet
|
||||
|
||||
# Build the container image
|
||||
./localnet.sh build
|
||||
|
||||
# Start the localnet
|
||||
./localnet.sh start
|
||||
|
||||
# Test the SOCKS5 proxy
|
||||
curl -L --socks5 localhost:1080 https://nymtech.net
|
||||
|
||||
# View logs
|
||||
./localnet.sh logs gateway
|
||||
./localnet.sh logs socks5
|
||||
|
||||
# Stop the localnet
|
||||
./localnet.sh stop
|
||||
|
||||
# Clean up everything
|
||||
./localnet.sh clean
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Container Network
|
||||
|
||||
All containers run on a custom bridge network (`nym-localnet-network`) with dynamic IP assignment:
|
||||
|
||||
```
|
||||
Host Machine (macOS)
|
||||
├── nym-localnet-network (bridge)
|
||||
│ ├── nym-mixnode1 (192.168.66.3)
|
||||
│ ├── nym-mixnode2 (192.168.66.4)
|
||||
│ ├── nym-mixnode3 (192.168.66.5)
|
||||
│ ├── nym-gateway (192.168.66.6)
|
||||
│ ├── nym-network-requester (192.168.66.7)
|
||||
│ └── nym-socks5-client (192.168.66.8)
|
||||
```
|
||||
|
||||
Ports published to host:
|
||||
- 1080 → SOCKS5 proxy
|
||||
- 9000 → Gateway entry
|
||||
- 10001-10004 → Mixnet ports
|
||||
- 20001-20004 → Verloc ports
|
||||
- 30001-30004 → HTTP APIs
|
||||
|
||||
### Startup Flow
|
||||
|
||||
1. **Container Initialization** (parallel)
|
||||
- Each container starts and gets a dynamic IP
|
||||
- Each node runs `nym-node run --init-only` with its container IP
|
||||
- Bonding JSON files are written to shared volume
|
||||
|
||||
2. **Topology Generation** (sequential)
|
||||
- Wait for all 4 bonding JSON files
|
||||
- Get container IPs dynamically
|
||||
- Run `build_topology.py` with container IPs
|
||||
- Generate `network.json` with correct addresses
|
||||
|
||||
3. **Node Startup** (parallel)
|
||||
- Each container starts its node with `--local` flag
|
||||
- Nodes read configuration from init phase
|
||||
- Clients use custom topology file
|
||||
|
||||
4. **Service Providers** (sequential)
|
||||
- Network requester initializes and starts
|
||||
- SOCKS5 client initializes with requester address
|
||||
|
||||
### Network Topology
|
||||
|
||||
The `network.json` file contains the complete network topology:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"key_rotation_id": 0,
|
||||
"absolute_epoch_id": 0,
|
||||
"refreshed_at": "2025-11-03T..."
|
||||
},
|
||||
"rewarded_set": {
|
||||
"epoch_id": 0,
|
||||
"entry_gateways": [4],
|
||||
"exit_gateways": [4],
|
||||
"layer1": [1],
|
||||
"layer2": [2],
|
||||
"layer3": [3],
|
||||
"standby": []
|
||||
},
|
||||
"node_details": {
|
||||
"1": { "mix_host": "192.168.66.3:10001", ... },
|
||||
"2": { "mix_host": "192.168.66.4:10002", ... },
|
||||
"3": { "mix_host": "192.168.66.5:10003", ... },
|
||||
"4": { "mix_host": "192.168.66.6:10004", ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Build
|
||||
```bash
|
||||
./localnet.sh build
|
||||
```
|
||||
Builds the Docker image and loads it into Apple container runtime.
|
||||
|
||||
**Note**: First build takes ~5-10 minutes to compile all components.
|
||||
|
||||
### Start
|
||||
```bash
|
||||
./localnet.sh start
|
||||
```
|
||||
Starts all containers, generates topology, and launches the complete network.
|
||||
|
||||
**Expected output**:
|
||||
```
|
||||
[INFO] Starting Nym Localnet...
|
||||
[SUCCESS] Network created: nym-localnet-network
|
||||
[INFO] Starting nym-mixnode1...
|
||||
[SUCCESS] nym-mixnode1 started
|
||||
...
|
||||
[INFO] Building network topology with container IPs...
|
||||
[SUCCESS] Network topology created successfully
|
||||
[SUCCESS] Nym Localnet is running!
|
||||
|
||||
Test with:
|
||||
curl -x socks5h://127.0.0.1:1080 https://nymtech.net
|
||||
```
|
||||
|
||||
### Stop
|
||||
```bash
|
||||
./localnet.sh stop
|
||||
```
|
||||
Stops and removes all running containers.
|
||||
|
||||
### Clean
|
||||
```bash
|
||||
./localnet.sh clean
|
||||
```
|
||||
Complete cleanup: removes containers, volumes, network, and temporary files.
|
||||
|
||||
### Logs
|
||||
```bash
|
||||
# View logs for a specific container
|
||||
./localnet.sh logs <container-name>
|
||||
|
||||
# Container names:
|
||||
# - mix1, mix2, mix3
|
||||
# - gateway
|
||||
# - requester
|
||||
# - socks5
|
||||
|
||||
# Examples:
|
||||
./localnet.sh logs gateway
|
||||
./localnet.sh logs socks5
|
||||
container logs nym-gateway --follow
|
||||
```
|
||||
|
||||
### Status
|
||||
```bash
|
||||
# List all containers
|
||||
container list
|
||||
|
||||
# Check specific container
|
||||
container logs nym-gateway
|
||||
|
||||
# Inspect network
|
||||
container network inspect nym-localnet-network
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Basic SOCKS5 Test
|
||||
```bash
|
||||
# Simple HTTP request with redirect following
|
||||
curl -L --socks5 localhost:1080 http://example.com
|
||||
|
||||
# HTTPS request
|
||||
curl -L --socks5 localhost:1080 https://nymtech.net
|
||||
|
||||
# Download a file
|
||||
curl -L --socks5 localhost:1080 \
|
||||
https://test-download-files-nym.s3.amazonaws.com/download-files/1MB.zip \
|
||||
--output /tmp/test.zip
|
||||
```
|
||||
|
||||
### Verify Network Topology
|
||||
```bash
|
||||
# View the generated topology
|
||||
container exec nym-gateway cat /localnet/network.json | jq .
|
||||
|
||||
# Check container IPs
|
||||
container list | grep nym-
|
||||
|
||||
# Verify all bonding files exist
|
||||
container exec nym-gateway ls -la /localnet/
|
||||
```
|
||||
|
||||
### Test Mixnet Routing
|
||||
```bash
|
||||
# All traffic flows through: client → mix1 → mix2 → mix3 → gateway → internet
|
||||
# Watch logs to verify routing:
|
||||
container logs nym-mixnode1 --follow &
|
||||
container logs nym-mixnode2 --follow &
|
||||
container logs nym-mixnode3 --follow &
|
||||
container logs nym-gateway --follow &
|
||||
|
||||
# Make a request
|
||||
curl -L --socks5 localhost:1080 https://nymtech.com
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
docker/localnet/
|
||||
├── README.md # This file
|
||||
├── localnet.sh # Main orchestration script
|
||||
├── Dockerfile.localnet # Docker image definition
|
||||
└── build_topology.py # Topology generator
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Node Initialization
|
||||
|
||||
Each node initializes itself at runtime inside its container:
|
||||
|
||||
```bash
|
||||
# Get container IP
|
||||
CONTAINER_IP=$(hostname -i)
|
||||
|
||||
# Initialize with container IP
|
||||
nym-node run --id mix1-localnet --init-only \
|
||||
--unsafe-disable-replay-protection \
|
||||
--local \
|
||||
--mixnet-bind-address=0.0.0.0:10001 \
|
||||
--verloc-bind-address=0.0.0.0:20001 \
|
||||
--http-bind-address=0.0.0.0:30001 \
|
||||
--http-access-token=lala \
|
||||
--public-ips $CONTAINER_IP \
|
||||
--output=json \
|
||||
--bonding-information-output="/localnet/mix1.json"
|
||||
```
|
||||
|
||||
**Key flags**:
|
||||
- `--local`: Accept private IPs for local development
|
||||
- `--public-ips`: Announce the container's IP address
|
||||
- `--unsafe-disable-replay-protection`: Disable bloomfilter to save memory
|
||||
|
||||
### Dynamic Topology
|
||||
|
||||
The topology is built **after** containers start:
|
||||
|
||||
```bash
|
||||
# Get container IPs
|
||||
MIX1_IP=$(container exec nym-mixnode1 hostname -i)
|
||||
MIX2_IP=$(container exec nym-mixnode2 hostname -i)
|
||||
MIX3_IP=$(container exec nym-mixnode3 hostname -i)
|
||||
GATEWAY_IP=$(container exec nym-gateway hostname -i)
|
||||
|
||||
# Build topology with actual IPs
|
||||
python3 build_topology.py /localnet localnet \
|
||||
$MIX1_IP $MIX2_IP $MIX3_IP $GATEWAY_IP
|
||||
```
|
||||
|
||||
This ensures the topology contains reachable container addresses.
|
||||
|
||||
### Client Configuration
|
||||
|
||||
Clients use `--custom-mixnet` to read the local topology:
|
||||
|
||||
```bash
|
||||
# Network requester
|
||||
nym-network-requester init \
|
||||
--id "network-requester-$SUFFIX" \
|
||||
--open-proxy=true \
|
||||
--custom-mixnet /localnet/network.json
|
||||
|
||||
# SOCKS5 client
|
||||
nym-socks5-client init \
|
||||
--id "socks5-client-$SUFFIX" \
|
||||
--provider "$REQUESTER_ADDRESS" \
|
||||
--custom-mixnet /localnet/network.json \
|
||||
--host 0.0.0.0
|
||||
```
|
||||
|
||||
The `--custom-mixnet` flag tells clients to use our local topology instead of fetching from nym-api.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container Build Issues
|
||||
|
||||
**Problem**: Docker build fails
|
||||
```bash
|
||||
# Check Docker is running
|
||||
docker info
|
||||
|
||||
# Clean Docker cache
|
||||
docker system prune -a
|
||||
|
||||
# Rebuild with no cache
|
||||
./localnet.sh build
|
||||
```
|
||||
|
||||
**Problem**: Container image load fails
|
||||
```bash
|
||||
# Verify temp file was created
|
||||
ls -lh /tmp/nym-localnet-image-*
|
||||
|
||||
# Check container runtime
|
||||
container image list
|
||||
|
||||
# Manually load if needed
|
||||
docker save -o /tmp/nym-image.tar nym-localnet:latest
|
||||
container image load --input /tmp/nym-image.tar
|
||||
```
|
||||
|
||||
### Network Issues
|
||||
|
||||
**Problem**: Containers can't communicate
|
||||
```bash
|
||||
# Check network exists
|
||||
container network list | grep nym-localnet
|
||||
|
||||
# Inspect network
|
||||
container network inspect nym-localnet-network
|
||||
|
||||
# Verify containers are on the network
|
||||
container list | grep nym-
|
||||
```
|
||||
|
||||
**Problem**: SOCKS5 connection refused
|
||||
```bash
|
||||
# Check SOCKS5 is listening
|
||||
container logs nym-socks5-client | grep "Listening on"
|
||||
|
||||
# Verify port mapping
|
||||
container list | grep socks5
|
||||
|
||||
# Test from host
|
||||
nc -zv localhost 1080
|
||||
```
|
||||
|
||||
### Node Issues
|
||||
|
||||
**Problem**: "No valid public addresses" error
|
||||
- Ensure `--local` flag is present in both init and run commands
|
||||
- Check container can resolve its own IP: `container exec nym-mixnode1 hostname -i`
|
||||
- Verify `--public-ips` is using `$CONTAINER_IP` variable
|
||||
|
||||
**Problem**: "TUN device error"
|
||||
- The gateway needs TUN device support for exit functionality
|
||||
- Verify `iproute2` is installed in the image (adds `ip` command)
|
||||
- Check gateway logs: `container logs nym-gateway`
|
||||
- The gateway should show: "Created TUN device: nymtun0"
|
||||
|
||||
**Problem**: "Noise handshake" warnings
|
||||
- These are warnings, not errors - nodes fall back to TCP
|
||||
- Does not affect functionality in local development
|
||||
- Safe to ignore for testing purposes
|
||||
|
||||
### Topology Issues
|
||||
|
||||
**Problem**: Network.json not created
|
||||
```bash
|
||||
# Check all bonding files exist
|
||||
container exec nym-gateway ls -la /localnet/
|
||||
|
||||
# Verify build_topology.py ran
|
||||
container logs nym-gateway | grep "Building network topology"
|
||||
|
||||
# Check Python dependencies
|
||||
container exec nym-gateway python3 -c "import base58"
|
||||
```
|
||||
|
||||
**Problem**: Clients can't connect to nodes
|
||||
```bash
|
||||
# Verify IPs in topology match container IPs
|
||||
container exec nym-gateway cat /localnet/network.json | jq '.node_details'
|
||||
container list | grep nym-
|
||||
|
||||
# Check containers can reach each other
|
||||
container exec nym-socks5-client ping -c 1 192.168.66.6
|
||||
```
|
||||
|
||||
### Startup Issues
|
||||
|
||||
**Problem**: Containers exit immediately
|
||||
```bash
|
||||
# Check logs for errors
|
||||
container logs nym-mixnode1
|
||||
|
||||
# Common issues:
|
||||
# - Missing network.json: Wait for topology to be built
|
||||
# - Port already in use: Check for conflicting services
|
||||
# - Init failed: Check for correct container IP
|
||||
```
|
||||
|
||||
**Problem**: Topology build times out
|
||||
```bash
|
||||
# Verify all containers initialized
|
||||
container exec nym-gateway ls -la /localnet/*.json
|
||||
|
||||
# Check for init errors
|
||||
container logs nym-mixnode1 | grep -i error
|
||||
|
||||
# Manual cleanup and restart
|
||||
./localnet.sh clean
|
||||
./localnet.sh start
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
### Memory Usage
|
||||
- Each mixnode: ~200MB
|
||||
- Gateway: ~300MB (includes TUN device)
|
||||
- Network requester: ~150MB
|
||||
- SOCKS5 client: ~150MB
|
||||
- **Total**: ~1.2GB + overhead
|
||||
|
||||
**Recommended**: 4GB+ system memory
|
||||
|
||||
### Startup Time
|
||||
- Image build: ~5-10 minutes (first time)
|
||||
- Network start: ~20-30 seconds
|
||||
- Node initialization: ~5-10 seconds per node (parallel)
|
||||
|
||||
### Latency
|
||||
Mixnet adds latency by design for privacy:
|
||||
- ~1-3 seconds for SOCKS5 requests
|
||||
- Cover traffic adds random delays
|
||||
- Local testing may show variable timing
|
||||
|
||||
This is **expected behavior** - the mixnet provides privacy through traffic mixing.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Node Configuration
|
||||
|
||||
Edit node init commands in `localnet.sh` (search for `nym-node run --init-only`):
|
||||
|
||||
```bash
|
||||
# Example: Change mixnode ports
|
||||
--mixnet-bind-address=0.0.0.0:11001 \
|
||||
--verloc-bind-address=0.0.0.0:21001 \
|
||||
--http-bind-address=0.0.0.0:31001 \
|
||||
```
|
||||
|
||||
Remember to update port mappings in the `container run` command as well.
|
||||
|
||||
### Enable Replay Protection
|
||||
|
||||
Remove `--unsafe-disable-replay-protection` flags (requires more memory):
|
||||
|
||||
```bash
|
||||
# In start_mixnode() and start_gateway() functions
|
||||
nym-node run --id mix1-localnet --init-only \
|
||||
--local \
|
||||
--mixnet-bind-address=0.0.0.0:10001 \
|
||||
# ... other flags (without --unsafe-disable-replay-protection)
|
||||
```
|
||||
|
||||
**Note**: Each node will require an additional ~1.5GB memory for bloomfilter.
|
||||
|
||||
### API Access
|
||||
|
||||
Each node exposes an HTTP API:
|
||||
|
||||
```bash
|
||||
# Get gateway info
|
||||
curl -H "Authorization: Bearer lala" http://localhost:30004/api/v1/gateway
|
||||
|
||||
# Get mixnode stats
|
||||
curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/stats
|
||||
|
||||
# Get node description
|
||||
curl -H "Authorization: Bearer lala" http://localhost:30001/api/v1/description
|
||||
```
|
||||
|
||||
Access token is `lala` (configured with `--http-access-token=lala`).
|
||||
|
||||
### Add More Mixnodes
|
||||
|
||||
To add a 4th mixnode:
|
||||
|
||||
1. **Update constants** in `localnet.sh`:
|
||||
```bash
|
||||
MIXNODE4_CONTAINER="nym-mixnode4"
|
||||
```
|
||||
|
||||
2. **Add start call** in `start_all()`:
|
||||
```bash
|
||||
start_mixnode 4 "$MIXNODE4_CONTAINER"
|
||||
```
|
||||
|
||||
3. **Update topology builder** to include the new node
|
||||
|
||||
4. **Rebuild and restart**:
|
||||
```bash
|
||||
./localnet.sh clean
|
||||
./localnet.sh build
|
||||
./localnet.sh start
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Container Runtime
|
||||
|
||||
Apple's container runtime is a native macOS container system:
|
||||
- Uses Virtualization.framework for isolation
|
||||
- Lightweight VMs for each container
|
||||
- Native macOS integration
|
||||
- Separate image store from Docker
|
||||
- Natively uses [Kata Containers](https://github.com/kata-containers/kata-containers) images
|
||||
|
||||
### Initial setup for [Container Runtime](https://github.com/apple/container)
|
||||
|
||||
- **MUST** have MacOS Tahoe for inter-container networking
|
||||
- `brew install --cask container`
|
||||
- Download Kata Containers 3.20, this one can be loaded by `container` and has `CONFIG_TUN=y` kernel flag
|
||||
- `https://github.com/kata-containers/kata-containers/releases/download/3.20.0/kata-static-3.20.0-arm64.tar.xz`
|
||||
- Load new kernel
|
||||
- `container system kernel set --tar kata-static-3.20.0-arm64.tar.xz --binary opt/kata/share/kata-containers/vmlinux-6.12.42-162`
|
||||
- Validate kernel version once you have container running
|
||||
- `uname -r` should return `6.12.42`
|
||||
- `cat /proc/config.gz | grep CONFIG_TUN` should return `CONFIG_TUN=y`
|
||||
|
||||
### Image Building
|
||||
|
||||
Images are built with Docker then transferred:
|
||||
1. `docker build` creates the image
|
||||
2. `docker save` exports to tar file
|
||||
3. `container image load` imports into container runtime
|
||||
4. Temporary file is cleaned up
|
||||
|
||||
This approach allows using Docker's build cache while running on Apple's runtime.
|
||||
|
||||
### Network Architecture
|
||||
|
||||
The custom bridge network (`nym-localnet-network`):
|
||||
- Provides container-to-container communication
|
||||
- Assigns dynamic IPs from 192.168.66.0/24
|
||||
- NAT for outbound internet access
|
||||
- Port publishing for host access
|
||||
|
||||
### Volumes
|
||||
|
||||
Two types of volumes:
|
||||
1. **Shared data** (`/tmp/nym-localnet-*`): Bonding files and topology
|
||||
2. **Node configs** (`/tmp/nym-localnet-home-*`): Node configurations
|
||||
|
||||
Both are ephemeral by default (cleaned up on stop).
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **macOS only**: Apple container runtime requires macOS
|
||||
- **No Docker Compose**: Uses custom orchestration script
|
||||
- **Dynamic IPs**: Container IPs may change between restarts
|
||||
- **Port conflicts**: Cannot run alongside services using same ports
|
||||
- **TUN device**: Gateway requires `ip` command for network interfaces
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- **GitHub Issues**: https://github.com/nymtech/nym/issues
|
||||
- **Documentation**: https://nymtech.net/docs
|
||||
- **Discord**: https://discord.gg/nym
|
||||
|
||||
## License
|
||||
|
||||
This localnet setup is part of the Nym project and follows the same license.
|
||||
@@ -1,287 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import base58
|
||||
|
||||
DEFAULT_OWNER = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf"
|
||||
DEFAULT_SUFFIX = os.environ.get("NYM_NODE_SUFFIX", "localnet")
|
||||
NYM_NODES_ROOT = Path.home() / ".nym" / "nym-nodes"
|
||||
|
||||
|
||||
def debug(msg):
|
||||
"""Print debug message to stderr"""
|
||||
print(f"[DEBUG] {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def error(msg):
|
||||
"""Print error message to stderr"""
|
||||
print(f"[ERROR] {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def maybe_assign(target, key, value):
|
||||
if value is not None:
|
||||
target[key] = value
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_nym_node_version():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nym-node", "--version"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
version_line = result.stdout.strip()
|
||||
if not version_line:
|
||||
return None
|
||||
|
||||
parts = version_line.split()
|
||||
for token in reversed(parts):
|
||||
if token and token[0].isdigit():
|
||||
return token
|
||||
return version_line
|
||||
|
||||
|
||||
def node_config_path(prefix, suffix):
|
||||
path = NYM_NODES_ROOT / f"{prefix}-{suffix}" / "config" / "config.toml"
|
||||
debug(f"Looking for config at: {path}")
|
||||
if path.exists():
|
||||
debug(f" ✓ Config found")
|
||||
return path
|
||||
else:
|
||||
error(f" ✗ Config NOT found at {path}")
|
||||
return None
|
||||
|
||||
|
||||
def read_node_details(prefix, suffix):
|
||||
config_path = node_config_path(prefix, suffix)
|
||||
if config_path is None:
|
||||
error(f"Cannot read node details for {prefix}-{suffix}: config not found")
|
||||
return {}
|
||||
|
||||
debug(f"Running: nym-node node-details --config-file {config_path}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"nym-node",
|
||||
"node-details",
|
||||
"--config-file",
|
||||
str(config_path),
|
||||
"--output=json",
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
debug(f" ✓ node-details command succeeded")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error(f"node-details command failed for {prefix}-{suffix}: {e}")
|
||||
error(f" stdout: {e.stdout}")
|
||||
error(f" stderr: {e.stderr}")
|
||||
return {}
|
||||
except FileNotFoundError:
|
||||
error("nym-node command not found in PATH")
|
||||
return {}
|
||||
|
||||
try:
|
||||
details = json.loads(result.stdout)
|
||||
debug(f" ✓ Parsed node-details JSON")
|
||||
except json.JSONDecodeError as e:
|
||||
error(f"Failed to parse node-details JSON: {e}")
|
||||
error(f" Output was: {result.stdout[:200]}")
|
||||
return {}
|
||||
|
||||
info = {}
|
||||
|
||||
# Get sphinx key and decode from Base58 to byte array
|
||||
sphinx_data = details.get("x25519_primary_sphinx_key")
|
||||
if isinstance(sphinx_data, dict):
|
||||
sphinx_key_b58 = sphinx_data.get("public_key")
|
||||
if sphinx_key_b58:
|
||||
debug(f" Got sphinx_key (Base58): {sphinx_key_b58[:20]}...")
|
||||
try:
|
||||
# Decode Base58 to byte array
|
||||
sphinx_bytes = base58.b58decode(sphinx_key_b58)
|
||||
info["sphinx_key"] = list(sphinx_bytes)
|
||||
debug(f" ✓ Decoded to {len(sphinx_bytes)} bytes")
|
||||
except Exception as e:
|
||||
error(f" Failed to decode sphinx_key: {e}")
|
||||
|
||||
version = get_nym_node_version()
|
||||
if version:
|
||||
info["version"] = version
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def resolve_host(data):
|
||||
# For localnet, always use 127.0.0.1 unless explicitly overridden
|
||||
env_host = os.environ.get("LOCALNET_PUBLIC_IP") or os.environ.get("NYMNODE_PUBLIC_IP")
|
||||
if env_host:
|
||||
return env_host.split(",")[0].strip()
|
||||
|
||||
# Default to localhost for localnet (containers can reach each other via published ports)
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def create_mixnode_entry(base_dir, mix_id, port_delta, suffix, host_ip):
|
||||
"""Create a node_details entry for a mixnode"""
|
||||
debug(f"\n=== Creating mixnode{mix_id} entry ===")
|
||||
mix_file = Path(base_dir) / f"mix{mix_id}.json"
|
||||
debug(f"Reading bonding JSON from: {mix_file}")
|
||||
with mix_file.open("r") as json_blob:
|
||||
mix_data = json.load(json_blob)
|
||||
|
||||
node_details = read_node_details(f"mix{mix_id}", suffix)
|
||||
|
||||
# Get identity key from bonding JSON (already byte array)
|
||||
identity = mix_data.get("identity_key")
|
||||
if not identity:
|
||||
raise RuntimeError(f"Missing identity_key in {mix_file}")
|
||||
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
|
||||
|
||||
# Get sphinx key from node-details (decoded from Base58)
|
||||
sphinx_key = node_details.get("sphinx_key")
|
||||
if not sphinx_key:
|
||||
raise RuntimeError(f"Missing sphinx_key from node-details for mix{mix_id}")
|
||||
|
||||
host = host_ip
|
||||
port = 10000 + port_delta
|
||||
debug(f" Using host: {host}:{port}")
|
||||
|
||||
entry = {
|
||||
"node_id": mix_id,
|
||||
"mix_host": f"{host}:{port}",
|
||||
"entry": None,
|
||||
"identity_key": identity,
|
||||
"sphinx_key": sphinx_key,
|
||||
"supported_roles": {
|
||||
"mixnode": True,
|
||||
"mixnet_entry": False,
|
||||
"mixnet_exit": False
|
||||
}
|
||||
}
|
||||
|
||||
maybe_assign(entry, "version", node_details.get("version") or mix_data.get("version"))
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
|
||||
"""Create a node_details entry for a gateway"""
|
||||
debug(f"\n=== Creating gateway entry ===")
|
||||
gateway_file = Path(base_dir) / "gateway.json"
|
||||
debug(f"Reading bonding JSON from: {gateway_file}")
|
||||
with gateway_file.open("r") as json_blob:
|
||||
gateway_data = json.load(json_blob)
|
||||
|
||||
node_details = read_node_details("gateway", suffix)
|
||||
|
||||
# Get identity key from bonding JSON (already byte array)
|
||||
identity = gateway_data.get("identity_key")
|
||||
if not identity:
|
||||
raise RuntimeError("Missing identity_key in gateway.json")
|
||||
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
|
||||
|
||||
# Get sphinx key from node-details (decoded from Base58)
|
||||
sphinx_key = node_details.get("sphinx_key")
|
||||
if not sphinx_key:
|
||||
raise RuntimeError("Missing sphinx_key from node-details for gateway")
|
||||
|
||||
host = host_ip
|
||||
mix_port = 10000 + port_delta
|
||||
clients_port = 9000
|
||||
debug(f" Using host: {host} (mix:{mix_port}, clients:{clients_port})")
|
||||
|
||||
entry = {
|
||||
"node_id": node_id,
|
||||
"mix_host": f"{host}:{mix_port}",
|
||||
"entry": {
|
||||
"ip_addresses": [host],
|
||||
"clients_ws_port": clients_port,
|
||||
"hostname": None,
|
||||
"clients_wss_port": None
|
||||
},
|
||||
"identity_key": identity,
|
||||
"sphinx_key": sphinx_key,
|
||||
"supported_roles": {
|
||||
"mixnode": False,
|
||||
"mixnet_entry": True,
|
||||
"mixnet_exit": True
|
||||
}
|
||||
}
|
||||
|
||||
maybe_assign(entry, "version", node_details.get("version") or gateway_data.get("version"))
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
def main(args):
|
||||
if not args:
|
||||
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip]")
|
||||
|
||||
base_dir = args[0]
|
||||
suffix = args[1] if len(args) > 1 and args[1] else DEFAULT_SUFFIX
|
||||
|
||||
# Get container IPs from arguments (or use 127.0.0.1 as fallback)
|
||||
mix1_ip = args[2] if len(args) > 2 else "127.0.0.1"
|
||||
mix2_ip = args[3] if len(args) > 3 else "127.0.0.1"
|
||||
mix3_ip = args[4] if len(args) > 4 else "127.0.0.1"
|
||||
gateway_ip = args[5] if len(args) > 5 else "127.0.0.1"
|
||||
|
||||
debug(f"\n=== Starting topology generation ===")
|
||||
debug(f"Output directory: {base_dir}")
|
||||
debug(f"Node suffix: {suffix}")
|
||||
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}")
|
||||
|
||||
# Create node_details entries with integer keys
|
||||
node_details = {
|
||||
1: create_mixnode_entry(base_dir, 1, 1, suffix, mix1_ip),
|
||||
2: create_mixnode_entry(base_dir, 2, 2, suffix, mix2_ip),
|
||||
3: create_mixnode_entry(base_dir, 3, 3, suffix, mix3_ip),
|
||||
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip)
|
||||
}
|
||||
|
||||
# Create the NymTopology structure
|
||||
topology = {
|
||||
"metadata": {
|
||||
"key_rotation_id": 0,
|
||||
"absolute_epoch_id": 0,
|
||||
"refreshed_at": datetime.utcnow().isoformat() + "Z"
|
||||
},
|
||||
"rewarded_set": {
|
||||
"epoch_id": 0,
|
||||
"entry_gateways": [4],
|
||||
"exit_gateways": [4],
|
||||
"layer1": [1],
|
||||
"layer2": [2],
|
||||
"layer3": [3],
|
||||
"standby": []
|
||||
},
|
||||
"node_details": node_details
|
||||
}
|
||||
|
||||
output_path = Path(base_dir) / "network.json"
|
||||
debug(f"\nWriting topology to: {output_path}")
|
||||
with output_path.open("w") as out:
|
||||
json.dump(topology, out, indent=2)
|
||||
|
||||
print(f"✓ Generated topology with {len(node_details)} nodes")
|
||||
print(f" - 3 mixnodes (layers 1, 2, 3)")
|
||||
print(f" - 1 gateway (entry + exit)")
|
||||
debug(f"\n=== Topology generation complete ===\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user