Compare commits

...

77 Commits

Author SHA1 Message Date
benedetta davico 039c599c20 Update Cargo.toml 2026-03-30 13:40:55 +02:00
benedetta davico 2cd38b6937 Update Cargo.toml 2026-03-30 13:33:13 +02:00
benedettadavico 1672135308 bump versions 2026-03-30 07:11:55 +02:00
mfahampshire c07ef0253d Max/sdk stream wrapper (#6320)
* Replace MixnetStream with LP framing
- Replace custom header with LpFrameHeader
- Added sequence number for message ordering

* IPR: support LP Stream-framed client connections
- Detect and route LP Stream frames in mixnet_listener
- Wrap inline responses in LP Stream frames
- Thread stream_id to ConnectedClientHandler for TUN responses

* sdk: add ipr_wrapper module with IpMixStream
- IpMixStream wraps MixnetStream for IPR tunnel over mixnet
- LP Stream framing handled automatically by MixnetStream
- Gateway discovery, connect handshake, IP packet send/receive

* sdk: remove superseded stream_wrapper module

* Trim obvious comments, add architecture.md stub

* sdk: add missing deps and fix warnings

* Cut down architecture diagram until finished with rest of the code, leaving stubs

* sdk: refactor IpMixStream, extract shared helpers

- Extract gateway discovery and connect response parsing
- Add recv() to MixnetStream, remove 64KB read buffer
- Simplify IpMixStream constructor

* Fix SphinxStream renames missed during rebase

* Add IpPacketResponse::from_bytes() for stream-based deserialization

* Clean up ip_packet_client: delete stale connect.rs, take raw bytes not ReconstructedMessage

* Clippy

* Delete unused ip_packet_client modules

- Remove helpers.rs (ICMP utilities moved to example)
- Remove error.rs (errors consolidated into sdk/error.rs)
- Remove README.md
- Update module root to only export discovery + listener

* Simplify listener, IpMixStream, and network_env

- Collapse IprListener struct into standalone handle_ipr_response()
- Move check_ipr_message_version() into listener.rs
- Remove IpMixStream test module (moved to example)
- Remove parse_network() and commented-out Sandbox arms
- Return Result from find_workspace_root() instead of panicking
- Add IprTunnelDisconnected and WorkspaceRootNotFound error variants

* Refactor IPR stream handling and document seq conventions
- Inline stream_id tracking (remove current_stream_id field)
- Re-export encode_stream_frame from clients module
- Document seq=0 reservation for inline control responses
- Document data-path counter starting at 1 with skip-on-wrap

* Add ipr_tunnel example for integration testing
- ICMP ping through IPR with --gateway flag for targeting specific exits
- Move pnet_packet from dependencies to dev-dependencies

* Add message reordering to stream router
- Buffer out-of-order messages per-stream using BTreeMap
- Drain contiguous sequences individually to preserve message boundaries
- Drop duplicate/old sequence numbers with a warning
- Remove dead_code allow on StreamFrame::sequence_num

* Clean up comments and fill architecture.md
- Remove separator line comments
- Update stale comments about ordering not being implemented
- Remove collapsible_if allows, use let-else instead
- Fill in architecture.md data flow and connection lifecycle

* Simplify ipr_tunnel example to minimal smoke test
- Single ping instead of multi-ping loop
- Remove identifier and PING_COUNT
- Collapse ICMP helpers into single build_icmp_ping function

* Add dual-stack IPv6 ping and rename gateway → ipr
- Rename --gateway flag to --ipr and new_with_gateway() to new_with_ipr()
- Add ICMPv6 ping to ipr_tunnel example for dual-stack smoke test
- Tighten echo reply validation (protocol field check, diagnostic output)
- Document IP allocation (subnets, static vs dynamic, client keying) in architecture.md
- Promote LP Stream Open handshake log to INFO

* Tweak subnet comment in docs

* Don't stop IPR listener on decode failure
- Change break to continue so garbage packets can't kill the listener
- Remaining valid packets in the bundle are still processed

* Fix license headers and use workspace dep for pnet_packet
- Switch GPL-3.0 to Apache-2.0 on all SDK library files
- Add missing license headers to 7 files
- Use workspace version for pnet_packet dependency

* Document IP pool isolation from WG/LP dVPN pool
- IPR uses 10.0.0.0/16 on nymtun, WG uses 10.1.0.0/16 on nymwg
- Reference constants.rs as source of truth

* Remove network_env.rs and simplify IpMixStream API
  - Default to mainnet via setup_env(None) instead of requiring env param
  - Remove NetworkEnvironment enum and workspace root detection
  - Remove WorkspaceRootNotFound error variant
  - Update ipr_tunnel example to match new signatures

* Use weighted random selection for IPR gateway discovery
  - Replace max_by_key with choose_weighted biased by performance score
  - Prevents all clients converging on a single highest-performing IPR

* Cap stream reorder buffer to prevent unbounded memory growth
- Add MAX_REORDER_BUFFER (256) to limit per-stream pending messages:
	- buffer overflows = skip ahead to lowest buffered seq and drain
	- protects against malicious senders that deliberately skip sequence numbers

* Extract shared IPR response helpers into nym-ip-packet-requests
  - Add response_helpers module with version check, connect response
    parsing, and control response dispatch
  - SDK ip_packet_client now delegates to shared module
  - Monorepo nym-ip-packet-client uses shared version check and
    connect response parsing
  - Fix doc comment attributing fork to nym-vpn-client

* Extract ICMP test helpers into nym-ip-packet-requests
  - Add icmp_utils module behind test-utils feature flag
  - Move build_icmp_ping, build_icmpv6_ping, is_echo_reply_v4/v6 from
    example
  - Update ipr_tunnel example to use shared helpers

* Add protocol v9 LP-framed transport marker

- Add v9 module (re-exports v8, VERSION=9)
- Accept v9 requests and responses in IPR
- Switch SDK IpMixStream to send v9

* Log protocol version in dynamic connect requests

* Remove KCP from IPR and fix unwrap_or_default in SDK
- Remove all KCP session management from ip-packet-router (replaced by
  LP Stream framing)
- Drop nym-kcp dependency and KcpError variant from IPR
- Replace unwrap_or_default with ok_or(Error::NoNymAPIUrl) in
  IpMixStream::new()

* Add v9 protocol wrapper constructors and enforce version/transport
consistency
- Add v9::new_connect_request(), new_data_request(),
  new_ip_packet_response() to centralise version stamping
- Replace manual protocol.version overrides in SDK and IPR with v9
  wrapper calls
- Bump nym-ip-packet-client current re-export from v8 to v9
- Enforce LP Stream frames must carry v9+ payloads, non-stream must be
  v8 or lower

* Filter IPR exit nodes by minimum v9-compatible release version
- Define MIN_RELEASE_VERSION (1.30.0) in ip-packet-requests/v9 alongside protocol constants
- Add semver-based filtering in SDK gateway discovery to skip nodes below v9 threshold
- Add semver dependency to ip-packet-requests and nym-sdk

* Use numeric version comparison for transport/version enforcement
- Compare version as u8 instead of enum equality so future v10+ is handled correctly
- Remove unused `use super::*` import left over from KCP test removal
2026-03-27 20:35:26 +00:00
benedetta davico cc799b69d3 Merge pull request #6622 from nymtech/jmwample/fallback-nym-ip
Update Fallback IP for Nym API
2026-03-27 10:06:13 +01:00
jmwample dd4bbc0708 nym-api moved default 2026-03-26 11:36:04 -06:00
Jack Wampler 7b77091fb1 Nym Node spam logging (#6621)
prevent spam logs when downstream node is slow
2026-03-26 11:27:14 -06:00
Jędrzej Stuczyński 6581ebf235 feat: multiple deposit prices (#6608)
* added reduced pricing handling logic

* admin methods for setting the whitelist of reduced price accounts

* updated client traits

* query to get all whitelisted accounts

* query for getting detailed deposit statistics

* fixes

* set initial whitelisted accounts in the migration

* stop transferring tokens to the holding account after redemption

* stop gateways from creating redemption multisig proposals

* make sure credential-proxy uses reduced deposits when available

* cargo fmt

* update deposit handler to allow EITHER default price or reduced price

this will allow non-breaking upgrades of NS and credential proxy

* removed use of unstable rust features

* rebuilt contract schema

* correct license timestamp
2026-03-26 16:02:19 +00:00
benedetta davico 82ace6d27b Merge pull request #6611 from nymtech/master
Keep master and develop in sync
2026-03-26 16:07:36 +01:00
import this e362207583 [DOCs/operators]: Fix - disable ufw to clean machine conf state (#6620) 2026-03-26 12:27:57 +01:00
import this 68caecff35 [DOCs]: Release notes v2026.6 stilton (#6606)
* operators updates

* add headers

* Update changelog.mdx

* bump up node version

* udpate time

* edit typos

---------

Co-authored-by: Merve <111695676+merve64@users.noreply.github.com>
2026-03-26 11:02:10 +01:00
import this 2fae4414d2 NTM Update: single port managment tool (#6607)
* update ntm

* update docs

* add table for ports

* cherry on the cake

* polish ntm

* quic cherry - add 4443
2026-03-26 10:18:32 +01:00
benedetta davico 6eca09b904 Merge pull request #6610 from nymtech/release/2026.6-stilton
Merge stilton to master
2026-03-25 17:09:28 +01:00
benedetta davico 7ab821cb11 Merge pull request #6609 from nymtech/release/2026.6-stilton
Merge stilton to develop
2026-03-25 17:09:16 +01:00
benedettadavico 0343469179 update changelog 2026-03-25 07:47:04 +01:00
mfahampshire 9904f6b17c Make mobile friendly (#6605)
- Add overflow:hidden on grid
- Shrink `pre` font on mobile
- Stack grid on narrow pages
2026-03-24 21:56:15 +00:00
mfahampshire 5e0eeeddd6 hotfix (#6603) 2026-03-24 15:32:30 +00:00
mfahampshire b6df383584 Max/docs theme rework (#6593)
* Rawer landing page
- Angular, clean docs styling inspired by Oxide
- zero all border-radius globally (kill rounded corners)
- sharp code blocks with subtle border
- callouts: left-border accent instead of rounded pill
- clean table grid lines, sharp search box and MUI buttons
- tighter heading letter-spacing (-0.02em)
- flat left-border sidebar active item instead of background blob

* Add JetBrains Mono for headings/sidebar, push Oxide styling further
- import JetBrains Mono via Google Fonts
- apply mono font to headings, sidebar, nav bar, search, table headers
- darken background (#181C1E), muted body text, h2 bottom border
- subtle background tint on active sidebar item
- inline code: background-only (no border), monospace table headers
- fix active sidebar item font size (scope separator label rule)

* Rework docs landing page: hero, ASCII cards, SDKs, get started
- add hero section with subtitle covering all doc areas
- replace PNG vector illustrations with ASCII art in primary green
- add SDKs section with Rust and TypeScript links
- add get started section: What is the Mixnet, Send a message, Run a node
- add footer links to GitHub and Matrix
- fix nav dropdown font (button + ul selectors)
- add landing card hover style

* Self-host JetBrains Mono, refine landing page
- replace Google Fonts import with local @font-face (woff2)
- add font files + OFL license to public/fonts/
- remove redundant "Nym Docs" hero heading (already in nav)
- drop quick-links pills section
- fix SDK box borders (negative margin collapse)
- rewrite footer as simple link row (GitHub, Matrix, nym.com)

* Light mode styling, dark-mode diagram invert, click-to-expand images
- add full light mode CSS: pale grey bg, darker green links, mono fonts
- invert diagram images in dark mode with mix-blend-mode: lighten
- add click-to-expand overlay for content images
- revert mermaid diagrams back to original PNGs

* Fix Lychee config for fonts

* Make light mode green darker

* Animate landing page ASCII art, remove architecture diagram

- Network: animated packet traversal through gw_e → M1/M2/M3 → gw_ex
  with diagonal cross-connections showing mixing paths
- Developers: typewriter effect with blinking cursor
- Operators: looping progress bar with continuously incrementing packet count
- APIs: staged line-by-line response reveal
- Remove architecture overview PNG from network/architecture.mdx

* Small copy change to SDK headers

* Fix links
2026-03-24 15:08:07 +00:00
Simon Wicky b7d13d6fa6 lp fixes (#6601) 2026-03-23 16:18:45 +01:00
benedetta davico 838dd630ae Merge pull request #6590 from nymtech/ci-runner-22.04
temporarily change binaries ci runner to 22.04
2026-03-20 15:38:46 +01:00
benedetta davico 3f00e2c317 Merge pull request #6592 from nymtech/bdq/bump-ns-version
bump NS versions
2026-03-20 15:37:18 +01:00
benedettadavico 3cdda8fdfd bump NS version 2026-03-20 15:33:16 +01:00
benedetta davico 33f47ef36e Merge pull request #6591 from nymtech/release/2026.6-stilton
merge stilton to develop
2026-03-20 15:30:48 +01:00
benedetta davico 7f9dba6e99 Change OS from arc-linux to ubuntu-22.04 2026-03-20 15:24:53 +01:00
benedetta davico 96e88b6ea9 Change CI platform from arc-linux to ubuntu-22.04 2026-03-20 14:42:10 +01:00
dynco-nym 180802feb8 Fix socks5 GW probe regression (#6576)
* Restore tested gateway into topology

* Bump agent version

* Update .sqlx files

* Clean up code in probe test

* Probe error & logging improvements

* Fix clippy, improve log line

* Improve logging in ns agent

* Better tooling in NS API

* Bump agent

* Bump NS agent version
2026-03-20 10:36:32 +01:00
Jędrzej Stuczyński 87882f70cf bugfix: allow deserialisation of LP data from either snake_case or lowercase (#6586) 2026-03-20 08:26:27 +00:00
mfahampshire 4077717d3a Max/lp stream framing (#6573)
* Add LpFrameKind::Stream variant with StreamFrameAttributes
- Define LP wire format for stream multiplexing
- Handle new variant in entry gateway match arm

* Replace MixnetStream with LP framing
- Replace custom header with LpFrameHeader
- Added sequence number for message ordering

* Revert accidental vergen bump

* Revert accidental bumps

* Rename Stream to SphinxStream and split match arms in client_handler

* Add LpFrameAttributes type alias for [u8; 14]
2026-03-19 15:30:59 +00:00
Simon Wicky bc3df31518 move format_debug_bytes in common crate (#6580)
* move format_debug_bytes in common crate

* license change
2026-03-19 15:09:20 +01:00
Jack Wampler 61d6acace8 HTTP domain rotation conditions (#6570)
Add more explicit handling for df enable and domain rotations
2026-03-19 07:38:48 -06:00
Jędrzej Stuczyński abb4e3f988 bugfix: make sure client keys are generated before requesting credentials (#6579) 2026-03-19 08:55:00 +00:00
mfahampshire c5488337da Max/mixfetch docs tweak (#6523)
* update mixfetch concurrency info

* Update MixFetch version + update note

* Update python3 install method on docs runners
2026-03-18 14:23:51 +00:00
mfahampshire f06eefe184 Only publish mixfetch in script (#6560) 2026-03-18 14:01:24 +00:00
benedettadavico 46a8697a5d version bump 2026-03-18 13:17:14 +01:00
Jędrzej Stuczyński 0429238b0f bugfix: make sure to run cargo install cosmwasm-check with --locked flag during CI (#6568) 2026-03-17 14:52:01 +00:00
dynco-nym 8dc3ba4ec3 Add LP to NS UI (#6562)
* Add LP column to gateway view

* Add LP to graphs
2026-03-16 14:07:19 +01:00
Lawrence Stalder 712e3f5183 Change runner from ubuntu-latest to arc-linux-latest 2026-03-13 14:25:33 +01:00
Lawrence Stalder 5229df47ab Change runner to arc-linux-latest for SonarQube job 2026-03-13 14:24:04 +01:00
Lawrence Stalder 32cffed36b Change runner from ubuntu-latest to arc-linux-latest 2026-03-13 14:16:42 +01:00
Jędrzej Stuczyński 49c710e651 feat: nyxd watcher (#6561)
* removed explicit storage_tx within MsgModule, TxModule and BlockModule impls

* created a NyxdWatcher that does not persist processed block info

* removed unused imports
2026-03-13 13:15:36 +00:00
Lawrence Stalder 0a5227a894 Remove cron schedule from nightly-build workflow
Removed scheduled cron job from nightly build workflow.
2026-03-13 14:01:02 +01:00
mfahampshire b231eb4f04 Max/asyncread asyncwrite nym client (#6318)
* Remove AsyncRead/Write traits from native client - moving them to
stream/

* Substream model first push

* Update / add examples

* Update lockfile

* Clippy

* clippy examples

* remove codecs

* Remove unused bincode setup

* Revert a lot of changes when SDK client itself implemented
AsyncRead/Write

* Remove unnecessary mut

* Use local PollSender in MixnetStream instead of client_input.input_sender

Now that client-core's input_sender is back to mpsc::Sender (reverted
PollSender migration), MixnetStream creates its own PollSender wrapper
for the AsyncWrite impl's poll_ready/start_send calls.

* Remove now-unnecessary parameter

* Clippy

* Cleanup more stragglers from previous setup (Async traits on
MixnetClient)

* Rename files (remove module inception)

* - Shrink StreamId from 16 bytes to u64, add version byte to wire format
  - Introduce MixStreamHeader/MixStreamFrame structs for decode
  - Replace StreamMap type alias with struct using tokio::sync::Mutex
  - Add StreamMap helper methods, eliminate lock().expect() panics
  - Rename stream.rs -> mixnet_stream.rs to avoid module inception
  - Document irrevocable stream mode, add LP integration TODO

* - Remove dummy channel
- Add err variant for reciever alredy taken
- Remove panics

* add timeout to stream

* clippy
2026-03-13 09:40:45 +00:00
mfahampshire fdd2c8fac2 update nymvpn cli docs (#6559)
* update nymvpn cli docs

* update nymvpn cli docs again
2026-03-12 16:32:39 +00:00
Jędrzej Stuczyński e2dd8ac743 feat: localnet v2 (#6277)
* squashing localnet-v2 commits (again)

cargo fmt

fixes to localnet purge

provide path in the error message

output args

log failed exec

print based on tty

check-prerequisites cmd

checked iptables update

basic kernel features check

enable ipv6 rules

add forwarding rules

squashing localnet-v2 commits

additional changes

propagate custom-dns flag to all run containers

remove is_mock from EcashManager

another localnet squash

unused import

chore: remove redundant testnet manager

missing impl

additional linux fixes

command to rebuild container image

wait for at least 2 blocks

additional node startup fixes

added --custom-dns flag to nym node setup

add gateway probe + wait for DKG magic file

fixed localnet down on linux

container ls

re-enable state resync

additional feature locking

macos adjustments

working nyxd startup on linux

wip linux box

wip

separating network inspect betweewn macos and linux

initial linux feature locking

moved all container commands into a single location

finally working initial node performance

squashing orchestrator commits

cleanup

fixed condition for naive rearrangement

added cache of cosmwasm contracts for speed up on subsequent runs

'down' command

refreshing described cache after nodes are bonded

nym nodes setup + wip on nym api refresh

nodes setup WIP

first pass cleanup

placeholder for nym-node setup

bypassing the dkg

further progress on nym-api setup

wip: api setup

up/down/purge placeholders

persisting contract setup data

fix contract upload by forcing amd64 container platform

wip: contracts setup4

wip: contracts setup3

wip: contracts setup2

wip: contracts setup

include network setup

init and spawn nyxd

build nyxd image in dedicated orchestrator

build nyxd image

squashed cherry-picked lp changes

Bits and bobs to make everything work

Title

MacOS setup instructions

Docker/Container localnet

* clippy

* fixes on non-unix targets

---------

Co-authored-by: durch <durch@users.noreply.github.com>
2026-03-12 14:46:00 +00:00
import this 8001fa7f40 [HOTFIX/DOCs]: Get Vercel deployment to work (#6557)
* try rebuild

* update package.json
2026-03-12 13:53:04 +00:00
dynco-nym 80370b98ec Additional ticket for agent (#6551)
* Additional ticket type for LP tests

* Remove hardcoded comments

* bump cargo version

* Nuke fallback edge case in the probe

* Cleanup unused code

* Bump API & agent versions
- agent bump required due to probe changes
2026-03-12 14:49:03 +01:00
import this 3524089ad8 [DOCs/operators]: Release notes for v2026.5 raclette (#6556)
* update changelog

* bump up versions

* bump up stats

* update stats

* rephrase probe info
2026-03-12 13:05:25 +00:00
mfahampshire ec7ee49282 Version bump (#6553)
* Version bump

* update docs dep
2026-03-12 10:40:07 +00:00
import this 653d1c2dea [NTM]: Open ports according to NIP-8 and NIP-9 (#6545)
* add nip-9 to NTM

* update ntm nip 8

* fix symbol syntax
2026-03-12 10:10:50 +00:00
mfahampshire b579f987b1 Max/mixfetch concurrentcy tweak (#6539)
* Remove debug connect logging

* Add random suffix to addressmapping for concurrent outgoing requests to
same endpoint

* Comment + renaming + pulling apart of mapping key & URL.

* Add certs file + remove hardcoding + add certs script

* Add cleanbuild helper script

* Update DEVELOPERS.md

* Add cleanbuild script info to DEVELOPERS.md

* Remove notice about blocking on concurrent same endpoint reqs
2026-03-11 18:42:07 +00:00
Jędrzej Stuczyński 59254c92c3 bugfix: make sure to use old values from metrics debug config during v12 migration (#6546) (#6547) 2026-03-11 08:33:53 +00:00
Simon Wicky 69887921cc typo (#6543) 2026-03-10 16:27:02 +01:00
import this e075b07632 Hotfix: Add a missing commit with an ansible role (#6542)
* Create ansible playbook for trimming and rotationg logs

* add docs for triming and log rotation

* update ansible docs

* add info on logic

* cleanup the cleanup guide

* update scraped stats

* ready for review

* address review

* add main default values
2026-03-10 14:23:02 +00:00
import this d32b680351 Server Ansible maintenance & documentation (#6514)
* Create ansible playbook for trimming and rotationg logs

* add docs for triming and log rotation

* update ansible docs

* add info on logic

* cleanup the cleanup guide

* update scraped stats

* ready for review

* address review
2026-03-10 13:28:39 +00:00
Simon Wicky fcd59a19be rng changes for a Send variant (#6541) 2026-03-10 13:43:49 +01:00
dynco-nym 08b20ac2ab Add LP fields (#6535)
* Add lp field to /dvpn/gateways

* Expand unit tests

* Add lp ports, keys, hashes

* Include the whole struct

* Update Toml version
2026-03-10 13:06:56 +01:00
Jędrzej Stuczyński 4c007669f9 chore: update ts-rs dep (#6517) 2026-03-10 11:51:30 +00:00
benedetta davico e86fa8fc7f Merge pull request #6537 from nymtech/release/2026.5-raclette
Raclette to master
2026-03-10 12:07:12 +01:00
benedetta davico c3a8fa8d0d Merge pull request #6536 from nymtech/release/2026.5-raclette
Raclette to develop
2026-03-10 12:06:56 +01:00
Simon Wicky d8769157fd enable LP registration in registration client (#6534) 2026-03-10 11:35:48 +01:00
benedettadavico 7cccf3cfff update changelog 2026-03-10 10:46:12 +01:00
Jędrzej Stuczyński 02eec164f8 bugfix: lp information to have proper snake_case on API endpoints (#6531) 2026-03-09 14:56:31 +00:00
Jędrzej Stuczyński 4f13ab1e0a Merge pull request #6532 from nymtech/chore/reg-metrics
chore: introduce additional prometheus metrics for registration times
2026-03-09 14:56:18 +00:00
benedetta davico a34c7ef19f Merge pull request #6533 from nymtech/bugfix/lp-gateway-probe
bugfix: correctly populate gateway probe LP data
2026-03-09 15:55:00 +01:00
Jędrzej Stuczyński f00b18298c bugfix: correctly populate gateway probe LP data 2026-03-09 14:10:24 +00:00
Jędrzej Stuczyński 0426adc94e chore: introduce additional prometheus metrics for registration times 2026-03-09 13:50:46 +00:00
Jędrzej Stuczyński 4b4a2fe387 Merge pull request #6530 from nymtech/chore/rename-lp-message
chore: rename LpMessage to LpFrame
2026-03-09 13:44:39 +00:00
Jędrzej Stuczyński 1ebb7e06c7 chore: rename LpMessage to LpFrame 2026-03-09 13:21:39 +00:00
Jędrzej Stuczyński 1fd17c5cb3 Merge pull request #6526 from nymtech/chore/lp-improvements
chore: LP improvements
2026-03-09 10:57:26 +00:00
Jędrzej Stuczyński ef65cf4c9e additional adjustments 2026-03-06 16:34:42 +00:00
Jędrzej Stuczyński 48dad0f16b bugfix: setting correct LpPeerConfig during handshake 2026-03-06 16:09:28 +00:00
Jędrzej Stuczyński 93ac638765 importing over changes from 'lp/persistent-node-connection' 2026-03-06 16:07:35 +00:00
Jędrzej Stuczyński c6589ca92c chore: add unit test for mutual KKT 2026-03-06 15:40:13 +00:00
Sachin Kamath 03d5a87826 Merge pull request #6525 from nymtech/readme-midnight-attribution
chore: add midnight attribution
2026-03-06 16:47:16 +05:30
Sachin Kamath 512cfd1b74 chore: add midnight attribution 2026-03-06 16:40:45 +05:30
Bogdan-Ștefan Neacşu ba0625cd97 Remove dep leak of strum iterator (#6522)
strum iterator over an enum leaks the version needed to iterate over it,
which can cause problems to dependent crates that use a different strum
version.

While at it, bump the strum crates as well
2026-03-06 10:44:14 +02:00
mfahampshire a2c489dc5b Max/sitemap generation fix (#6515)
* Tweak README ordering

* Linting

* Add sitemap generation + NEXT env var to CI

* Update domain for sitemap generation

* Inc. sitemap -0

* test remove lockfile

* fix borked name in package

* add redoc

* add framer

* Add pnpm-lock file

* Add sitemap to remote + ci workflow

* remove extra sitemap

* remove static files from remote for vercel

* add sitemap gen to next build step for vercel
2026-03-04 16:01:51 +00:00
525 changed files with 31694 additions and 15435 deletions
+2 -4
View File
@@ -7,7 +7,7 @@ jobs:
build:
runs-on: arc-ubuntu-22.04
env:
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
NEXT_PUBLIC_SITE_URL: https://nym.com/docs
defaults:
run:
working-directory: documentation/docs
@@ -15,10 +15,8 @@ jobs:
- uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo pip3 install pandas tabulate
run: sudo apt install -y python3-pandas python3-tabulate
- name: Install rsync
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
@@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [arc-linux-latest]
platform: [ubuntu-22.04]
runs-on: ${{ matrix.platform }}
env:
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
components: rustfmt, clippy
- name: Install cosmwasm-check
run: cargo install cosmwasm-check
run: cargo install cosmwasm-check --locked
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
+3 -3
View File
@@ -20,10 +20,8 @@ jobs:
- uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo pip3 install pandas tabulate
run: sudo apt install -y python3-pandas python3-tabulate
- name: Install rsync
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
@@ -48,6 +46,8 @@ jobs:
run: pnpm i
- name: Build project
run: pnpm run build
- name: Generate sitemap
run: npx next-sitemap
- name: Move files to /dist/
run: ../scripts/move-to-dist.sh
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
runs-on: arc-linux-latest
steps:
- uses: actions/checkout@v6
with:
-2
View File
@@ -2,8 +2,6 @@ name: nightly-build
on:
workflow_dispatch:
schedule:
- cron: '14 1 * * *'
jobs:
build:
@@ -9,7 +9,7 @@ on:
jobs:
integration-tests:
runs-on: ubuntu-latest
runs-on: arc-linux-latest
env:
API_BASE_URL: http://localhost:8000
+1 -1
View File
@@ -23,7 +23,7 @@ env:
jobs:
check-milestone:
name: Check Milestone
runs-on: ubuntu-latest
runs-on: arc-linux-latest
steps:
- if: github.event.pull_request.milestone == null && contains( env.LABELS, 'no-milestone' ) == false
run: exit 1
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: arc-linux-latest
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
+3
View File
@@ -36,6 +36,9 @@ jobs:
with:
go-version: "1.24.6"
- name: Update root CA certificate bundle
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
- name: Install dependencies
run: yarn
+94
View File
@@ -4,6 +4,100 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.6-stilton] (2026-03-25)
- lp fixes ([#6601])
- bugfix: allow deserialisation of LP data from either snake_case or lowercase ([#6586])
- bugfix: make sure to run cargo install cosmwasm-check with --locked flag during CI ([#6568])
- Add LP to NS UI ([#6562])
- feat: nyxd watcher ([#6561])
- Additional ticket for agent ([#6551])
- bugfix: make sure to use old values from metrics debug config during v12 migration (#6546) ([#6547])
- typo ([#6543])
- rng changes for a Send variant ([#6541])
- Add LP fields ([#6535])
- enable LP registration in registration client ([#6534])
- chore: rename LpMessage to LpFrame ([#6530])
- chore: LP improvements ([#6526])
- Remove dep leak of strum iterator ([#6522])
- chore: update ts-rs dep ([#6517])
- addressing LP PR comments ([#6513])
- remove redundant LP state machine in favour of in place processing ([#6512])
- chore: split up lp listener ([#6507])
- feat: enable mutual KKT exchange ([#6505])
- feat: introduce /v3/unstable/nym-nodes/semi-skimmed to aggregate LP information ([#6499])
- Max/asyncread asyncwrite nym client ([#6318])
- feat: localnet v2 ([#6277])
[#6601]: https://github.com/nymtech/nym/pull/6601
[#6586]: https://github.com/nymtech/nym/pull/6586
[#6568]: https://github.com/nymtech/nym/pull/6568
[#6562]: https://github.com/nymtech/nym/pull/6562
[#6561]: https://github.com/nymtech/nym/pull/6561
[#6551]: https://github.com/nymtech/nym/pull/6551
[#6547]: https://github.com/nymtech/nym/pull/6547
[#6543]: https://github.com/nymtech/nym/pull/6543
[#6541]: https://github.com/nymtech/nym/pull/6541
[#6535]: https://github.com/nymtech/nym/pull/6535
[#6534]: https://github.com/nymtech/nym/pull/6534
[#6530]: https://github.com/nymtech/nym/pull/6530
[#6526]: https://github.com/nymtech/nym/pull/6526
[#6522]: https://github.com/nymtech/nym/pull/6522
[#6517]: https://github.com/nymtech/nym/pull/6517
[#6513]: https://github.com/nymtech/nym/pull/6513
[#6512]: https://github.com/nymtech/nym/pull/6512
[#6507]: https://github.com/nymtech/nym/pull/6507
[#6505]: https://github.com/nymtech/nym/pull/6505
[#6499]: https://github.com/nymtech/nym/pull/6499
[#6318]: https://github.com/nymtech/nym/pull/6318
[#6277]: https://github.com/nymtech/nym/pull/6277
## [2026.5-raclette] (2026-03-10)
- bugfix: correctly populate gateway probe LP data ([#6533])
- chore: introduce additional prometheus metrics for registration times ([#6532])
- bugfix: lp information to have proper snake_case on API endpoints ([#6531])
- removed redundant LP states ([#6509])
- chore: removed all matrix notifications from github actions ([#6495])
- feat: Lewes Protocol with PSQv2 ([#6491])
- build(deps): bump minimatch from 3.1.2 to 3.1.4 in /documentation/docs ([#6486])
- build(deps): bump bn.js from 4.12.2 to 4.12.3 in /documentation/docs ([#6484])
- build(deps): bump bn.js from 4.12.2 to 4.12.3 ([#6483])
- build(deps): bump ajv from 8.17.1 to 8.18.0 in /clients/native/examples/js-examples/websocket ([#6478])
- build(deps): bump ajv from 6.12.6 to 6.14.0 in /documentation/docs ([#6477])
- build(deps): bump minimatch and glob in /documentation/scripts/post-process ([#6476])
- build(deps): bump hono from 4.11.9 to 4.12.0 ([#6475])
- build(deps): bump keccak from 0.1.5 to 0.1.6 ([#6472])
- build(deps-dev): bump qs from 6.14.1 to 6.14.2 in /clients/native/examples/js-examples/websocket ([#6466])
- build(deps): bump mikefarah/yq from 4.52.2 to 4.52.4 ([#6465])
- Otel minimal v2 ([#6464])
- build(deps): bump qs and express in /wasm/client/internal-dev ([#6461])
- bugfix: restore 'latest_measurement' field for nym-node /verloc endpoint ([#6452])
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/node-tester/internal-dev ([#6451])
- Max/mixfetch concurrent test ([#6417])
[#6533]: https://github.com/nymtech/nym/pull/6533
[#6532]: https://github.com/nymtech/nym/pull/6532
[#6531]: https://github.com/nymtech/nym/pull/6531
[#6509]: https://github.com/nymtech/nym/pull/6509
[#6495]: https://github.com/nymtech/nym/pull/6495
[#6491]: https://github.com/nymtech/nym/pull/6491
[#6486]: https://github.com/nymtech/nym/pull/6486
[#6484]: https://github.com/nymtech/nym/pull/6484
[#6483]: https://github.com/nymtech/nym/pull/6483
[#6478]: https://github.com/nymtech/nym/pull/6478
[#6477]: https://github.com/nymtech/nym/pull/6477
[#6476]: https://github.com/nymtech/nym/pull/6476
[#6475]: https://github.com/nymtech/nym/pull/6475
[#6472]: https://github.com/nymtech/nym/pull/6472
[#6466]: https://github.com/nymtech/nym/pull/6466
[#6465]: https://github.com/nymtech/nym/pull/6465
[#6464]: https://github.com/nymtech/nym/pull/6464
[#6461]: https://github.com/nymtech/nym/pull/6461
[#6452]: https://github.com/nymtech/nym/pull/6452
[#6451]: https://github.com/nymtech/nym/pull/6451
[#6417]: https://github.com/nymtech/nym/pull/6417
## [2026.4-quark] (2026-02-24)
- Enhance CI workflow with feature inputs ([#6462])
Generated
+567 -183
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -157,8 +157,8 @@ members = [
"tools/internal/mixnet-connectivity-check",
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/localnet-orchestrator",
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
@@ -171,9 +171,10 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
# "nym-gateway-probe",
"nym-gateway-probe",
"integration-tests",
"common/nym-kkt-ciphersuite", "common/nym-kkt-context",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
]
default-members = [
@@ -190,7 +191,8 @@ default-members = [
"service-providers/ip-packet-router",
"service-providers/network-requester",
"tools/nymvisor",
"nym-registration-client"
"nym-registration-client",
"tools/internal/localnet-orchestrator"
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -232,6 +234,7 @@ bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.11.1"
cargo-edit = "0.13.8"
cargo_metadata = "0.19.2"
celes = "2.6.0"
cfg-if = "1.0.0"
@@ -331,6 +334,7 @@ rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.13.1", default-features = false }
rs_merkle = "1.5.0"
rustls = { version = "0.23.37", default-features = false }
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
@@ -347,8 +351,8 @@ si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
strum = "0.27.2"
strum_macros = "0.27.2"
strum = "0.28.0"
strum_macros = "0.28.0"
subtle-encoding = "0.5"
syn = "2"
sysinfo = "0.37.0"
@@ -375,7 +379,7 @@ tracing-opentelemetry = "0.32.1"
tracing-subscriber = "0.3.20"
tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
ts-rs = "10.1.0"
ts-rs = "12.0.1"
tungstenite = { version = "0.20.1", default-features = false }
typed-builder = "0.23.0"
uniffi = "0.29.2"
@@ -438,6 +442,7 @@ nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
+3
View File
@@ -30,8 +30,11 @@ client ───► Gateway ──┘ mix │ mix ┌─►mix ───►
```
<!-- This is broken
[![Build Status](https://img.shields.io/github/actions/workflow/status/nymtech/nym/build.yml?branch=develop&style=for-the-badge&logo=github-actions)](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
-->
> This project integrates with the Midnight Network
### Building
+89 -13
View File
@@ -1,32 +1,38 @@
---
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
# nym_version: "v2025.21-mozzarella"
#
# NOTE:
# if you want to pin Nym to a specific version instead of using the
# latest release from GitHub in /tasks/main.yml then
# uncomment the line above and set the tag
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
# NOTE: These values will be used globally unless overwritten per node in inventory/all
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
###############################################################################
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
email: "<EMAIL>" # used in certbot, description.toml and landing page
website: "<WEBSITE>" # it is used in the description.toml
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
## Set these vars only if you want them globally for all nodes
## Per node changes in inventory/all will overwrite these global vars
###############################################################################
# NOTE: Set these vars if you want them globally for all nodes
# Per node changes in inventory/all will overwrite these global ones:
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
###############################################################################
## GLOBAL PACKAGES
## These will be installed during deployment
###############################################################################
# NOTE: Possible vars to incule on landing page, etc.
# operator_name: "<OPERATOR_NAME>"
packages:
- tmux
@@ -42,3 +48,73 @@ packages:
- jq
- wget
- ufw
###############################################################################
## OPTIONAL OVERRIDES
## All values below already have defaults in the playbook/roles
## Uncomment only if you want to override them
###############################################################################
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
# nym_version: "v2025.21-mozzarella"
## NOTE:
## if you want to pin Nym to a specific version instead of using the
## latest release from GitHub in /tasks/main.yml then
## uncomment the line above and set the tag
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
## JOURNALD LIMITS
# journald_system_max_use: "100M" # max persistent journal size
# journald_runtime_max_use: "50M" # max runtime journal size
# journald_system_max_file_size: "25M" # max single journal file
# journald_runtime_max_file_size: "10M" # max runtime journal file
# journald_max_retention_sec: "3day" # retention time
# journald_rate_limit_interval: "30s" # rate limit window
# journald_rate_limit_burst: "1000" # rate limit burst
## NYM-NODE LOG CONTROL
# nymnode_log_level_max: "warning" # drop INFO logs
# nymnode_rate_limit_interval: "30s" # per nym-node rate limit window
# nymnode_rate_limit_burst: "200" # per nym-node rate limit burst
## JOURNAL VACUUM TARGETS
# journal_vacuum_size: "100M"
# journal_vacuum_time: "3days"
## RSYSLOG
# disable_rsyslog: true
## FSTRIM SCHEDULE
# fstrim_every_calendar: "*:0/15" # Aggressive
# fstrim_every_calendar: "hourly" # Less aggressive
## OPTIONAL CLEANUPS
# enable_apt_cleanup: true
# enable_snap_cleanup: true
## WRITEBACK TUNING
# enable_writeback_tuning: true
# writeback_dirty_writeback_centisecs: 1500
# writeback_dirty_expire_centisecs: 6000
@@ -0,0 +1,38 @@
---
- name: Restrict logging, vacuum journals, and enable periodic trim
hosts: all
become: true
gather_facts: false
# global knobs - override in inventory/group_vars/host_vars as needed
vars:
journald_system_max_use: "100M"
journald_runtime_max_use: "50M"
journald_system_max_file_size: "25M"
journald_runtime_max_file_size: "10M"
journald_max_retention_sec: "3day"
journald_rate_limit_interval: "30s"
journald_rate_limit_burst: "1000"
# per nym-node rate limit + level cap
nymnode_log_level_max: "warning"
nymnode_rate_limit_interval: "30s"
nymnode_rate_limit_burst: "200"
# journal vacuum targets
journal_vacuum_size: "100M"
journal_vacuum_time: "3days"
# fstrim cadence (note: the systemd override uses cron-like calendar)
fstrim_every_calendar: "*:0/15"
roles:
- role: journald_limits
- role: nymnode_logging
- role: rsyslog_disable
- role: journal_vacuum
- role: classic_log_cleanup
- role: apt_cleanup
- role: snap_cleanup
- role: fstrim_15min
- role: report
@@ -0,0 +1,21 @@
---
- name: Clean apt cache
command: apt-get clean
ignore_errors: true
- name: Autoremove unused packages
command: apt-get -y autoremove
ignore_errors: true
- name: Remove apt lists to reclaim space (they will be re-fetched on update)
file:
path: /var/lib/apt/lists
state: absent
ignore_errors: true
- name: Recreate apt lists directory
file:
path: /var/lib/apt/lists
state: directory
mode: "0755"
ignore_errors: true
@@ -0,0 +1,20 @@
---
- name: Remove classic /var/log files if present (optional)
file:
path: "{{ item }}"
state: absent
loop:
- /var/log/syslog
- /var/log/syslog.1
- /var/log/kern.log
- /var/log/kern.log.1
- /var/log/auth.log
- /var/log/auth.log.1
- /var/log/ufw.log
- /var/log/ufw.log.1
ignore_errors: true
# This is best-effort and may still fail if other packages' postrotate scripts assume services exist.
- name: Force logrotate (best-effort)
command: "logrotate --force /etc/logrotate.conf"
ignore_errors: true
@@ -0,0 +1,3 @@
---
fstrim_timer_dropin_dir: "/etc/systemd/system/fstrim.timer.d"
fstrim_every_calendar: "*:0/15"
@@ -0,0 +1,31 @@
---
- name: Ensure systemd drop-in dir for fstrim.timer exists
file:
path: "{{ fstrim_timer_dropin_dir }}"
state: directory
mode: "0755"
- name: Override fstrim.timer schedule
copy:
dest: "{{ fstrim_timer_dropin_dir }}/override.conf"
mode: "0644"
content: |
[Timer]
OnCalendar=
OnCalendar={{ fstrim_every_calendar }}
Persistent=true
RandomizedDelaySec=0
- name: Reload systemd after fstrim override
systemd:
daemon_reload: true
- name: Enable and start fstrim timer
systemd:
name: fstrim.timer
enabled: true
state: started
- name: Run fstrim now (best-effort)
command: fstrim -av
ignore_errors: true
@@ -0,0 +1,3 @@
---
journal_vacuum_size: "100M"
journal_vacuum_time: "3days"
@@ -0,0 +1,6 @@
---
- name: Vacuum journal to size cap (hard)
command: "journalctl --vacuum-size={{ journal_vacuum_size }}"
- name: Vacuum journal older than retention window (time)
command: "journalctl --vacuum-time={{ journal_vacuum_time }}"
@@ -0,0 +1,8 @@
---
journald_system_max_use: "100M"
journald_runtime_max_use: "50M"
journald_system_max_file_size: "25M"
journald_runtime_max_file_size: "10M"
journald_max_retention_sec: "3day"
journald_rate_limit_interval: "30s"
journald_rate_limit_burst: "1000"
@@ -0,0 +1,5 @@
---
- name: Restart journald
systemd:
name: systemd-journald
state: restarted
@@ -0,0 +1,20 @@
---
- name: Configure journald limits (persistent, capped, rate-limited)
copy:
dest: /etc/systemd/journald.conf
mode: "0644"
content: |
[Journal]
Storage=persistent
Compress=yes
Seal=yes
SystemMaxUse={{ journald_system_max_use }}
RuntimeMaxUse={{ journald_runtime_max_use }}
SystemMaxFileSize={{ journald_system_max_file_size }}
RuntimeMaxFileSize={{ journald_runtime_max_file_size }}
MaxRetentionSec={{ journald_max_retention_sec }}
RateLimitIntervalSec={{ journald_rate_limit_interval }}
RateLimitBurst={{ journald_rate_limit_burst }}
notify: Restart journald
@@ -0,0 +1,7 @@
---
nymnode_log_level_max: "warning"
nymnode_rate_limit_interval: "30s"
nymnode_rate_limit_burst: "200"
nymnode_unit_name: "nym-node" # set to "nym-node.service" if your distro expects it
nymnode_dropin_dir: "/etc/systemd/system/nym-node.service.d"
nymnode_dropin_file: "10-logging.conf"
@@ -0,0 +1,26 @@
---
- name: Ensure systemd drop-in dir for nym-node exists
file:
path: "{{ nymnode_dropin_dir }}"
state: directory
mode: "0755"
- name: Cap nym-node logs + apply per-unit rate limiting
copy:
dest: "{{ nymnode_dropin_dir }}/{{ nymnode_dropin_file }}"
mode: "0644"
content: |
[Service]
LogLevelMax={{ nymnode_log_level_max }}
LogRateLimitIntervalSec={{ nymnode_rate_limit_interval }}
LogRateLimitBurst={{ nymnode_rate_limit_burst }}
- name: Reload systemd after nym-node drop-in
systemd:
daemon_reload: true
- name: Restart nym-node to apply new logging limits (best-effort)
systemd:
name: "{{ nymnode_unit_name }}"
state: restarted
ignore_errors: true
@@ -0,0 +1,8 @@
---
- name: Show journal disk usage
command: journalctl --disk-usage
register: journal_usage
changed_when: false
- debug:
var: journal_usage.stdout
@@ -0,0 +1,13 @@
---
- name: Stop/disable rsyslog if installed (best-effort)
systemd:
name: rsyslog
state: stopped
enabled: false
ignore_errors: true
- name: Remove rsyslog logrotate stanza if present (prevents logrotate failures)
file:
path: /etc/logrotate.d/rsyslog
state: absent
ignore_errors: true
@@ -0,0 +1,10 @@
---
- name: Remove disabled snap revisions (best-effort)
shell: |
set -euo pipefail
snap list --all | awk '/disabled/{print $1, $3}' | while read -r name rev; do
snap remove "$name" --revision="$rev" || true
done
args:
executable: /bin/bash
ignore_errors: true
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.72"
version = "1.1.74"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.72"
version = "1.1.74"
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"
@@ -34,7 +34,7 @@ where
let signing_key = ed25519::PrivateKey::new(&mut rng);
let expiration = expiration.unwrap_or_else(ecash_default_expiration_date);
let deposit_amount = client.get_required_deposit_amount().await?;
let deposit_amount = client.get_default_deposit_amount().await?;
info!("we'll need to deposit {deposit_amount} to obtain the ticketbook");
let result = client
.make_ticketbook_deposit(
+1 -1
View File
@@ -2,7 +2,7 @@
name = "nym-client-core"
version.workspace = true
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
edition = "2024"
rust-version = "1.85"
license.workspace = true
description = "Crate containing core client functionality and configs, used by all other Nym client implentations"
@@ -32,6 +32,7 @@ const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead
const DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead
// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause
// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the
@@ -555,6 +556,11 @@ pub struct Topology {
#[serde(with = "humantime_serde")]
pub max_startup_gateway_waiting_period: Duration,
/// Defines how long the client is going to wait on startup for minimal topology to become online,
/// before abandoning the procedure.
#[serde(with = "humantime_serde")]
pub max_startup_network_waiting_period: Duration,
/// Specifies a minimum performance of a mixnode that is used on route construction.
/// This setting is only applicable when `NymApi` topology is used.
pub minimum_mixnode_performance: u8,
@@ -583,6 +589,7 @@ impl Default for Topology {
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_refreshing: false,
max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD,
max_startup_network_waiting_period: DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD,
minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE,
minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE,
use_extended_topology: false,
@@ -159,6 +159,7 @@ impl From<ConfigV6> for Config {
use_extended_topology: value.debug.topology.use_extended_topology,
ignore_egress_epoch_role: value.debug.topology.ignore_egress_epoch_role,
ignore_ingress_epoch_role: value.debug.topology.ignore_ingress_epoch_role,
..Default::default()
},
reply_surbs: ReplySurbs {
minimum_reply_surb_storage_threshold: value
@@ -160,7 +160,10 @@ where
)
.await?;
} else {
info!("registered with new gateway {} (under address {address}), but this will not be our default address", gateway_details.gateway_id);
info!(
"registered with new gateway {} (under address {address}), but this will not be our default address",
gateway_details.gateway_id
);
}
Ok(GatewayInfo {
@@ -4,13 +4,13 @@
use super::mix_traffic::ClientRequestSender;
use super::received_buffer::ReceivedBufferMessage;
use super::statistics_control::StatisticsControl;
use crate::client::base_client::storage::helpers::store_client_keys;
use crate::client::base_client::storage::MixnetClientStorage;
use crate::client::base_client::storage::helpers::store_client_keys;
use crate::client::cover_traffic_stream::LoopCoverTrafficStream;
use crate::client::event_control::EventControl;
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
use crate::client::real_messages_control;
@@ -52,12 +52,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver};
use nym_statistics_common::clients::ClientStatsSender;
use nym_statistics_common::generate_client_stats_id;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_task::ShutdownTracker;
use nym_topology::provider_trait::TopologyProvider;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_topology::HardcodedTopologyProvider;
use nym_topology::provider_trait::TopologyProvider;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use nym_validator_client::{UserAgent, nyxd::contract_traits::DkgQueryClient};
use rand::prelude::SliceRandom;
use rand::rngs::OsRng;
use rand::thread_rng;
@@ -220,6 +220,7 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
nym_api_urls: Option<Vec<nym_network_defaults::ApiUrl>>,
wait_for_gateway: bool,
wait_for_initial_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
shutdown: Option<ShutdownTracker>,
@@ -250,6 +251,7 @@ where
dkg_query_client,
nym_api_urls: None,
wait_for_gateway: false,
wait_for_initial_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown: None,
@@ -305,6 +307,12 @@ where
self
}
#[must_use]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_initial_topology = wait_for_initial_topology;
self
}
#[must_use]
pub fn with_topology_provider(
mut self,
@@ -674,6 +682,7 @@ where
topology_accessor: TopologyAccessor,
local_gateway: NodeIdentity,
wait_for_gateway: bool,
wait_for_initial_topology: bool,
shutdown_tracker: &ShutdownTracker,
) -> Result<(), ClientCoreError> {
let topology_refresher_config =
@@ -694,6 +703,46 @@ where
tracing::info!("Obtaining initial network topology");
topology_refresher.try_refresh().await;
// 1. wait for the minimum topology (if applicable)
if topology_refresher
.ensure_topology_is_routable()
.await
.is_err()
&& wait_for_initial_topology
{
if let Err(err) = topology_refresher
.wait_for_initial_network(topology_config.max_startup_network_waiting_period)
.await
{
tracing::error!(
"the network did not come become online within the specified timeout: {err}"
);
return Err(err.into());
}
}
// 2. wait for our gateway (if applicable)
if topology_refresher
.ensure_contains_routable_egress(local_gateway)
.await
.is_err()
&& wait_for_gateway
{
if let Err(err) = topology_refresher
.wait_for_gateway(
local_gateway,
topology_config.max_startup_gateway_waiting_period,
)
.await
{
tracing::error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
}
// 3. check if the topology is routable (in case we were NOT waiting for it)
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
tracing::error!(
"The current network topology seem to be insufficient to route any packets through \
@@ -702,30 +751,15 @@ where
return Err(ClientCoreError::InsufficientNetworkTopology(err));
}
let gateway_wait_timeout = if wait_for_gateway {
Some(topology_config.max_startup_gateway_waiting_period)
} else {
None
};
// 4. check if the gateway exists (in case we were NOT waiting for it)
if let Err(err) = topology_refresher
.ensure_contains_routable_egress(local_gateway)
.await
{
if let Some(waiting_timeout) = gateway_wait_timeout {
if let Err(err) = topology_refresher
.wait_for_gateway(local_gateway, waiting_timeout)
.await
{
tracing::error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
} else {
tracing::error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
return Err(err.into());
}
tracing::error!(
"the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}"
);
return Err(err.into());
}
if !topology_config.disable_refreshing {
@@ -1024,6 +1058,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
self.wait_for_initial_topology,
&shutdown_tracker.clone(),
)
.await?;
@@ -1195,9 +1230,11 @@ mod tests {
]);
assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2);
assert!(network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some());
assert!(
network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some()
);
}
#[test]
@@ -1210,11 +1247,13 @@ mod tests {
assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/");
assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2);
assert!(api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string()));
assert!(
api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string())
);
}
#[test]
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend, fs_backend},
config,
config::Config,
error::ClientCoreError,
@@ -10,7 +10,7 @@ use crate::{
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path};
use time::OffsetDateTime;
use tracing::{error, info, trace};
@@ -24,7 +24,9 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
error!(
"setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"
);
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -93,7 +95,9 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
match fs_backend::Backend::try_load(db_path).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
error!(
"setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"
);
archive_corrupted_database(db_path).await?;
setup_fresh_backend(db_path, surb_config).await
}
@@ -1,8 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::error::ClientCoreError;
use nym_client_core_gateways_storage::{
ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore,
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
pub mod v1_1_33 {
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::disk_persistence::CommonClientPaths;
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33;
use crate::error::ClientCoreError;
@@ -11,8 +11,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::cover::generate_loop_cover_packet;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::utils::sample_poisson_duration;
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use rand::{rngs::OsRng, CryptoRng, Rng};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use rand::{CryptoRng, Rng, rngs::OsRng};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
@@ -20,10 +20,10 @@ use tokio::sync::mpsc::error::TrySendError;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep};
use tokio::time::{Sleep, sleep};
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::{sleep, Sleep};
use wasmtimer::tokio::{Sleep, sleep};
pub struct LoopCoverTrafficStream<R>
where
@@ -179,7 +179,9 @@ impl LoopCoverTrafficStream<OsRng> {
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
warn!(
"We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"
);
return;
}
};
@@ -13,10 +13,10 @@ use crate::config::disk_persistence::ClientKeysPaths;
#[cfg(not(target_arch = "wasm32"))]
use nym_crypto::asymmetric::{ed25519, x25519};
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::KeyPairPath;
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
#[cfg(not(target_arch = "wasm32"))]
use nym_sphinx::acknowledgements::AckKey;
// we have to define it as an async trait since wasm storage is async
@@ -4,8 +4,8 @@
use async_trait::async_trait;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::error::GatewayClientError;
use nym_gateway_client::GatewayClient;
use nym_gateway_client::error::GatewayClientError;
pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter};
use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use futures::StreamExt;
use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::{
acknowledgements::{identifier::recover_identifier, AckKey},
chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID},
acknowledgements::{AckKey, identifier::recover_identifier},
chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier},
};
use nym_task::ShutdownToken;
use std::sync::Arc;
@@ -3,11 +3,11 @@
use super::PendingAcknowledgement;
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
use futures::channel::mpsc;
use futures::StreamExt;
use futures::channel::mpsc;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_sphinx::Delay as SphinxDelay;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::ShutdownToken;
use std::collections::HashMap;
use std::sync::Arc;
@@ -9,8 +9,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane;
use nym_task::ShutdownToken;
use nym_task::connections::TransmissionLane;
use rand::{CryptoRng, Rng};
use tracing::*;
@@ -16,10 +16,10 @@ use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::{
Delay as SphinxDelay,
acknowledgements::AckKey,
addressing::clients::Recipient,
chunking::fragment::{Fragment, FragmentIdentifier},
Delay as SphinxDelay,
};
use nym_statistics_common::clients::ClientStatsSender;
use rand::{CryptoRng, Rng};
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use super::{
action_controller::{AckActionSender, Action},
PendingAcknowledgement, RetransmissionRequestReceiver,
action_controller::{AckActionSender, Action},
};
use crate::client::real_messages_control::acknowledgement_control::PacketDestination;
use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError};
@@ -13,7 +13,7 @@ use futures::StreamExt;
use nym_sphinx::chunking::fragment::Fragment;
use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
use nym_task::{connections::TransmissionLane, ShutdownToken};
use nym_task::{ShutdownToken, connections::TransmissionLane};
use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak};
use tracing::*;
@@ -1,10 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use super::SentPacketNotificationReceiver;
use super::action_controller::{AckActionSender, Action};
use futures::StreamExt;
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
use nym_sphinx::chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier};
use tracing::*;
/// Module responsible for starting up retransmission timers.
@@ -10,17 +10,17 @@ use crate::client::replies::reply_controller::MaxRetransmissions;
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_client_core_surb_storage::RetrievedReplySurb;
use nym_sphinx::Delay;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::preparer::{MessagePreparer, PreparedFragment};
use nym_sphinx::Delay;
use nym_task::connections::TransmissionLane;
use nym_task::ShutdownToken;
use nym_task::connections::TransmissionLane;
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, Rng};
use std::collections::HashMap;
@@ -272,7 +272,9 @@ where
let primary_count = msg.required_packets(self.config.primary_packet_size);
let secondary_count = msg.required_packets(secondary_packet);
trace!("This message would require: {primary_count} primary packets or {secondary_count} secondary packets...");
trace!(
"This message would require: {primary_count} primary packets or {secondary_count} secondary packets..."
);
// if there would be no benefit in using the secondary packet - use the primary (duh)
if primary_count <= secondary_count {
trace!("so choosing primary for this message");
@@ -25,9 +25,9 @@ use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_statistics_common::clients::ClientStatsSender;
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use nym_task::ShutdownToken;
use rand::{rngs::OsRng, CryptoRng, Rng};
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use rand::{CryptoRng, Rng, rngs::OsRng};
use std::sync::Arc;
use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig;
@@ -17,11 +17,11 @@ use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketSize;
use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::utils::sample_poisson_duration;
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use nym_task::ShutdownToken;
use nym_task::connections::{
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use nym_task::ShutdownToken;
use rand::{CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
@@ -29,11 +29,11 @@ use std::time::Duration;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep};
use tokio::time::{Sleep, sleep};
// use nym_wasm_utils::console_log;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::{sleep, Sleep};
use wasmtimer::tokio::{Sleep, sleep};
mod sending_delay_controller;
/// Configurable parameters of the `OutQueueControl`
@@ -230,7 +230,9 @@ where
let (next_message, fragment_id, packet_size) = match next_message {
StreamMessage::Cover => {
let cover_traffic_packet_size = self.loop_cover_message_size();
trace!("the next loop cover message will be put in a {cover_traffic_packet_size} packet");
trace!(
"the next loop cover message will be put in a {cover_traffic_packet_size} packet"
);
// TODO for way down the line: in very rare cases (during topology update) we might have
// to wait a really tiny bit before actually obtaining the permit hence messing with our
@@ -244,7 +246,9 @@ where
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
warn!(
"We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"
);
return;
}
};
@@ -436,7 +440,7 @@ where
}
}
if let Some(ref mut next_delay) = &mut self.next_delay {
if let Some(next_delay) = &mut self.next_delay {
// it is not yet time to return a message
if next_delay.as_mut().poll(cx).is_pending() {
return Poll::Pending;
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::helpers::{get_time_now, Instant};
use crate::client::helpers::{Instant, get_time_now};
use std::time::Duration;
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
@@ -5,20 +5,20 @@ use crate::client::helpers::get_time_now;
use crate::client::replies::{
reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys,
};
use futures::StreamExt;
use futures::channel::mpsc;
use futures::lock::Mutex;
use futures::StreamExt;
use nym_crypto::asymmetric::x25519;
use nym_crypto::Digest;
use nym_crypto::asymmetric::x25519;
use nym_gateway_client::MixnetMessageReceiver;
use nym_sphinx::anonymous_replies::requests::{
RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent,
};
use nym_sphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey};
use nym_sphinx::anonymous_replies::{SurbEncryptionKey, encryption_key::EncryptionKeyDigest};
use nym_sphinx::message::{NymMessage, PlainMessage};
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use nym_task::ShutdownToken;
use std::collections::HashSet;
use std::sync::Arc;
@@ -78,14 +78,19 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
let fragment = match self.message_receiver.recover_fragment(fragment_data) {
Err(err) => {
warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!");
warn!(
"failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!"
);
return None;
}
Ok(frag) => frag,
};
if self.recently_reconstructed.contains(&fragment.id()) {
debug!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id());
debug!(
"Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost",
fragment.id()
);
return None;
}
@@ -93,7 +98,9 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
match self.message_receiver.insert_new_fragment(fragment) {
Err(err) => match err {
MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => {
error!("message reconstruction failed - {source}. Attempting to re-use the message sets...");
error!(
"message reconstruction failed - {source}. Attempting to re-use the message sets..."
);
// TODO: should we really insert reconstructed sets? could this be abused for some attack?
for set_id in used_sets {
if !self.recently_reconstructed.insert(set_id) {
@@ -144,7 +151,9 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
&mut raw_fragment,
) {
Err(err) => {
warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!");
warn!(
"failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!"
);
return None;
}
Ok(frag_data) => frag_data,
@@ -275,7 +284,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
RepliableMessageContent::Heartbeat(content) => {
let additional_reply_surbs = content.additional_reply_surbs;
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
error!(
"received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"
);
(additional_reply_surbs, false)
}
RepliableMessageContent::DataV2(content) => {
@@ -304,7 +315,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
RepliableMessageContent::HeartbeatV2(content) => {
let additional_reply_surbs = content.additional_reply_surbs;
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
error!(
"received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"
);
(additional_reply_surbs, false)
}
};
@@ -380,7 +393,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
if let Some(sender) = &inner_guard.message_sender {
trace!("Sending reconstructed messages to announced sender");
if let Err(err) = sender.unbounded_send(reconstructed_messages) {
warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})");
warn!(
"The reconstructed message receiver went offline without explicit notification (relevant error: - {err})"
);
inner_guard.message_sender = None;
inner_guard.messages.extend(err.into_inner());
}
@@ -5,15 +5,15 @@ use crate::client::real_messages_control::acknowledgement_control::PendingAcknow
use crate::client::real_messages_control::message_handler::{
FragmentWithMaxRetransmissions, MessageHandler, PreparationError,
};
use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState;
use crate::client::replies::reply_controller::Config;
use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState;
use crate::client::topology_control::TopologyAccessor;
use crate::client::transmission_buffer::TransmissionBuffer;
use futures::channel::oneshot;
use nym_client_core_surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap};
use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_topology::NymTopologyMetadata;
@@ -50,7 +50,9 @@ impl SenderData {
let pending_retransmissions = self.pending_retransmissions.len();
let total_pending = pending_retransmissions + pending_replies;
debug!("total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}");
debug!(
"total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}"
);
total_pending
}
@@ -200,7 +202,9 @@ where
let total_required_surbs = total_queue + target_surbs_after_clearing_queue;
let total_available_surbs = pending_surbs + available_surbs;
debug!("available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}");
debug!(
"available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"
);
// We should request more surbs if:
// 1. We haven't hit the maximum surb threshold, and
@@ -225,9 +229,13 @@ where
.is_none()
{
// don't report it every single time
warn!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
warn!(
"received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"
);
} else {
trace!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
trace!(
"received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"
);
}
return;
}
@@ -383,7 +391,9 @@ where
let (surbs_for_reply, _) = self.surbs_storage.get_reply_surbs(&target, to_take.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
error!(
"somehow different task has stolen our reply surbs! - this should have been impossible"
);
self.re_insert_pending_retransmission(&target, to_take);
return;
};
@@ -459,7 +469,9 @@ where
.get_reply_surbs(&target, to_send_clone.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
error!(
"somehow different task has stolen our reply surbs! - this should have been impossible"
);
self.re_insert_pending_replies(&target, to_send);
return;
};
@@ -543,7 +555,9 @@ where
let ack_ref = match timed_out_ack.upgrade() {
Some(ack) => ack,
None => {
debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue");
debug!(
"we received the ack for one of the reply packets as we were putting it in the retransmission queue"
);
return;
}
};
@@ -657,9 +671,13 @@ where
// only log at higher level if it's the first time this error has occurred in a while
if now - last_failure > time::Duration::seconds(30) {
warn!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
warn!(
"failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}"
)
} else {
debug!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
debug!(
"failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}"
)
}
}
}
@@ -681,7 +699,10 @@ where
.surbs_storage
.surbs_last_received_at(pending_reply_target)
else {
error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", retransmission_buf.total_size());
error!(
"we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!",
retransmission_buf.total_size()
);
to_remove.push(*pending_reply_target);
continue;
};
@@ -702,7 +723,9 @@ where
// if client is offline)
if vals.current_clear_rerequest_counter > max_rerequests {
to_remove.push(*pending_reply_target);
debug!("we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender");
debug!(
"we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender"
);
continue;
}
@@ -710,7 +733,10 @@ where
if diff > max_drop_wait {
to_remove.push(*pending_reply_target)
} else {
debug!("We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", humantime::format_duration(diff.unsigned_abs()));
debug!(
"We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more",
humantime::format_duration(diff.unsigned_abs())
);
vals.increment_current_clear_rerequest_counter();
to_request.push(*pending_reply_target);
}
@@ -4,8 +4,8 @@
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use futures::channel::{mpsc, oneshot};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;
@@ -43,7 +43,9 @@ where
// 1. check whether we sent any surbs in the past to this recipient, otherwise
// they have no business in asking for more
if !self.tags_storage.exists(&recipient) {
warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!");
warn!(
"{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"
);
return;
}
@@ -54,7 +56,12 @@ where
.reply_surbs
.maximum_allowed_reply_surb_request_size
{
warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size);
warn!(
"The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...",
self.config
.reply_surbs
.maximum_allowed_reply_surb_request_size
);
amount = self
.config
.reply_surbs
@@ -23,7 +23,7 @@ use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{
ClientStatsController, ClientStatsReceiver, ClientStatsSender,
};
use nym_task::{connections::TransmissionLane, ShutdownToken, ShutdownTracker};
use nym_task::{ShutdownToken, ShutdownTracker, connections::TransmissionLane};
use std::time::Duration;
/// Time interval between reporting statistics locally (logging/shutdown_token)
@@ -5,8 +5,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata};
use nym_validator_client::models::KeyRotationId;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::{Notify, RwLock, RwLockReadGuard};
#[derive(Debug)]
@@ -63,7 +63,9 @@ impl TopologyRefresher {
trace!("Refreshing the topology");
if self.topology_accessor.controlled_manually() {
info!("topology is being controlled manually - we're going to wait until the control is released...");
info!(
"topology is being controlled manually - we're going to wait until the control is released..."
);
self.topology_accessor
.wait_for_released_manual_control()
.await;
@@ -138,6 +140,35 @@ impl TopologyRefresher {
}
}
pub async fn wait_for_initial_network(
&mut self,
timeout_duration: Duration,
) -> Result<(), NymTopologyError> {
info!(
"going to wait for at most {timeout_duration:?} for initial network to become online"
);
let deadline = sleep(timeout_duration);
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => {
return Err(NymTopologyError::TimedOutWaitingForTopology)
}
_ = self.try_refresh() => {
if let Err(err) = self.ensure_topology_is_routable().await {
info!("network is still not routable...: {err}");
} else {
return Ok(())
}
sleep(self.refresh_rate).await
}
}
}
}
// it's perfectly fine if task is interrupted mid-refresh
// there's no data to persist or send over
pub async fn run(&mut self) {
@@ -3,8 +3,8 @@
use async_trait::async_trait;
use nym_mixnet_contract_common::EpochRewardedSet;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
use nym_topology::NymTopology;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
use nym_validator_client::nym_api::NymApiClientExt;
use rand::prelude::SliceRandom;
use rand::thread_rng;
@@ -82,7 +82,9 @@ impl NymApiTopologyProvider {
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
warn!(
"There's only a single nym API available - it won't be possible to use a different one"
);
return;
}
@@ -155,7 +157,10 @@ impl NymApiTopologyProvider {
let mixnodes = mixnodes_res.nodes;
if !gateways_res.metadata.consistency_check(&metadata) {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
warn!(
"inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}",
gateways_res.metadata
);
return None;
}
@@ -1,11 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::helpers::{get_time_now, Instant};
use crate::client::helpers::{Instant, get_time_now};
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use nym_sphinx::chunking::fragment::Fragment;
use nym_task::connections::TransmissionLane;
use rand::{seq::SliceRandom, Rng};
use rand::{Rng, seq::SliceRandom};
use std::{
collections::{HashMap, HashSet, VecDeque},
time::Duration,
+13 -5
View File
@@ -7,9 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
use nym_task::RegistryAccessError;
use nym_topology::node::RoutingNodeError;
use nym_topology::{NodeId, NymTopologyError};
use nym_validator_client::ValidatorClientError;
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::ValidatorClientError;
use rand::distributions::WeightedError;
use std::error::Error;
use std::path::PathBuf;
@@ -56,7 +56,9 @@ pub enum ClientCoreError {
#[error("no gateways on network")]
NoGatewaysOnNetwork,
#[error("there are no more new gateways on the network - it seems this client has already registered with all nodes it could have")]
#[error(
"there are no more new gateways on the network - it seems this client has already registered with all nodes it could have"
)]
NoNewGatewaysAvailable,
#[error("list of nym apis is empty")]
@@ -127,7 +129,9 @@ pub enum ClientCoreError {
#[error("unexpected exit")]
UnexpectedExit,
#[error("this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission")]
#[error(
"this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission"
)]
ForbiddenGatewayKeyOverwrite { gateway_id: String },
#[error(
@@ -151,7 +155,9 @@ pub enum ClientCoreError {
#[error("attempted to obtain fresh gateway details whilst already knowing about one")]
UnexpectedGatewayDetails,
#[error("the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys")]
#[error(
"the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys"
)]
MismatchedGatewayDetails { gateway_id: String },
#[error("unable to upgrade config file from `{current_version}`")]
@@ -227,7 +233,9 @@ pub enum ClientCoreError {
source: url::ParseError,
},
#[error("this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command")]
#[error(
"this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command"
)]
AlreadyInitialised { client_id: String },
#[error("this client has already registered with gateway {gateway_id}")]
+5 -5
View File
@@ -5,13 +5,13 @@ use crate::error::ClientCoreError;
use crate::init::types::RegistrationResult;
use futures::{SinkExt, StreamExt};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::GatewayListeners;
use nym_gateway_client::GatewayClient;
use nym_gateway_client::client::GatewayListeners;
use nym_topology::node::RoutingNode;
use nym_validator_client::UserAgent;
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata;
use nym_validator_client::UserAgent;
use rand::{seq::SliceRandom, Rng};
use rand::{Rng, seq::SliceRandom};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::{sync::Arc, time::Duration};
@@ -28,10 +28,10 @@ use nym_wasm_utils::websocket::JSWebsocket;
#[cfg(not(target_arch = "wasm32"))]
use tokio::net::TcpStream;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(not(target_arch = "wasm32"))]
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
#[cfg(target_arch = "wasm32")]
use wasmtimer::std::Instant;
+1 -1
View File
@@ -7,8 +7,8 @@ use crate::client::base_client::storage::helpers::{
has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details,
store_gateway_details, update_stored_published_data_gateway,
};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::error::ClientCoreError;
use crate::init::helpers::{
choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway,
+2 -2
View File
@@ -1,8 +1,8 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::config::Config;
use crate::error::ClientCoreError;
use crate::init::{setup_gateway, use_loaded_gateway_details};
@@ -10,8 +10,8 @@ use nym_client_core_gateways_storage::{
GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails,
};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
use nym_gateway_client::SharedSymmetricKey;
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
use nym_sphinx::addressing::clients::Recipient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKey;
+1
View File
@@ -1,3 +1,4 @@
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
use std::future::Future;
#[cfg(all(
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
pub use backend::*;
pub use combined::CombinedReplyStorage;
pub use key_storage::SentReplyKeys;
@@ -342,7 +342,7 @@ impl SendWithoutResponse for Client {
sending_res.map_err(|err| {
match err {
TrySendError::Full(_) => {
warn!(
trace!(
event = "mixclient.try_send",
peer = %address,
result = "full_dropped",
@@ -8,6 +8,7 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmwasm_std::Coin;
use nym_ecash_contract_common::deposit::LatestDepositResponse;
use nym_ecash_contract_common::deposit_statistics::DepositsStatistics;
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
use serde::Deserialize;
@@ -17,6 +18,9 @@ pub use nym_ecash_contract_common::blacklist::{
pub use nym_ecash_contract_common::deposit::{
Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse,
};
pub use nym_ecash_contract_common::reduced_deposit::{
WhitelistedAccount, WhitelistedAccountsResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -42,8 +46,18 @@ pub trait EcashQueryClient {
.await
}
async fn get_required_deposit_amount(&self) -> Result<Coin, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {})
async fn get_default_deposit_amount(&self) -> Result<Coin, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDefaultDepositAmount {})
.await
}
async fn get_reduced_deposit_amount(&self, address: String) -> Result<Option<Coin>, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetReducedDepositAmount { address })
.await
}
async fn get_all_whitelisted_accounts(&self) -> Result<WhitelistedAccountsResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetAllWhitelistedAccounts {})
.await
}
@@ -65,6 +79,11 @@ pub trait EcashQueryClient {
self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit })
.await
}
async fn get_deposits_statistics(&self) -> Result<DepositsStatistics, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDepositsStatistics {})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -122,10 +141,17 @@ mod tests {
EcashQueryMsg::GetDepositsPaged { limit, start_after } => {
client.get_deposits_paged(start_after, limit).ignore()
}
EcashQueryMsg::GetRequiredDepositAmount {} => {
client.get_required_deposit_amount().ignore()
EcashQueryMsg::GetDefaultDepositAmount {} => {
client.get_default_deposit_amount().ignore()
}
EcashQueryMsg::GetReducedDepositAmount { address } => {
client.get_reduced_deposit_amount(address).ignore()
}
EcashQueryMsg::GetAllWhitelistedAccounts {} => {
client.get_all_whitelisted_accounts().ignore()
}
EcashQueryMsg::GetLatestDeposit {} => client.get_latest_deposit().ignore(),
EcashQueryMsg::GetDepositsStatistics {} => client.get_deposits_statistics().ignore(),
};
}
}
@@ -62,13 +62,47 @@ pub trait EcashSigningClient {
new_deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::UpdateDepositValue {
let req = EcashExecuteMsg::UpdateDefaultDepositValue {
new_deposit: new_deposit.into(),
};
self.execute_ecash_contract(fee, req, "Ecash::UpdateDepositValue".to_string(), vec![])
.await
}
async fn set_reduced_deposit_price(
&self,
address: String,
deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::SetReducedDepositPrice {
address,
deposit: deposit.into(),
};
self.execute_ecash_contract(
fee,
req,
"Ecash::SetReducedDepositPrice".to_string(),
vec![],
)
.await
}
async fn remove_reduced_deposit_price(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::RemoveReducedDepositPrice { address };
self.execute_ecash_contract(
fee,
req,
"Ecash::RemoveReducedDepositPrice".to_string(),
vec![],
)
.await
}
async fn propose_for_blacklist(
&self,
public_key: String,
@@ -141,9 +175,15 @@ mod tests {
.ignore(),
ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client
ExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(),
ExecuteMsg::UpdateDepositValue { new_deposit } => client
ExecuteMsg::UpdateDefaultDepositValue { new_deposit } => client
.update_deposit_value(new_deposit.into(), None)
.ignore(),
ExecuteMsg::SetReducedDepositPrice { address, deposit } => client
.set_reduced_deposit_price(address, deposit.into(), None)
.ignore(),
ExecuteMsg::RemoveReducedDepositPrice { address } => {
client.remove_reduced_deposit_price(address, None).ignore()
}
};
}
}
@@ -130,7 +130,7 @@ pub trait CosmWasmClient: TendermintRpcClient {
let req = QueryBalanceRequest {
address: address.to_string(),
denom: search_denom.to_string(),
denom: search_denom,
};
let res = self
@@ -199,6 +199,18 @@ impl NyxdClient<HttpClient, DirectSecp256k1HdWallet> {
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?;
Ok(Self::connect_with_signer(config, client, wallet))
}
pub fn connect_with_mnemonic_and_network_details<U>(
endpoint: U,
network_details: NymNetworkDetails,
mnemonic: bip39::Mnemonic,
) -> Result<DirectSigningHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&network_details)?;
Self::connect_with_mnemonic(config, endpoint, mnemonic)
}
}
#[allow(deprecated)]
@@ -1,6 +1,8 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::derivable_impls)]
// MAX: surpressing warning for the moment, will be dealt with in a different PR (TODO)
use cosmwasm_schema::cw_serde;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
@@ -6,6 +6,13 @@ use cosmwasm_std::Coin;
#[cw_serde]
pub struct PoolCounters {
/// Represents the total amount of funds deposited into the contract.
pub total_deposited: Coin,
/// Represents the total amount of funds redeemed from the contract that got transferred into the holding account.
pub total_redeemed: Coin,
/// Represents the total amount of tickets requested to be redeemed from the contract and get moved into the holding account,
/// after that functionality got disabled.
pub tickets_requested_and_not_redeemed: u64,
}
@@ -0,0 +1,38 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
use std::collections::HashMap;
/// Aggregate statistics about all deposits made through the ecash contract.
#[cw_serde]
pub struct DepositsStatistics {
/// Total number of deposits ever made (at any price tier),
/// derived from the deposit id counter.
pub total_deposits_made: u32,
/// Total value of all deposits ever made (at any price tier),
/// sourced from `PoolCounters::total_deposited`.
pub total_deposited: Coin,
/// Number of deposits made at the default (non-reduced) price.
pub total_deposits_made_with_default_price: u32,
/// Total value deposited at the default price.
pub total_deposited_with_default_price: Coin,
/// Number of deposits made at any custom (reduced) price, summed across all whitelisted accounts.
pub total_deposits_made_with_custom_price: u32,
/// Total value deposited at custom prices, summed across all whitelisted accounts.
pub total_deposited_with_custom_price: Coin,
/// Per-account breakdown of deposit counts for whitelisted addresses.
// note: we use String for addressing due to serialisation incompatibility
pub deposits_made_with_custom_price: HashMap<String, u32>,
/// Per-account breakdown of deposited amounts for whitelisted addresses.
// note: we use String for addressing due to serialisation incompatibility
pub deposited_with_custom_price: HashMap<String, Coin>,
}
@@ -65,4 +65,26 @@ pub enum EcashContractError {
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
InvalidReducedDepositDenom { expected: String, got: String },
#[error(
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
)]
ReducedDepositNotReduced {
reduced: cosmwasm_std::Uint128,
default: cosmwasm_std::Uint128,
},
#[error("address '{address}' does not have a custom reduced deposit price set")]
NoReducedDepositPrice { address: String },
#[error(
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
)]
DepositBelowTicketBookSize {
amount: cosmwasm_std::Uint128,
ticket_book_size: u64,
},
}
@@ -4,10 +4,12 @@
pub mod blacklist;
pub mod counters;
pub mod deposit;
pub mod deposit_statistics;
pub mod error;
pub mod event_attributes;
pub mod events;
pub mod msg;
pub mod redeem_credential;
pub mod reduced_deposit;
pub use error::EcashContractError;
@@ -9,6 +9,10 @@ use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountRespon
#[cfg(feature = "schema")]
use crate::deposit::{DepositResponse, LatestDepositResponse, PagedDepositsResponse};
#[cfg(feature = "schema")]
use crate::deposit_statistics::DepositsStatistics;
#[cfg(feature = "schema")]
use crate::reduced_deposit::WhitelistedAccountsResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
#[cw_serde]
@@ -42,10 +46,25 @@ pub enum ExecuteMsg {
admin: String,
},
UpdateDepositValue {
#[serde(alias = "update_deposit_value")]
UpdateDefaultDepositValue {
new_deposit: Coin,
},
/// Set (or overwrite) a reduced deposit price for a specific address.
/// Only callable by the contract admin.
SetReducedDepositPrice {
address: String,
deposit: Coin,
},
/// Remove the reduced deposit price for a specific address, reverting them to
/// the default price. Returns an error if the address has no custom price set.
/// Only callable by the contract admin.
RemoveReducedDepositPrice {
address: String,
},
// TODO: properly implement
ProposeToBlacklist {
public_key: String,
@@ -68,7 +87,15 @@ pub enum QueryMsg {
},
#[cfg_attr(feature = "schema", returns(Coin))]
GetRequiredDepositAmount {},
#[serde(alias = "get_required_deposit_amount")]
#[serde(alias = "GetRequiredDepositAmount")]
GetDefaultDepositAmount {},
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
GetReducedDepositAmount { address: String },
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
GetAllWhitelistedAccounts {},
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
@@ -81,7 +108,22 @@ pub enum QueryMsg {
limit: Option<u32>,
start_after: Option<u32>,
},
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
GetDepositsStatistics {},
}
#[cw_serde]
pub struct MigrateMsg {}
pub struct MigrateMsg {
/// Initial set of whitelisted accounts with their reduced deposit prices.
/// Each entry is validated and stored during migration.
pub initial_whitelist: Vec<WhitelistedDeposit>,
}
/// An address and its reduced deposit price, used when seeding the whitelist
/// via migration.
#[cw_serde]
pub struct WhitelistedDeposit {
pub address: String,
pub deposit: Coin,
}
@@ -0,0 +1,16 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
#[cw_serde]
pub struct WhitelistedAccount {
pub address: Addr,
pub deposit: Coin,
}
#[cw_serde]
pub struct WhitelistedAccountsResponse {
pub whitelisted_accounts: Vec<WhitelistedAccount>,
}
@@ -6,7 +6,7 @@ use crate::helpers::LockTimer;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_validator_client::nyxd::contract_traits::NymContractsProvider;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::nyxd::{AccountId, Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, nyxd};
use std::ops::Deref;
use std::sync::Arc;
@@ -50,6 +50,10 @@ impl ChainClient {
Ok(ChainClient(Arc::new(RwLock::new(client))))
}
pub async fn address(&self) -> AccountId {
self.0.read().await.address()
}
pub async fn query_chain(&self) -> ChainReadPermit<'_> {
let _acquire_timer = LockTimer::new("acquire chain query permit");
self.0.read().await
@@ -8,6 +8,7 @@ use nym_validator_client::nyxd::contract_traits::EcashQueryClient;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::RwLock;
use tracing::{info, warn};
pub struct CachedDeposit {
valid_until: OffsetDateTime,
@@ -56,13 +57,29 @@ impl RequiredDepositCache {
// update cache
drop(read_guard);
let address = chain_client.address().await;
info!("checking deposit required by {address}");
let mut write_guard = self.inner.write().await;
let deposit_amount = chain_client
.query_chain()
.await
.get_required_deposit_amount()
let read_permit = chain_client.query_chain().await;
let reduced = read_permit
.get_reduced_deposit_amount(address.to_string())
.await?;
let deposit_amount = match reduced {
Some(reduced) => {
info!("we're permitted to use reduced price");
reduced
}
None => {
warn!(
"using default deposit value {address} is not whitelisted for price reduction"
);
read_permit.get_default_deposit_amount().await?
}
};
let nym_coin: Coin = deposit_amount.into();
write_guard.update(nym_coin.clone());
@@ -3,25 +3,19 @@
use crate::Error;
use crate::ecash::error::EcashTicketError;
use crate::ecash::helpers::for_each_api_concurrent;
use crate::ecash::state::SharedState;
use cosmwasm_std::Fraction;
use cw_utils::ThresholdResponse;
use futures::channel::mpsc::UnboundedReceiver;
use futures::{Stream, StreamExt};
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody};
use nym_api_requests::ecash::models::VerifyEcashTicketBody;
use nym_credentials_interface::Bandwidth;
use nym_credentials_interface::{ClientTicket, TicketType};
use nym_validator_client::EcashApiClient;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::nyxd::contract_traits::{
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
};
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::cw3::Status;
use nym_validator_client::nyxd::contract_traits::MultisigQueryClient;
use si_scale::helpers::bibytes2;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
@@ -31,22 +25,6 @@ use tokio::sync::{Mutex, RwLockReadGuard};
use tokio::time::{Duration, Instant, interval_at};
use tracing::{debug, error, info, instrument, trace, warn};
enum ProposalResult {
Executed,
Rejected,
Pending,
}
impl ProposalResult {
fn is_pending(&self) -> bool {
matches!(self, ProposalResult::Pending)
}
fn is_rejected(&self) -> bool {
matches!(self, ProposalResult::Rejected)
}
}
struct PendingVerification {
ticket: ClientTicket,
@@ -68,43 +46,6 @@ impl PendingVerification {
}
}
struct PendingRedemptionVote {
proposal_id: u64,
digest: Vec<u8>,
included_serial_numbers: Vec<Vec<u8>>,
epoch_id: EpochId,
// vec of node ids of apis that haven't sent a valid response
pending: Vec<u64>,
}
impl PendingRedemptionVote {
fn new(
proposal_id: u64,
digest: Vec<u8>,
included_serial_numbers: Vec<Vec<u8>>,
epoch_id: EpochId,
pending: Vec<u64>,
) -> Self {
PendingRedemptionVote {
proposal_id,
digest,
included_serial_numbers,
epoch_id,
pending,
}
}
fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody {
BatchRedeemTicketsBody::new(
self.digest.clone(),
self.proposal_id,
self.included_serial_numbers.clone(),
gateway_cosmos_addr,
)
}
}
pub struct CredentialHandlerConfig {
/// Specifies the multiplier for revoking a malformed/double-spent ticket
/// (if it has to go all the way to the nym-api for verification)
@@ -132,7 +73,6 @@ pub struct CredentialHandler {
ticket_receiver: UnboundedReceiver<ClientTicket>,
shared_state: SharedState,
pending_tickets: Vec<PendingVerification>,
pending_redemptions: Vec<PendingRedemptionVote>,
}
impl CredentialHandler {
@@ -184,75 +124,6 @@ impl CredentialHandler {
Ok(pending)
}
async fn rebuild_pending_votes(
shared_state: &SharedState,
) -> Result<Vec<PendingRedemptionVote>, EcashTicketError> {
// 1. get all tickets that were not fully verified
let unverified = shared_state.storage.get_all_unresolved_proposals().await?;
let mut pending = Vec::with_capacity(unverified.len());
let epoch_id = shared_state.current_epoch_id().await?;
let apis = shared_state
.api_clients(epoch_id)
.await?
.iter()
.map(|s| (s.cosmos_address.to_string(), s.node_id))
.collect::<Vec<_>>();
for proposal_id in unverified {
// get all of the votes
let votes = shared_state
.start_query()
.await
.get_all_votes(proposal_id as u64)
.await
.map_err(EcashTicketError::chain_query_failure)?
.into_iter()
.map(|v| v.voter)
.collect::<HashSet<_>>();
let mut missing_votes = Vec::new();
// see who hasn't voted
for (api_address, api_id) in &apis {
// for each signer, check if they have actually voted; if not, that's the missing guy
if !votes.contains(api_address) {
missing_votes.push(*api_id)
}
}
// attempt to rebuild SN and digest from the proposal info + storage data
let proposal_info = shared_state
.start_query()
.await
.query_proposal(proposal_id as u64)
.await
.map_err(EcashTicketError::chain_query_failure)?;
let tickets = shared_state
.storage
.get_all_proposed_tickets_with_sn(proposal_id as u32)
.await?;
let digest =
BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number));
let encoded_digest = bs58::encode(&digest).into_string();
if encoded_digest != proposal_info.description {
error!("the lost proposal {proposal_id} does not have a matching digest!");
continue;
}
pending.push(PendingRedemptionVote {
proposal_id: proposal_id as u64,
digest,
included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(),
epoch_id,
pending: missing_votes,
})
}
Ok(pending)
}
pub(crate) async fn new(
config: CredentialHandlerConfig,
ticket_receiver: UnboundedReceiver<ClientTicket>,
@@ -276,51 +147,15 @@ impl CredentialHandler {
// on startup read pending credentials and api responses from the storage
let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?;
// on startup read pending proposals from the storage
// then reconstruct the votes by querying the multisig contract for votes on those proposals
// digest from the description and count from the message
let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?;
Ok(CredentialHandler {
config,
multisig_threshold,
ticket_receiver,
shared_state,
pending_tickets,
pending_redemptions,
})
}
// the argument is temporary as we'll be reading from the storage
async fn create_redemption_proposal(
&self,
commitment: &[u8],
number_of_tickets: u16,
) -> Result<u64, EcashTicketError> {
let res = self
.shared_state
.start_tx()
.await
.request_ticket_redemption(
bs58::encode(commitment).into_string(),
number_of_tickets,
None,
)
.await
.map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?;
// that one is quite tricky because proposal exists on chain, but we didn't get the id...
// but it should be quite impossible to ever reach this unless we make breaking changes
let proposal_id = res
.parse_singleton_u64_contract_data()
.inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}"))
.map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?;
info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets");
Ok(proposal_id)
}
/// Attempt to send ticket verification request to the provided ecash verifier.
async fn verify_ticket(
&self,
@@ -522,42 +357,7 @@ impl CredentialHandler {
async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> {
let mut still_failing = Vec::new();
// 1. attempt to resolve all pending proposals
while let Some(mut pending) = self.pending_redemptions.pop() {
match self.try_resolve_pending_proposal(&mut pending, None).await {
Ok(resolution) => {
if resolution.is_pending() {
warn!(
"still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later",
pending.proposal_id, pending.pending
);
still_failing.push(pending);
} else {
self.shared_state
.storage
.clear_post_proposal_data(
pending.proposal_id as u32,
OffsetDateTime::now_utc(),
resolution.is_rejected(),
)
.await?;
}
}
Err(err) => {
error!(
"experienced internal error when attempting to resolve pending proposal: {err}"
);
// make sure to update internal state to not lose any data
self.pending_redemptions.push(pending);
self.pending_redemptions.append(&mut still_failing);
return Err(err);
}
}
}
let mut still_failing = Vec::new();
// 2. attempt to verify the remaining tickets
// 1. attempt to verify the remaining tickets
while let Some(mut pending) = self.pending_tickets.pop() {
// possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients
match self
@@ -595,362 +395,14 @@ impl CredentialHandler {
Ok(())
}
/// Attempt to send batch redemption request to the provided ecash verifier.
async fn redeem_tickets(
&self,
proposal_id: u64,
request: &BatchRedeemTicketsBody,
client: &EcashApiClient,
) -> Result<bool, EcashTicketError> {
match client.api_client.batch_redeem_ecash_tickets(request).await {
Ok(res) => {
let accepted = if res.proposal_accepted {
trace!("{client} has accepted proposal {proposal_id}");
true
} else {
warn!("{client} has rejected proposal {proposal_id}");
false
};
Ok(accepted)
}
Err(err) => {
error!(
"failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"
);
Ok(false)
}
}
}
async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> {
self.shared_state
.start_tx()
.await
.execute_proposal(proposal_id, None)
.await
.map_err(
|source| EcashTicketError::RedemptionProposalExecutionFailure {
proposal_id,
source,
},
)?;
Ok(())
}
async fn get_proposal_status(&self, proposal_id: u64) -> Result<Status, EcashTicketError> {
Ok(self
.shared_state
.start_query()
.await
.query_proposal(proposal_id)
.await
.map_err(EcashTicketError::chain_query_failure)?
.status)
}
async fn try_finalize_proposal(
&self,
proposal_id: u64,
) -> Result<ProposalResult, EcashTicketError> {
match self.get_proposal_status(proposal_id).await? {
Status::Pending => {
// the voting hasn't even begun!
error!("impossible case! the proposal {proposal_id} is still pending");
Ok(ProposalResult::Pending)
}
Status::Open => {
debug!("proposal {proposal_id} is still open and needs more votes");
Ok(ProposalResult::Pending)
}
Status::Rejected => {
warn!("proposal {proposal_id} has been rejected");
Ok(ProposalResult::Rejected)
}
Status::Passed => {
info!(
"proposal {proposal_id} has already been passed - we just need to execute it"
);
self.try_execute_proposal(proposal_id).await?;
info!("executed proposal {proposal_id}");
Ok(ProposalResult::Executed)
}
Status::Executed => {
info!("proposal {proposal_id} has already been executed - nothing to do!");
Ok(ProposalResult::Executed)
}
}
}
async fn try_resolve_pending_proposal(
&self,
pending: &mut PendingRedemptionVote,
api_clients: Option<RwLockReadGuard<'_, Vec<EcashApiClient>>>,
) -> Result<ProposalResult, EcashTicketError> {
let proposal_id = pending.proposal_id;
info!(
"attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets",
pending.included_serial_numbers.len()
);
// check if the proposal still needs more votes from the apis
let result = self.try_finalize_proposal(proposal_id).await?;
if !result.is_pending() {
return Ok(result);
}
let api_clients = match api_clients {
Some(clients) => clients,
None => self.shared_state.api_clients(pending.epoch_id).await?,
};
let redemption_request = pending.to_request_body(self.shared_state.address.clone());
// TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted
let total = api_clients.len();
let api_failures = Mutex::new(Vec::new());
let rejected = AtomicUsize::new(0);
for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async {
// errors are only returned on hard, storage, failures
match self
.redeem_tickets(pending.proposal_id, &redemption_request, ecash_client)
.await
{
Err(err) => {
error!("internal failure. could not proceed with ticket redemption: {err}");
api_failures.lock().await.push(ecash_client.node_id);
}
Ok(false) => {
rejected.fetch_add(1, Ordering::SeqCst);
}
_ => {}
}
})
.await;
let api_failures = api_failures.into_inner();
let num_failures = api_failures.len();
pending.pending = api_failures;
let rejected = rejected.into_inner();
let rejected_ratio = rejected as f32 / total as f32;
let rejected_perc = rejected_ratio * 100.;
if rejected_ratio >= (1. - self.multisig_threshold) {
error!(
"{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"
);
// no need to query the chain as with so many rejections it's impossible it has passed.
return Ok(ProposalResult::Rejected);
}
let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32;
let accepted_perc = accepted_ratio * 100.;
match accepted_ratio {
n if n < self.multisig_threshold => {
error!(
"less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"
);
return Ok(ProposalResult::Pending);
}
n if n < self.config.minimum_api_quorum => {
warn!(
"the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"
);
}
_ => {
trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}");
}
}
// attempt to execute the proposal if it reached the required threshold
self.try_finalize_proposal(proposal_id).await
}
async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> {
if !self.pending_tickets.is_empty() {
return Err(EcashTicketError::PendingTickets);
}
let latest_stored = self.shared_state.storage.latest_proposal().await?;
// check if we have already created the proposal but crashed before persisting it in the db
//
// if we have some persisted proposals in storage, try to see if there's anything more recent on chain
// (i.e. the missing proposal)
// if not (i.e. this would have been our first) check the latest page of proposals.
// while this is not ideal, realistically speaking we probably crashed few minutes ago
// and worst case scenario we'll just recreate the proposal instead
//
// LIMITATION: if MULTIPLE proposals got created in between, well. though luck.
let latest_on_chain = if let Some(latest_stored) = &latest_stored {
// those are sorted in ASCENDING way
self.shared_state
.proposals_since(latest_stored.proposal_id as u64)
.await?
.pop()
} else {
// but those are DESCENDING
self.shared_state
.last_proposal_page()
.await?
.first()
.cloned()
};
let now = OffsetDateTime::now_utc();
let prior_proposal = match (&latest_stored, latest_on_chain) {
(None, None) => {
// we haven't created any proposals before
trace!("this could be our first redemption proposal");
None
}
(Some(stored), None) => {
if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now {
trace!("too soon to create new redemption proposal");
return Ok(());
}
None
}
(_, Some(on_chain)) => {
warn!(
"we seem to have crashed after creating proposal, but before persisting it onto disk!"
);
Some(on_chain)
}
};
// technically we could have been just caching all of those serial numbers as we verify tickets,
// but given how infrequently we call this, there's no point in wasting this memory
let verified_tickets = self
.shared_state
.storage
.get_all_verified_tickets_with_sn()
.await?;
// TODO: somehow simplify that nasty nested if
if verified_tickets.len() < self.config.minimum_redemption_tickets {
// bypass the number of tickets check if we're about to lose our rewards due to expiration
if let Some(latest_stored) = latest_stored {
if latest_stored.created_at + self.config.maximum_time_between_redemption < now {
{}
} else {
debug!(
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
verified_tickets.len(),
self.config.minimum_redemption_tickets
);
return Ok(());
}
} else {
// first proposal
debug!(
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
verified_tickets.len(),
self.config.minimum_redemption_tickets
);
return Ok(());
}
}
// this should have been ensured when querying
assert!(verified_tickets.len() <= u16::MAX as usize);
let digest =
BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number));
let encoded_digest = bs58::encode(&digest).into_string();
let prior_proposal_id = if let Some(prior_proposal) = prior_proposal {
if prior_proposal.description == encoded_digest {
info!("we have already created proposal for those tickets");
Some(prior_proposal.id)
} else {
warn!(
"our missed proposal seem to have been for different tickets - abandoning it"
);
None
}
} else {
None
};
// if the proposal has already existed on chain, do use it. otherwise create a new one
let proposal_id = if let Some(prior) = prior_proposal_id {
prior
} else {
self.create_redemption_proposal(&digest, verified_tickets.len() as u16)
.await?
};
if proposal_id > u32::MAX as u64 {
// realistically will we ever reach it? no.
panic!(
"we have created more than {} proposals. we can't handle that.",
u32::MAX
)
}
self.shared_state
.storage
.insert_redemption_proposal(
&verified_tickets,
proposal_id as u32,
OffsetDateTime::now_utc(),
)
.await?;
let current_epoch = self.shared_state.current_epoch_id().await?;
let api_clients = self.shared_state.api_clients(current_epoch).await?;
let ids = api_clients.iter().map(|c| c.node_id).collect();
let mut pending = PendingRedemptionVote::new(
proposal_id,
digest,
verified_tickets
.into_iter()
.map(|t| t.serial_number)
.collect(),
current_epoch,
ids,
);
let resolution = self
.try_resolve_pending_proposal(&mut pending, Some(api_clients))
.await?;
if resolution.is_pending() {
warn!(
"failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later",
pending.pending
);
self.pending_redemptions.push(pending);
} else {
self.shared_state
.storage
.clear_post_proposal_data(
proposal_id as u32,
OffsetDateTime::now_utc(),
resolution.is_rejected(),
)
.await?;
}
Ok(())
}
async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> {
trace!(
"attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"
"attempting to resolve all pending operations -> tickets that are waiting for verification"
);
// 1. retry all operations that have failed in the past: verification requests and pending redemption
// retry the pending verification requests that have failed before
self.resolve_pending().await?;
// 2. if applicable, attempt to redeem all newly verified tickets
self.maybe_redeem_tickets().await?;
Ok(())
}
@@ -7,7 +7,7 @@ use std::future::Future;
use std::ops::Deref;
use tokio::sync::RwLockReadGuard;
pub(crate) fn apis_stream<'a>(
pub fn apis_stream<'a>(
// if needed we could make this argument more generic to accept either locks or iterators, etc.
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
filter_by_id: &'a [u64],
@@ -22,7 +22,7 @@ pub(crate) fn apis_stream<'a>(
)
}
pub(crate) async fn for_each_api_concurrent<'a, F, Fut>(
pub async fn for_each_api_concurrent<'a, F, Fut>(
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
filter_by_id: &'a [u64],
f: F,
@@ -20,7 +20,7 @@ use tracing::error;
pub mod credential_sender;
pub mod error;
mod helpers;
pub mod helpers;
mod state;
pub mod traits;
@@ -3,17 +3,12 @@
use crate::Error;
use crate::ecash::error::EcashTicketError;
use cosmwasm_std::{CosmosMsg, WasmMsg, from_json};
use nym_credentials_interface::VerificationKeyAuth;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::nyxd::contract_traits::{
DkgQueryClient, MultisigQueryClient, NymContractsProvider,
};
use nym_validator_client::nyxd::cw3::ProposalResponse;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient};
use std::collections::BTreeMap;
use std::ops::Deref;
@@ -77,53 +72,6 @@ impl SharedState {
Ok(this)
}
fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool {
let Some(msg) = proposal.msgs.first() else {
return false;
};
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else {
return false;
};
let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_json(msg) else {
return false;
};
gw == self.address.as_ref()
}
/// retrieve all redemption proposals made by this gateway since, but excluding, the provided id
pub(crate) async fn proposals_since(
&self,
proposal_id: u64,
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
Ok(self
.start_query()
.await
.list_proposals(Some(proposal_id), None)
.await
.map_err(EcashTicketError::chain_query_failure)?
.proposals
.into_iter()
.filter(|p| self.created_redemption_proposal(p))
.collect())
}
/// retrieve all redemption proposals made by this gateway that are available on the last page of the query
pub(crate) async fn last_proposal_page(
&self,
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
Ok(self
.start_query()
.await
.reverse_proposals(None, None)
.await
.map_err(EcashTicketError::chain_query_failure)?
.proposals
.into_iter()
.filter(|p| self.created_redemption_proposal(p))
.collect())
}
async fn set_epoch_data(
&self,
epoch_id: EpochId,
@@ -240,24 +188,6 @@ impl SharedState {
data.get(&epoch_id).map(|d| &d.master_key).unwrap()
}))
}
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.write().await
}
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.read().await
}
pub(crate) async fn current_epoch_id(&self) -> Result<EpochId, EcashTicketError> {
Ok(self
.start_query()
.await
.get_current_epoch()
.await
.map_err(EcashTicketError::chain_query_failure)?
.epoch_id)
}
}
pub(crate) struct EpochState {
+5
View File
@@ -4,6 +4,7 @@
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use strum::IntoEnumIterator as _;
use thiserror::Error;
use time::{Date, OffsetDateTime};
@@ -315,6 +316,10 @@ impl TicketType {
_ => Err(UnknownTicketType),
}
}
pub fn exposed_iter() -> impl Iterator<Item = TicketType> {
TicketType::iter()
}
}
impl From<TicketType> for TicketTypeRepr {
+2
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
#[cfg(feature = "asymmetric")]
pub mod asymmetric;
pub mod bech32_address_validation;
+1
View File
@@ -1,5 +1,6 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub use nym_crypto::generic_array;
use nym_crypto::OutputSizeUser;
+1
View File
@@ -34,6 +34,7 @@ tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace=true }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }
+1 -1
View File
@@ -4,7 +4,7 @@ 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_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(92, 39, 63, 14))];
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
+68 -5
View File
@@ -1,6 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
// silences clippy warning: use of deprecated tuple variant `HttpClientError::GenericRequestFailure`: use another more strongly typed variant - this variant is only left for compatibility reasons - TODO
//! Nym HTTP API Client
//!
//! Centralizes and implements the core API client functionality. This crate provides custom,
@@ -158,6 +161,8 @@ use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[cfg(not(target_arch = "wasm32"))]
use std::io::ErrorKind;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use thiserror::Error;
@@ -1164,14 +1169,10 @@ impl ApiClientCore for Client {
match response {
Ok(resp) => return Ok(resp),
Err(err) => {
// only if there was a network issue should we consider updating the host info
//
// note: for now this includes DNS resolution failure, I am not sure how I would go about
// segregating that based on the interface provided by request for errors.
#[cfg(target_arch = "wasm32")]
let is_network_err = err.is_timeout();
#[cfg(not(target_arch = "wasm32"))]
let is_network_err = err.is_timeout() || err.is_connect();
let is_network_err = might_be_network_interference(&err);
if is_network_err {
// if we have multiple urls, update to the next
@@ -1219,6 +1220,68 @@ impl ApiClientCore for Client {
}
}
#[cfg(not(target_arch = "wasm32"))]
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
/// This functions attempts to check the error returned by reqwest to see if
/// rotating host informtion (for clients with mutliple hosts defined) could be
/// helpful. This looks for situations where the error could plausibly be caused
/// by a network adversary, or where rotating to an equival hostname might help.
///
/// For example --> NetworkUnreachable will not be helped by rotating domains,
/// but ConnectionReset might be caused by a network adversary blocking by SNI
/// which could possibly benefit from rotating domains.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
if err.is_timeout() {
return true;
}
if !(err.is_connect() || err.is_request()) {
return false;
}
// The io::Error source is several layers deep, for clarity this is done as a loop
// * reqwest::Error -> hyper_util::Error
// * hyper_util::Error -> hyper_util::ClientError
// * hyper_util::ClientError -> io::Error
let mut inner = err.source();
for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
if let Some(e) = inner {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
// try downcast to io::Error from <dyn std::error:Error>
match io_err.kind() {
// device not connected to the internet
ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
// connection errors can indicate connection interference
ErrorKind::ConnectionReset
| ErrorKind::HostUnreachable
| ErrorKind::ConnectionRefused => return true,
// TLS errors get wrapped in custom io::Errors
ErrorKind::Other | ErrorKind::InvalidData => {
// io::Error get_ref works while source doesn't here -_-
// if you don't like it take it up with the rust devs https://users.rust-lang.org/t/question-about-implementation-of-std-source/121117
inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
}
_ => return false,
}
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
// try downcast to TLS error
return true;
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
// try downcast to DNS error
return resolve_err.is_nx_domain();
} else {
inner = e.source();
}
} else {
break;
}
}
false
}
/// Common usage functionality for the http client.
///
/// These functions allow for cleaner downstream usage free of type parameters and unneeded imports.
+6
View File
@@ -10,7 +10,11 @@ license.workspace = true
description = "Codec, signing functionality, and different version definitions for IP packet request and responses"
[features]
test-utils = ["pnet_packet"]
[dependencies]
pnet_packet = { workspace = true, optional = true }
bincode = { workspace = true }
bytes = { workspace = true }
nym-bin-common = { workspace = true }
@@ -18,8 +22,10 @@ nym-crypto = { workspace = true }
nym-service-provider-requests-common = { workspace = true }
nym-sphinx = { workspace = true }
rand = { workspace = true }
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tokio-util = { workspace = true, features = ["codec"] }
tracing = { workspace = true }
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// Extracted from sdk/rust/nym-sdk/examples/ipr_tunnel.rs
//! ICMP/ICMPv6 packet construction and reply detection helpers for testing
//! IPR connectivity. Gated behind the `test-utils` feature.
use std::net::{Ipv4Addr, Ipv6Addr};
use pnet_packet::Packet;
use pnet_packet::icmp::echo_reply::EchoReplyPacket;
use pnet_packet::icmp::echo_request::MutableEchoRequestPacket;
use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
use pnet_packet::icmpv6::Icmpv6Types;
use pnet_packet::ipv4::{Ipv4Flags, MutableIpv4Packet};
use pnet_packet::ipv6::MutableIpv6Packet;
/// Build a complete IPv4 ICMP echo request packet.
pub fn build_icmp_ping(src: Ipv4Addr, dst: Ipv4Addr, seq: u16) -> Option<Vec<u8>> {
let mut echo = MutableEchoRequestPacket::owned(vec![0u8; 64])?;
echo.set_icmp_type(IcmpTypes::EchoRequest);
echo.set_icmp_code(pnet_packet::icmp::IcmpCode::new(0));
echo.set_sequence_number(seq);
let cksum = pnet_packet::icmp::checksum(&IcmpPacket::new(echo.packet())?);
echo.set_checksum(cksum);
let total_len = 20 + echo.packet().len();
let mut ip = MutableIpv4Packet::owned(vec![0u8; total_len])?;
ip.set_version(4);
ip.set_header_length(5);
ip.set_total_length(total_len as u16);
ip.set_ttl(64);
ip.set_next_level_protocol(pnet_packet::ip::IpNextHeaderProtocols::Icmp);
ip.set_source(src);
ip.set_destination(dst);
ip.set_flags(Ipv4Flags::DontFragment);
ip.set_payload(echo.packet());
let mut buf = ip.consume_to_immutable().packet().to_vec();
let cksum = ipv4_checksum(&buf);
buf[10] = (cksum >> 8) as u8;
buf[11] = cksum as u8;
Some(buf)
}
/// Build a complete IPv6 ICMPv6 echo request packet.
pub fn build_icmpv6_ping(src: Ipv6Addr, dst: Ipv6Addr, seq: u16) -> Option<Vec<u8>> {
let mut echo =
pnet_packet::icmpv6::echo_request::MutableEchoRequestPacket::owned(vec![0u8; 64])?;
echo.set_icmpv6_type(Icmpv6Types::EchoRequest);
echo.set_icmpv6_code(pnet_packet::icmpv6::Icmpv6Code::new(0));
echo.set_sequence_number(seq);
let cksum = pnet_packet::icmpv6::checksum(
&pnet_packet::icmpv6::Icmpv6Packet::new(echo.packet())?,
&src,
&dst,
);
echo.set_checksum(cksum);
let payload_len = echo.packet().len();
let mut ip = MutableIpv6Packet::owned(vec![0u8; 40 + payload_len])?;
ip.set_version(6);
ip.set_payload_length(payload_len as u16);
ip.set_next_header(pnet_packet::ip::IpNextHeaderProtocols::Icmpv6);
ip.set_hop_limit(64);
ip.set_source(src);
ip.set_destination(dst);
ip.set_payload(echo.packet());
Some(ip.consume_to_immutable().packet().to_vec())
}
/// Check if a raw packet is an IPv4 ICMP echo reply destined to `expected_dst`.
pub fn is_echo_reply_v4(data: &[u8], expected_dst: Ipv4Addr) -> bool {
let Some(ip) = pnet_packet::ipv4::Ipv4Packet::new(data) else {
return false;
};
if ip.get_destination() != expected_dst {
return false;
}
if ip.get_next_level_protocol() != pnet_packet::ip::IpNextHeaderProtocols::Icmp {
return false;
}
let Some(reply) = EchoReplyPacket::new(ip.payload()) else {
return false;
};
reply.get_icmp_type() == IcmpTypes::EchoReply
}
/// Check if a raw packet is an IPv6 ICMPv6 echo reply destined to `expected_dst`.
pub fn is_echo_reply_v6(data: &[u8], expected_dst: Ipv6Addr) -> bool {
let Some(ip) = pnet_packet::ipv6::Ipv6Packet::new(data) else {
return false;
};
if ip.get_destination() != expected_dst {
return false;
}
if ip.get_next_header() != pnet_packet::ip::IpNextHeaderProtocols::Icmpv6 {
return false;
}
let Some(reply) = pnet_packet::icmpv6::echo_reply::EchoReplyPacket::new(ip.payload()) else {
return false;
};
reply.get_icmpv6_type() == Icmpv6Types::EchoReply
}
fn ipv4_checksum(header: &[u8]) -> u16 {
let mut sum = 0u32;
for i in (0..20).step_by(2) {
sum += ((header[i] as u32) << 8) | header[i + 1] as u32;
}
while (sum >> 16) > 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
+6
View File
@@ -3,10 +3,14 @@ use std::fmt::{Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
pub mod codec;
#[cfg(feature = "test-utils")]
pub mod icmp_utils;
pub mod response_helpers;
pub mod sign;
pub mod v6;
pub mod v7;
pub mod v8;
pub mod v9;
// version 3: initial version
// version 4: IPv6 support
@@ -14,6 +18,8 @@ pub mod v8;
// version 6: Increase the available IPs
// version 7: Add signature support (for the future)
// version 8: Anonymous sends
// version 9: LP-framed transport (SphinxStream)
// response_helpers: shared IPR response parsing (nym-ip-packet-client + nym-sdk)
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
@@ -0,0 +1,134 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bytes::{Bytes, BytesMut};
use tokio_util::codec::Decoder;
use tracing::{error, info, warn};
use crate::{
IpPair,
codec::MultiIpPacketCodec,
v8::response::{
ConnectResponseReply, ControlResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
},
};
#[derive(Debug, thiserror::Error)]
pub enum IprResponseError {
#[error("no version byte in message")]
NoVersionByte,
#[error("version mismatch: received v{received}, expected v{expected}")]
VersionMismatch { expected: u8, received: u8 },
#[error("expected control response, got {0:?}")]
UnexpectedResponse(IpPacketResponseData),
#[error("connect denied: {0:?}")]
ConnectDenied(crate::v8::response::ConnectFailureReason),
}
pub enum MixnetMessageOutcome {
IpPackets(Vec<Bytes>),
Disconnect,
}
// Extracted from:
// nym-ip-packet-client/src/helpers.rs — check_ipr_message_version()
// sdk/rust/nym-sdk/src/ip_packet_client/listener.rs — check_ipr_message_version()
/// Check that the first byte of an IPR message matches the expected protocol version.
pub fn check_ipr_message_version(data: &[u8], expected: u8) -> Result<(), IprResponseError> {
let version = data.first().ok_or(IprResponseError::NoVersionByte)?;
if *version != expected {
return Err(IprResponseError::VersionMismatch {
expected,
received: *version,
});
}
Ok(())
}
// Extracted from:
// nym-ip-packet-client/src/connect.rs — handle_connect_response() + handle_ip_packet_router_response()
// sdk/rust/nym-sdk/src/ip_packet_client/discovery.rs — parse_connect_response()
/// Parse an IPR connect response, returning allocated IPs on success.
pub fn parse_connect_response(response: IpPacketResponse) -> Result<IpPair, IprResponseError> {
let control_response = match response.data {
IpPacketResponseData::Control(c) => c,
other => return Err(IprResponseError::UnexpectedResponse(other)),
};
match *control_response {
ControlResponse::Connect(connect_resp) => match connect_resp.reply {
ConnectResponseReply::Success(success) => Ok(success.ips),
ConnectResponseReply::Failure(reason) => Err(IprResponseError::ConnectDenied(reason)),
},
_ => Err(IprResponseError::UnexpectedResponse(
IpPacketResponseData::Control(control_response),
)),
}
}
// Extracted from:
// nym-ip-packet-client/src/listener.rs — IprListener::handle_reconstructed_message()
// sdk/rust/nym-sdk/src/ip_packet_client/listener.rs — handle_ipr_response()
/// Parse raw IPR response bytes into an outcome.
///
/// Logs non-fatal conditions (unknown control messages, deserialization
/// failures) and returns `None` for them.
pub fn handle_ipr_response(data: &[u8]) -> Option<MixnetMessageOutcome> {
match IpPacketResponse::from_bytes(data) {
Ok(response) => match response.data {
IpPacketResponseData::Data(data_response) => {
let mut codec = MultiIpPacketCodec::new();
let mut buf = BytesMut::from(data_response.ip_packet.as_ref());
let mut packets = Vec::new();
loop {
match codec.decode(&mut buf) {
Ok(Some(packet)) => packets.push(packet.into_bytes()),
Ok(None) => break,
Err(e) => {
warn!("Failed to decode bundled IP packet: {e}");
break;
}
}
}
Some(MixnetMessageOutcome::IpPackets(packets))
}
IpPacketResponseData::Control(control_response) => match *control_response {
ControlResponse::Connect(_) => {
info!("Received connect response when already connected - ignoring");
None
}
ControlResponse::Disconnect(_) | ControlResponse::UnrequestedDisconnect(_) => {
info!("Received disconnect from IPR");
Some(MixnetMessageOutcome::Disconnect)
}
ControlResponse::Pong(_) => {
info!("Received pong response");
None
}
ControlResponse::Health(_) => {
info!("Received health response");
None
}
ControlResponse::Info(info_resp) => {
let msg = format!(
"Received info response from the mixnet: {}",
info_resp.reply
);
match info_resp.level {
InfoLevel::Info => info!("{msg}"),
InfoLevel::Warn => warn!("{msg}"),
InfoLevel::Error => error!("{msg}"),
}
None
}
},
},
Err(err) => {
warn!("Failed to deserialize IPR response: {err}");
None
}
}
}
+6 -2
View File
@@ -179,11 +179,15 @@ impl IpPacketResponse {
make_bincode_serializer().serialize(self)
}
pub fn from_bytes(data: &[u8]) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(data)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
Self::from_bytes(&message.message)
}
}
+34
View File
@@ -0,0 +1,34 @@
pub const VERSION: u8 = 9;
/// Minimum nym-node release version that supports v9 (LP Stream framing).
/// Nodes running older versions will not understand LP-wrapped packets.
pub const MIN_RELEASE_VERSION: semver::Version = semver::Version::new(1, 30, 0);
// v9 uses the same wire format as v8. The version bump indicates
// the message was sent with LP framing (SphinxStream).
//
// Types are re-exported for deserialization/matching. Use the wrapper
// constructors below to create correctly-versioned packets — never
// manually set `protocol.version` or `response.version`.
pub use super::v8::{request, response};
/// Create a v9 connect request (version byte set to 9).
pub fn new_connect_request(buffer_timeout: Option<u64>) -> (request::IpPacketRequest, u64) {
let (mut req, id) = request::IpPacketRequest::new_connect_request(buffer_timeout);
req.protocol.version = VERSION;
(req, id)
}
/// Create a v9 data request (version byte set to 9).
pub fn new_data_request(data: bytes::Bytes) -> request::IpPacketRequest {
let mut req = request::IpPacketRequest::new_data_request(data);
req.protocol.version = VERSION;
req
}
/// Create a v9 IP packet response (version byte set to 9).
pub fn new_ip_packet_response(ip_packet: bytes::Bytes) -> response::IpPacketResponse {
let mut resp = response::IpPacketResponse::new_ip_packet(ip_packet);
resp.version = VERSION;
resp
}
+11 -5
View File
@@ -22,6 +22,16 @@ pub struct ChainDetails {
pub stake_denom: DenomDetailsOwned,
}
impl ChainDetails {
pub fn mainnet() -> Self {
ChainDetails {
bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
mix_denom: mainnet::MIX_DENOM.into(),
stake_denom: mainnet::STAKE_DENOM.into(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymContracts {
@@ -181,11 +191,7 @@ impl NymNetworkDetails {
// Consider caching this process (lazy static)
NymNetworkDetails {
network_name: mainnet::NETWORK_NAME.into(),
chain_details: ChainDetails {
bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
mix_denom: mainnet::MIX_DENOM.into(),
stake_denom: mainnet::STAKE_DENOM.into(),
},
chain_details: ChainDetails::mainnet(),
endpoints: mainnet::validators(),
contracts: NymContracts {
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),

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