Merge pull request #5551 from nymtech/release/2025.4-dorina
Merge release/2025.4-dorina to master
This commit is contained in:
@@ -54,6 +54,20 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
# To avoid running out of disk space, skip generating debug symbols
|
||||
- name: Set debug to false (unix)
|
||||
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
|
||||
run: |
|
||||
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: Set debug to false (win)
|
||||
if: contains(matrix.os, 'windows')
|
||||
shell: pwsh
|
||||
run: |
|
||||
(Get-Content Cargo.toml) -replace '\[profile.dev\]', "`$&`ndebug = false" | Set-Content Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
paths:
|
||||
- 'contracts/**'
|
||||
- 'common/**'
|
||||
- 'Cargo.lock'
|
||||
- 'Cargo.toml'
|
||||
- '.github/workflows/ci-contracts.yml'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -30,6 +30,12 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Set debug to false
|
||||
working-directory: nym-wallet
|
||||
run: |
|
||||
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
|
||||
@@ -45,6 +45,11 @@ jobs:
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli
|
||||
|
||||
- name: Set debug to false
|
||||
run: |
|
||||
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: "Build"
|
||||
run: make sdk-wasm-build
|
||||
|
||||
|
||||
@@ -4,6 +4,96 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.4-dorina] (2025-03-04)
|
||||
|
||||
- fixed sphinx version metrics registration ([#5546])
|
||||
- Feature/chain status api ([#5539])
|
||||
- Add SURBs soft threshold ([#5535])
|
||||
- Simplify IPR v8 ([#5532])
|
||||
- Shared instance for DNS AsyncResolver ([#5523])
|
||||
- merge #5512 again after reverting due to incorrect rebase ([#5520])
|
||||
- cherry-pick 17d3ff2d775f61aee381d90a304ed416c08f33fc onto dorina ([#5519])
|
||||
- cherry-pick 6e5d0dac1b75413c5f09122b0d953f8ec6ef48df onto dorina ([#5518])
|
||||
- chore: workspace global panic preventing lints ([#5512])
|
||||
- bugfix: dont query for ecash apis unless necessary when spending ticketbooks ([#5508])
|
||||
- bugfix: bound check when recovering a reply SURB ([#5502])
|
||||
- chore: removed all old coconut code ([#5500])
|
||||
- IPR request types v8 ([#5498])
|
||||
- Support static routes for HTTP requests ([#5487])
|
||||
- build(deps): bump the patch-updates group across 1 directory with 3 updates ([#5482])
|
||||
- added missing import to doctest ([#5480])
|
||||
- adjusted TestSetup::new_complex to ensure bonded node's existence ([#5478])
|
||||
- Trigger contracts CI on main workspace Cargo changes ([#5477])
|
||||
- build(deps): bump http from 1.1.0 to 1.2.0 ([#5472])
|
||||
- build(deps): bump utoipa-swagger-ui from 8.0.3 to 8.1.0 ([#5471])
|
||||
- build(deps): bump colored from 2.1.0 to 2.2.0 ([#5470])
|
||||
- build(deps): bump celes from 2.4.0 to 2.5.0 ([#5469])
|
||||
- build(deps): bump the patch-updates group with 2 updates ([#5467])
|
||||
- build(deps): bump elliptic from 6.5.4 to 6.6.1 in /docker/typescript_client/upload_contract ([#5463])
|
||||
- Run cargo autoinherit ([#5460])
|
||||
- Fix clippy::precedence ([#5457])
|
||||
- Provide Interval context with node descriptor endpoints ([#5456])
|
||||
- fix: update fx average rate calcs to ignore 0 values ([#5454])
|
||||
- Feature/add gbp currency ([#5453])
|
||||
- Add helper to extract a list of sqlite files with journal files wal/shm ([#5452])
|
||||
- Add a middleware layer to the nym api allowing for data compression ([#5451])
|
||||
- Condense core API functionalities and enable gzip decompression for reqwest payloads ([#5450])
|
||||
- build(deps): bump uniffi_build from 0.25.3 to 0.29.0 ([#5448])
|
||||
- Upgrade tower to 0.5.2 ([#5446])
|
||||
- build(deps): bump hickory-proto from 0.24.2 to 0.24.3 ([#5444])
|
||||
- Seedable clients ([#5440])
|
||||
- build(deps): bump the patch-updates group across 1 directory with 10 updates ([#5439])
|
||||
- Remove all recv_with_delay and add shutdown condition to loops in client-core ([#5435])
|
||||
- Disable the test for checking the remaining bandwidth in nym-node-status-api ([#5425])
|
||||
- Dz nym node stats ([#5418])
|
||||
- build(deps): bump hyper from 1.4.1 to 1.6.0 ([#5416])
|
||||
- build(deps): bump publicsuffix from 2.2.3 to 2.3.0 ([#5367])
|
||||
- Nymnode entrypoint docker ([#5300])
|
||||
|
||||
[#5546]: https://github.com/nymtech/nym/pull/5546
|
||||
[#5539]: https://github.com/nymtech/nym/pull/5539
|
||||
[#5535]: https://github.com/nymtech/nym/pull/5535
|
||||
[#5532]: https://github.com/nymtech/nym/pull/5532
|
||||
[#5523]: https://github.com/nymtech/nym/pull/5523
|
||||
[#5520]: https://github.com/nymtech/nym/pull/5520
|
||||
[#5519]: https://github.com/nymtech/nym/pull/5519
|
||||
[#5518]: https://github.com/nymtech/nym/pull/5518
|
||||
[#5512]: https://github.com/nymtech/nym/pull/5512
|
||||
[#5508]: https://github.com/nymtech/nym/pull/5508
|
||||
[#5502]: https://github.com/nymtech/nym/pull/5502
|
||||
[#5500]: https://github.com/nymtech/nym/pull/5500
|
||||
[#5498]: https://github.com/nymtech/nym/pull/5498
|
||||
[#5487]: https://github.com/nymtech/nym/pull/5487
|
||||
[#5482]: https://github.com/nymtech/nym/pull/5482
|
||||
[#5480]: https://github.com/nymtech/nym/pull/5480
|
||||
[#5478]: https://github.com/nymtech/nym/pull/5478
|
||||
[#5477]: https://github.com/nymtech/nym/pull/5477
|
||||
[#5472]: https://github.com/nymtech/nym/pull/5472
|
||||
[#5471]: https://github.com/nymtech/nym/pull/5471
|
||||
[#5470]: https://github.com/nymtech/nym/pull/5470
|
||||
[#5469]: https://github.com/nymtech/nym/pull/5469
|
||||
[#5467]: https://github.com/nymtech/nym/pull/5467
|
||||
[#5463]: https://github.com/nymtech/nym/pull/5463
|
||||
[#5460]: https://github.com/nymtech/nym/pull/5460
|
||||
[#5457]: https://github.com/nymtech/nym/pull/5457
|
||||
[#5456]: https://github.com/nymtech/nym/pull/5456
|
||||
[#5454]: https://github.com/nymtech/nym/pull/5454
|
||||
[#5453]: https://github.com/nymtech/nym/pull/5453
|
||||
[#5452]: https://github.com/nymtech/nym/pull/5452
|
||||
[#5451]: https://github.com/nymtech/nym/pull/5451
|
||||
[#5450]: https://github.com/nymtech/nym/pull/5450
|
||||
[#5448]: https://github.com/nymtech/nym/pull/5448
|
||||
[#5446]: https://github.com/nymtech/nym/pull/5446
|
||||
[#5444]: https://github.com/nymtech/nym/pull/5444
|
||||
[#5440]: https://github.com/nymtech/nym/pull/5440
|
||||
[#5439]: https://github.com/nymtech/nym/pull/5439
|
||||
[#5435]: https://github.com/nymtech/nym/pull/5435
|
||||
[#5425]: https://github.com/nymtech/nym/pull/5425
|
||||
[#5418]: https://github.com/nymtech/nym/pull/5418
|
||||
[#5416]: https://github.com/nymtech/nym/pull/5416
|
||||
[#5367]: https://github.com/nymtech/nym/pull/5367
|
||||
[#5300]: https://github.com/nymtech/nym/pull/5300
|
||||
|
||||
## [2025.3-ruta] (2025-02-10)
|
||||
|
||||
- Push down forget me to client configs ([#5431])
|
||||
|
||||
Generated
+526
-382
File diff suppressed because it is too large
Load Diff
+39
-23
@@ -66,7 +66,6 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymcoconut",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
@@ -187,18 +186,21 @@ readme = "README.md"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
aead = "0.5.2"
|
||||
aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
aead = "0.5.2"
|
||||
ammonia = "4"
|
||||
anyhow = "1.0.95"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.85"
|
||||
axum-client-ip = "0.6.1"
|
||||
async-trait = "0.1.86"
|
||||
axum = "0.7.5"
|
||||
axum-client-ip = "0.6.1"
|
||||
axum-extra = "0.9.4"
|
||||
axum-test = "16.2.0"
|
||||
base64 = "0.22.1"
|
||||
base85rs = "0.1.3"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bit-vec = "0.7.0" # can we unify those?
|
||||
@@ -209,17 +211,17 @@ bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.7.2"
|
||||
cargo_metadata = "0.18.1"
|
||||
celes = "2.4.0"
|
||||
celes = "2.5.0"
|
||||
cfg-if = "1.0.0"
|
||||
chacha20 = "0.9.0"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
chrono = "0.4.39"
|
||||
cipher = "0.4.3"
|
||||
clap = "4.5.27"
|
||||
clap = "4.5.30"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
colored = "2.0"
|
||||
comfy-table = "7.1.3"
|
||||
colored = "2.2"
|
||||
comfy-table = "7.1.4"
|
||||
console = "0.15.10"
|
||||
console-subscriber = "0.1.1"
|
||||
console_error_panic_hook = "0.1"
|
||||
@@ -239,8 +241,9 @@ doc-comment = "0.3"
|
||||
dotenvy = "0.15.6"
|
||||
ecdsa = "0.16"
|
||||
ed25519-dalek = "2.1"
|
||||
etherparse = "0.13.0"
|
||||
env_logger = "0.11.6"
|
||||
envy = "0.4"
|
||||
etherparse = "0.13.0"
|
||||
eyre = "0.6.9"
|
||||
fastrand = "2.1.1"
|
||||
flate2 = "1.0.35"
|
||||
@@ -253,16 +256,16 @@ handlebars = "3.5.5"
|
||||
headers = "0.4.0"
|
||||
hex = "0.4.3"
|
||||
hex-literal = "0.3.3"
|
||||
hickory-resolver = "0.24.2"
|
||||
hickory-resolver = "0.24.4"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
http-body-util = "0.1"
|
||||
httpcodec = "0.2.3"
|
||||
human-repr = "1.1.0"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
human-repr = "1.1.0"
|
||||
hyper = "1.4.1"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1"
|
||||
indicatif = "0.17.11"
|
||||
inquire = "0.6.2"
|
||||
@@ -276,13 +279,12 @@ ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
log = "0.4"
|
||||
maxminddb = "0.23.0"
|
||||
rs_merkle = "1.4.2"
|
||||
mime = "0.3.17"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
nix = "0.27.1"
|
||||
notify = "5.1.0"
|
||||
okapi = "0.7.0"
|
||||
once_cell = "1.20.2"
|
||||
once_cell = "1.20.3"
|
||||
opentelemetry = "0.19.0"
|
||||
opentelemetry-jaeger = "0.18.0"
|
||||
parking_lot = "0.12.3"
|
||||
@@ -291,7 +293,7 @@ petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
pretty_env_logger = "0.4.0"
|
||||
publicsuffix = "2.2.3"
|
||||
publicsuffix = "2.3.0"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
@@ -305,6 +307,7 @@ reqwest = { version = "0.12.4", default-features = false }
|
||||
rocket = "0.5.0"
|
||||
rocket_cors = "0.6.0"
|
||||
rocket_okapi = "0.8.0"
|
||||
rs_merkle = "1.4.2"
|
||||
safer-ffi = "0.1.13"
|
||||
schemars = "0.8.21"
|
||||
semver = "1.0.25"
|
||||
@@ -312,13 +315,13 @@ serde = "1.0.217"
|
||||
serde_bytes = "0.11.15"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0.138"
|
||||
serde_json_path = "0.7.1"
|
||||
serde_json_path = "0.7.2"
|
||||
serde_repr = "0.1"
|
||||
serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.8"
|
||||
si-scale = "0.2.3"
|
||||
sphinx-packet = "0.1.1"
|
||||
sphinx-packet = "0.3.1"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
@@ -326,29 +329,32 @@ subtle-encoding = "0.5"
|
||||
syn = "1"
|
||||
sysinfo = "0.33.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.43"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.15"
|
||||
thiserror = "2.0"
|
||||
time = "0.3.37"
|
||||
tokio = "1.43"
|
||||
tokio-postgres = "0.7"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-test = "0.4.4"
|
||||
tokio-tun = "0.11.5"
|
||||
tokio-tungstenite = { version = "0.20.1" }
|
||||
tokio-util = "0.7.13"
|
||||
toml = "0.8.19"
|
||||
tower = "0.4.13"
|
||||
toml = "0.8.20"
|
||||
tower = "0.5.2"
|
||||
tower-http = "0.5.2"
|
||||
tracing = "0.1.41"
|
||||
tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-tree = "0.2.2"
|
||||
tracing-log = "0.2"
|
||||
ts-rs = "10.1.0"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
uniffi = "0.29.0"
|
||||
uniffi_build = "0.29.0"
|
||||
url = "2.5"
|
||||
utoipa = "5.2"
|
||||
utoipa-swagger-ui = "8.0"
|
||||
utoipa-swagger-ui = "8.1"
|
||||
utoipauto = "0.2"
|
||||
uuid = "*"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
@@ -387,7 +393,7 @@ cw-controllers = { version = "=1.1.0" }
|
||||
bip32 = { version = "0.5.3", default-features = false }
|
||||
|
||||
|
||||
cosmrs = { version = "0.21.0" }
|
||||
cosmrs = { version = "0.21.1" }
|
||||
tendermint = "0.40.0"
|
||||
tendermint-rpc = "0.40.0"
|
||||
prost = { version = "0.13", default-features = false }
|
||||
@@ -431,3 +437,13 @@ opt-level = 'z'
|
||||
[profile.release.package.mix-fetch-wasm]
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
expect_used = "deny"
|
||||
todo = "deny"
|
||||
dbg_macro = "deny"
|
||||
exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.48"
|
||||
version = "1.1.49"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -49,7 +49,7 @@ impl SocketClient {
|
||||
client_output: ClientOutput,
|
||||
client_state: ClientState,
|
||||
self_address: &Recipient,
|
||||
shutdown: nym_task::TaskClient,
|
||||
task_client: nym_task::TaskClient,
|
||||
packet_type: PacketType,
|
||||
) {
|
||||
info!("Starting websocket listener...");
|
||||
@@ -77,10 +77,15 @@ impl SocketClient {
|
||||
shared_lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
Some(packet_type),
|
||||
task_client.fork("websocket_handler"),
|
||||
);
|
||||
|
||||
websocket::Listener::new(config.socket.host, config.socket.listening_port)
|
||||
.start(websocket_handler, shutdown);
|
||||
websocket::Listener::new(
|
||||
config.socket.host,
|
||||
config.socket.listening_port,
|
||||
task_client.with_suffix("websocket_listener"),
|
||||
)
|
||||
.start(websocket_handler);
|
||||
}
|
||||
|
||||
/// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent)
|
||||
|
||||
@@ -19,6 +19,7 @@ use nym_sphinx::receiver::ReconstructedMessage;
|
||||
use nym_task::connections::{
|
||||
ConnectionCommand, ConnectionCommandSender, ConnectionId, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use nym_task::TaskClient;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
@@ -43,9 +44,11 @@ pub(crate) struct HandlerBuilder {
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
packet_type: Option<PacketType>,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl HandlerBuilder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
msg_input: InputMessageSender,
|
||||
client_connection_tx: ConnectionCommandSender,
|
||||
@@ -54,6 +57,7 @@ impl HandlerBuilder {
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
packet_type: Option<PacketType>,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
Self {
|
||||
msg_input,
|
||||
@@ -63,11 +67,14 @@ impl HandlerBuilder {
|
||||
lane_queue_lengths,
|
||||
reply_controller_sender,
|
||||
packet_type,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make sure we only ever have one active handler
|
||||
pub fn create_active_handler(&self) -> Handler {
|
||||
let mut task_client = self.task_client.fork("active_handler");
|
||||
task_client.disarm();
|
||||
Handler {
|
||||
msg_input: self.msg_input.clone(),
|
||||
client_connection_tx: self.client_connection_tx.clone(),
|
||||
@@ -78,6 +85,7 @@ impl HandlerBuilder {
|
||||
lane_queue_lengths: self.lane_queue_lengths.clone(),
|
||||
reply_controller_sender: self.reply_controller_sender.clone(),
|
||||
packet_type: self.packet_type,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,16 +100,18 @@ pub(crate) struct Handler {
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
packet_type: Option<PacketType>,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl Drop for Handler {
|
||||
fn drop(&mut self) {
|
||||
if self
|
||||
if let Err(err) = self
|
||||
.buffer_requester
|
||||
.unbounded_send(ReceivedBufferMessage::ReceiverDisconnect)
|
||||
.is_err()
|
||||
{
|
||||
error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!")
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("failed to disconnect the receiver from the buffer: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,10 +135,23 @@ impl Handler {
|
||||
};
|
||||
|
||||
// get the number of pending replies waiting for reply surbs
|
||||
let reply_queue_length = self
|
||||
let reply_queue_length = match self
|
||||
.reply_controller_sender
|
||||
.get_lane_queue_length(connection_id)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(length) => length,
|
||||
Err(err) => {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!(
|
||||
"Failed to get reply queue length for connection {connection_id}: {err}"
|
||||
);
|
||||
}
|
||||
// We're just going to assume that the queue is empty, and I think that's okay
|
||||
// during shutdown.
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let queue_length = base_length + reply_queue_length;
|
||||
|
||||
@@ -168,10 +191,11 @@ impl Handler {
|
||||
|
||||
// the ack control is now responsible for chunking, etc.
|
||||
let input_msg = InputMessage::new_regular(recipient, message, lane, self.packet_type);
|
||||
self.msg_input
|
||||
.send(input_msg)
|
||||
.await
|
||||
.expect("InputMessageReceiver has stopped receiving!");
|
||||
if let Err(err) = self.msg_input.send(input_msg).await {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send message to the input buffer: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
|
||||
let TransmissionLane::ConnectionId(connection_id) = lane else {
|
||||
@@ -200,10 +224,11 @@ impl Handler {
|
||||
|
||||
let input_msg =
|
||||
InputMessage::new_anonymous(recipient, message, reply_surbs, lane, self.packet_type);
|
||||
self.msg_input
|
||||
.send(input_msg)
|
||||
.await
|
||||
.expect("InputMessageReceiver has stopped receiving!");
|
||||
if let Err(err) = self.msg_input.send(input_msg).await {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send anonymous message to the input buffer: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
|
||||
let TransmissionLane::ConnectionId(connection_id) = lane else {
|
||||
@@ -227,10 +252,11 @@ impl Handler {
|
||||
});
|
||||
|
||||
let input_msg = InputMessage::new_reply(recipient_tag, message, lane, self.packet_type);
|
||||
self.msg_input
|
||||
.send(input_msg)
|
||||
.await
|
||||
.expect("InputMessageReceiver has stopped receiving!");
|
||||
if let Err(err) = self.msg_input.send(input_msg).await {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send reply message to the input buffer: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
|
||||
let TransmissionLane::ConnectionId(connection_id) = lane else {
|
||||
@@ -245,9 +271,14 @@ impl Handler {
|
||||
}
|
||||
|
||||
fn handle_closed_connection(&self, connection_id: u64) -> Option<ServerResponse> {
|
||||
self.client_connection_tx
|
||||
if let Err(err) = self
|
||||
.client_connection_tx
|
||||
.unbounded_send(ConnectionCommand::Close(connection_id))
|
||||
.unwrap();
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send close connection command: {err}");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -362,11 +393,10 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_for_requests(
|
||||
&mut self,
|
||||
mut msg_receiver: ReconstructedMessagesReceiver,
|
||||
mut task_client: nym_task::TaskClient,
|
||||
) {
|
||||
async fn listen_for_requests(&mut self, mut msg_receiver: ReconstructedMessagesReceiver) {
|
||||
let mut task_client = self.task_client.fork("select");
|
||||
task_client.disarm();
|
||||
|
||||
while !task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
// we can either get a client request from the websocket
|
||||
@@ -415,15 +445,7 @@ impl Handler {
|
||||
}
|
||||
|
||||
// consume self to make sure `drop` is called after this is done
|
||||
pub(crate) async fn handle_connection(
|
||||
mut self,
|
||||
socket: TcpStream,
|
||||
mut task_client: nym_task::TaskClient,
|
||||
) {
|
||||
// We don't want a crash in the connection handler to trigger a shutdown of the whole
|
||||
// process.
|
||||
task_client.disarm();
|
||||
|
||||
pub(crate) async fn handle_connection(mut self, socket: TcpStream) {
|
||||
let ws_stream = match accept_async(socket).await {
|
||||
Ok(ws_stream) => ws_stream,
|
||||
Err(err) => {
|
||||
@@ -436,14 +458,18 @@ impl Handler {
|
||||
let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded();
|
||||
|
||||
// tell the buffer to start sending stuff to us
|
||||
self.buffer_requester
|
||||
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
|
||||
reconstructed_sender,
|
||||
))
|
||||
.expect("the buffer request failed!");
|
||||
if let Err(err) =
|
||||
self.buffer_requester
|
||||
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
|
||||
reconstructed_sender,
|
||||
))
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("failed to announce the receiver to the buffer: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
self.listen_for_requests(reconstructed_receiver, task_client)
|
||||
.await;
|
||||
self.listen_for_requests(reconstructed_receiver).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use super::handler::HandlerBuilder;
|
||||
use log::*;
|
||||
use nym_task::TaskClient;
|
||||
use std::net::IpAddr;
|
||||
use std::{net::SocketAddr, process, sync::Arc};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -22,21 +23,19 @@ impl State {
|
||||
pub(crate) struct Listener {
|
||||
address: SocketAddr,
|
||||
state: State,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
pub(crate) fn new(host: IpAddr, port: u16) -> Self {
|
||||
pub(crate) fn new(host: IpAddr, port: u16, task_client: TaskClient) -> Self {
|
||||
Listener {
|
||||
address: SocketAddr::new(host, port),
|
||||
state: State::AwaitingConnection,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
&mut self,
|
||||
handler: HandlerBuilder,
|
||||
mut task_client: nym_task::TaskClient,
|
||||
) {
|
||||
pub(crate) async fn run(&mut self, handler: HandlerBuilder) {
|
||||
let tcp_listener = match tokio::net::TcpListener::bind(self.address).await {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
@@ -47,11 +46,11 @@ impl Listener {
|
||||
|
||||
let notify = Arc::new(Notify::new());
|
||||
|
||||
loop {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
// When the handler finishes we check if shutdown is signalled
|
||||
_ = notify.notified() => {
|
||||
if task_client.is_shutdown() {
|
||||
if self.task_client.is_shutdown() {
|
||||
log::trace!("Websocket listener: detected shutdown after connection closed");
|
||||
break;
|
||||
}
|
||||
@@ -60,7 +59,7 @@ impl Listener {
|
||||
}
|
||||
// ... but when there is no connected client at the time of shutdown being
|
||||
// signalled, we handle it here.
|
||||
_ = task_client.recv() => {
|
||||
_ = self.task_client.recv() => {
|
||||
if !self.state.is_connected() {
|
||||
log::trace!("Not connected: shutting down");
|
||||
break;
|
||||
@@ -88,9 +87,8 @@ impl Listener {
|
||||
// hanging because the executor doesn't come back here
|
||||
let notify_clone = Arc::clone(¬ify);
|
||||
let fresh_handler = handler.create_active_handler();
|
||||
let task_client_handler = task_client.clone();
|
||||
tokio::spawn(async move {
|
||||
fresh_handler.handle_connection(socket, task_client_handler).await;
|
||||
fresh_handler.handle_connection(socket).await;
|
||||
notify_clone.notify_one();
|
||||
});
|
||||
self.state = State::Connected;
|
||||
@@ -104,13 +102,9 @@ impl Listener {
|
||||
log::debug!("Websocket listener: Exiting");
|
||||
}
|
||||
|
||||
pub(crate) fn start(
|
||||
mut self,
|
||||
handler: HandlerBuilder,
|
||||
shutdown: nym_task::TaskClient,
|
||||
) -> JoinHandle<()> {
|
||||
pub(crate) fn start(mut self, handler: HandlerBuilder) -> JoinHandle<()> {
|
||||
info!("Running websocket on {:?}", self.address.to_string());
|
||||
|
||||
tokio::spawn(async move { self.run(handler, shutdown).await })
|
||||
tokio::spawn(async move { self.run(handler).await })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.48"
|
||||
version = "1.1.49"
|
||||
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"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
allow-panic-in-tests = true
|
||||
@@ -60,7 +60,7 @@ impl From<IpAddr> for IpPair {
|
||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
};
|
||||
let last_bytes = (before_last_byte as u16) << 8 | last_byte as u16;
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
|
||||
@@ -105,26 +105,24 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
async fn get_aggregate_verification_key(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
apis: &mut ApiClientsWrapper,
|
||||
ecash_apis: &mut ApiClientsWrapper<'_, C>,
|
||||
) -> Result<VerificationKeyAuth, BandwidthControllerError>
|
||||
where
|
||||
C: DkgQueryClient + Sync + Send,
|
||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
|
||||
get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
|
||||
}
|
||||
|
||||
async fn get_coin_index_signatures(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
apis: &mut ApiClientsWrapper,
|
||||
ecash_apis: &mut ApiClientsWrapper<'_, C>,
|
||||
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
|
||||
where
|
||||
C: DkgQueryClient + Sync + Send,
|
||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
|
||||
get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await
|
||||
}
|
||||
|
||||
@@ -132,13 +130,12 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
expiration_date: Date,
|
||||
apis: &mut ApiClientsWrapper,
|
||||
ecash_apis: &mut ApiClientsWrapper<'_, C>,
|
||||
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
|
||||
where
|
||||
C: DkgQueryClient + Sync + Send,
|
||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
|
||||
get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await
|
||||
}
|
||||
|
||||
@@ -154,7 +151,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
{
|
||||
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
|
||||
let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
|
||||
let mut api_clients = Default::default();
|
||||
let mut api_clients = ApiClientsWrapper::new(&self.client, epoch_id);
|
||||
|
||||
let verification_key = self
|
||||
.get_aggregate_verification_key(epoch_id, &mut api_clients)
|
||||
|
||||
@@ -21,30 +21,67 @@ use rand::thread_rng;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
|
||||
// it really doesn't need the RwLock because it's never moved across tasks,
|
||||
// but we need all the Send/Sync action
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ApiClientsWrapper(Option<Vec<EcashApiClient>>);
|
||||
|
||||
impl ApiClientsWrapper {
|
||||
pub(crate) async fn get_or_init<C>(
|
||||
pub(crate) trait EcashClientsProvider {
|
||||
async fn try_get_ecash_clients(
|
||||
&mut self,
|
||||
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>;
|
||||
}
|
||||
|
||||
impl EcashClientsProvider for Vec<EcashApiClient> {
|
||||
async fn try_get_ecash_clients(
|
||||
&mut self,
|
||||
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> EcashClientsProvider for &mut ApiClientsWrapper<'_, C>
|
||||
where
|
||||
C: DkgQueryClient + Sync + Send,
|
||||
{
|
||||
async fn try_get_ecash_clients(
|
||||
&mut self,
|
||||
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
|
||||
self.clients().await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ApiClientsWrapper<'a, C> {
|
||||
Uninitialised {
|
||||
query_client: &'a C,
|
||||
epoch_id: EpochId,
|
||||
dkg_client: &C,
|
||||
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
|
||||
},
|
||||
Cached {
|
||||
clients: Vec<EcashApiClient>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a, C> ApiClientsWrapper<'a, C> {
|
||||
pub(crate) fn new(query_client: &'a C, epoch_id: EpochId) -> Self {
|
||||
ApiClientsWrapper::Uninitialised {
|
||||
query_client,
|
||||
epoch_id,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clients(&mut self) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
|
||||
where
|
||||
C: DkgQueryClient + Sync + Send,
|
||||
{
|
||||
if let Some(cached) = &self.0 {
|
||||
return Ok(cached.clone());
|
||||
match self {
|
||||
ApiClientsWrapper::Uninitialised {
|
||||
query_client,
|
||||
epoch_id,
|
||||
} => {
|
||||
let clients = all_ecash_api_clients(*query_client, *epoch_id).await?;
|
||||
*self = ApiClientsWrapper::Cached {
|
||||
clients: clients.clone(),
|
||||
};
|
||||
|
||||
Ok(clients)
|
||||
}
|
||||
ApiClientsWrapper::Cached { clients } => Ok(clients.clone()),
|
||||
}
|
||||
|
||||
let clients = all_ecash_api_clients(dkg_client, epoch_id).await?;
|
||||
|
||||
// technically we don't have to be cloning all the clients here, but it's way simpler than
|
||||
// dealing with locking and whatnot given the performance penalty is negligible
|
||||
self.0 = Some(clients.clone());
|
||||
Ok(clients)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +113,7 @@ where
|
||||
pub(crate) async fn get_aggregate_verification_key<St>(
|
||||
storage: &St,
|
||||
epoch_id: EpochId,
|
||||
ecash_apis: Vec<EcashApiClient>,
|
||||
mut ecash_apis: impl EcashClientsProvider,
|
||||
) -> Result<VerificationKeyAuth, BandwidthControllerError>
|
||||
where
|
||||
St: Storage,
|
||||
@@ -90,6 +127,8 @@ where
|
||||
return Ok(stored);
|
||||
};
|
||||
|
||||
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
|
||||
|
||||
let master_vk = query_random_apis_until_success(
|
||||
ecash_apis,
|
||||
|api| async move { api.api_client.master_verification_key(Some(epoch_id)).await },
|
||||
@@ -115,7 +154,7 @@ where
|
||||
pub(crate) async fn get_coin_index_signatures<St>(
|
||||
storage: &St,
|
||||
epoch_id: EpochId,
|
||||
ecash_apis: Vec<EcashApiClient>,
|
||||
mut ecash_apis: impl EcashClientsProvider,
|
||||
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
|
||||
where
|
||||
St: Storage,
|
||||
@@ -129,6 +168,8 @@ where
|
||||
return Ok(stored);
|
||||
};
|
||||
|
||||
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
|
||||
|
||||
let index_sigs = query_random_apis_until_success(
|
||||
ecash_apis,
|
||||
|api| async move {
|
||||
@@ -159,7 +200,7 @@ pub(crate) async fn get_expiration_date_signatures<St>(
|
||||
storage: &St,
|
||||
epoch_id: EpochId,
|
||||
expiration_date: Date,
|
||||
ecash_apis: Vec<EcashApiClient>,
|
||||
mut ecash_apis: impl EcashClientsProvider,
|
||||
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
|
||||
where
|
||||
St: Storage,
|
||||
@@ -173,6 +214,8 @@ where
|
||||
return Ok(stored);
|
||||
};
|
||||
|
||||
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
|
||||
|
||||
let expiration_sigs = query_random_apis_until_success(
|
||||
ecash_apis,
|
||||
|api| async move {
|
||||
|
||||
@@ -45,6 +45,7 @@ const DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO: f64 = 0.70;
|
||||
// clients/client-core/src/client/replies/reply_storage/surb_storage.rs
|
||||
const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10;
|
||||
const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200;
|
||||
const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0;
|
||||
|
||||
// define how much to request at once
|
||||
// clients/client-core/src/client/replies/reply_controller.rs
|
||||
@@ -621,6 +622,10 @@ pub struct ReplySurbs {
|
||||
/// Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
|
||||
pub maximum_reply_surb_storage_threshold: usize,
|
||||
|
||||
/// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client
|
||||
/// should proactively request additional reply surbs.
|
||||
pub minimum_reply_surb_threshold_buffer: usize,
|
||||
|
||||
/// Defines the minimum number of reply surbs the client would request.
|
||||
pub minimum_reply_surb_request_size: u32,
|
||||
|
||||
@@ -660,6 +665,7 @@ impl Default for ReplySurbs {
|
||||
ReplySurbs {
|
||||
minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD,
|
||||
maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD,
|
||||
minimum_reply_surb_threshold_buffer: DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER,
|
||||
minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE,
|
||||
maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE,
|
||||
maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE,
|
||||
|
||||
@@ -181,6 +181,7 @@ impl From<ConfigV5> for Config {
|
||||
maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age,
|
||||
maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age,
|
||||
surb_mix_hops: value.debug.reply_surbs.surb_mix_hops,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ use nym_client_core_config_types::ForgetMe;
|
||||
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_crypto::hkdf::DerivationMaterial;
|
||||
use nym_gateway_client::client::config::GatewayClientConfig;
|
||||
use nym_gateway_client::{
|
||||
AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter,
|
||||
@@ -192,6 +193,8 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
|
||||
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
|
||||
derivation_material: Option<DerivationMaterial>,
|
||||
}
|
||||
|
||||
impl<C, S> BaseClientBuilder<C, S>
|
||||
@@ -216,9 +219,19 @@ where
|
||||
setup_method: GatewaySetup::MustLoad { gateway_id: None },
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback: None,
|
||||
derivation_material: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_derivation_material(
|
||||
mut self,
|
||||
derivation_material: Option<DerivationMaterial>,
|
||||
) -> Self {
|
||||
self.derivation_material = derivation_material;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
|
||||
self.config.debug.forget_me = *forget_me;
|
||||
@@ -297,7 +310,7 @@ where
|
||||
topology_accessor: TopologyAccessor,
|
||||
mix_tx: BatchMixMessageSender,
|
||||
stats_tx: ClientStatsSender,
|
||||
shutdown: TaskClient,
|
||||
task_client: TaskClient,
|
||||
) {
|
||||
info!("Starting loop cover traffic stream...");
|
||||
|
||||
@@ -310,9 +323,10 @@ where
|
||||
debug_config.traffic,
|
||||
debug_config.cover_traffic,
|
||||
stats_tx,
|
||||
task_client,
|
||||
);
|
||||
|
||||
stream.start_with_shutdown(shutdown);
|
||||
stream.start();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -327,7 +341,7 @@ where
|
||||
reply_controller_receiver: ReplyControllerReceiver,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
client_connection_rx: ConnectionCommandReceiver,
|
||||
shutdown: TaskClient,
|
||||
task_client: TaskClient,
|
||||
packet_type: PacketType,
|
||||
stats_tx: ClientStatsSender,
|
||||
) {
|
||||
@@ -345,8 +359,9 @@ where
|
||||
lane_queue_lengths,
|
||||
client_connection_rx,
|
||||
stats_tx,
|
||||
task_client,
|
||||
)
|
||||
.start_with_shutdown(shutdown, packet_type);
|
||||
.start(packet_type);
|
||||
}
|
||||
|
||||
// buffer controlling all messages fetched from provider
|
||||
@@ -369,8 +384,9 @@ where
|
||||
reply_key_storage,
|
||||
reply_controller_sender,
|
||||
metrics_reporter,
|
||||
shutdown,
|
||||
);
|
||||
controller.start_with_shutdown(shutdown)
|
||||
controller.start()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -559,15 +575,22 @@ where
|
||||
topology_accessor: TopologyAccessor,
|
||||
local_gateway: NodeIdentity,
|
||||
wait_for_gateway: bool,
|
||||
mut shutdown: TaskClient,
|
||||
mut task_client: TaskClient,
|
||||
) -> Result<(), ClientCoreError> {
|
||||
let topology_refresher_config =
|
||||
TopologyRefresherConfig::new(topology_config.topology_refresh_rate);
|
||||
|
||||
if topology_config.disable_refreshing {
|
||||
// if we're not spawning the refresher, don't cause shutdown immediately
|
||||
info!("The background topology refesher is not going to be started");
|
||||
task_client.disarm();
|
||||
}
|
||||
|
||||
let mut topology_refresher = TopologyRefresher::new(
|
||||
topology_refresher_config,
|
||||
topology_accessor,
|
||||
topology_provider,
|
||||
task_client,
|
||||
);
|
||||
// before returning, block entire runtime to refresh the current network view so that any
|
||||
// components depending on topology would see a non-empty view
|
||||
@@ -608,15 +631,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if topology_config.disable_refreshing {
|
||||
// if we're not spawning the refresher, don't cause shutdown immediately
|
||||
info!("The topology refesher is not going to be started");
|
||||
shutdown.disarm();
|
||||
} else {
|
||||
if !topology_config.disable_refreshing {
|
||||
// don't spawn the refresher if we don't want to be refreshing the topology.
|
||||
// only use the initial values obtained
|
||||
info!("Starting topology refresher...");
|
||||
topology_refresher.start_with_shutdown(shutdown);
|
||||
topology_refresher.start();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -627,17 +646,17 @@ where
|
||||
user_agent: Option<UserAgent>,
|
||||
client_stats_id: String,
|
||||
input_sender: Sender<InputMessage>,
|
||||
shutdown: TaskClient,
|
||||
task_client: TaskClient,
|
||||
) -> ClientStatsSender {
|
||||
info!("Starting statistics control...");
|
||||
StatisticsControl::create_and_start_with_shutdown(
|
||||
StatisticsControl::create_and_start(
|
||||
config.debug.stats_reporting,
|
||||
user_agent
|
||||
.map(|u| u.application)
|
||||
.unwrap_or("unknown".to_string()),
|
||||
client_stats_id,
|
||||
input_sender.clone(),
|
||||
shutdown.with_suffix("controller"),
|
||||
task_client,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -647,8 +666,8 @@ where
|
||||
) -> (BatchMixMessageSender, ClientRequestSender) {
|
||||
info!("Starting mix traffic controller...");
|
||||
let (mix_traffic_controller, mix_tx, client_tx) =
|
||||
MixTrafficController::new(gateway_transceiver);
|
||||
mix_traffic_controller.start_with_shutdown(shutdown);
|
||||
MixTrafficController::new(gateway_transceiver, shutdown);
|
||||
mix_traffic_controller.start();
|
||||
(mix_tx, client_tx)
|
||||
}
|
||||
|
||||
@@ -684,6 +703,7 @@ where
|
||||
setup_method: GatewaySetup,
|
||||
key_store: &S::KeyStore,
|
||||
details_store: &S::GatewaysDetailsStore,
|
||||
derivation_material: Option<DerivationMaterial>,
|
||||
) -> Result<InitialisationResult, ClientCoreError>
|
||||
where
|
||||
<S::KeyStore as KeyStore>::StorageError: Sync + Send,
|
||||
@@ -693,7 +713,12 @@ where
|
||||
if key_store.load_keys().await.is_err() {
|
||||
info!("could not find valid client keys - a new set will be generated");
|
||||
let mut rng = OsRng;
|
||||
let keys = ClientKeys::generate_new(&mut rng);
|
||||
let keys = if let Some(derivation_material) = derivation_material {
|
||||
ClientKeys::from_master_key(&mut rng, &derivation_material)
|
||||
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
|
||||
} else {
|
||||
ClientKeys::generate_new(&mut rng)
|
||||
};
|
||||
store_client_keys(keys, key_store).await?;
|
||||
}
|
||||
|
||||
@@ -715,6 +740,7 @@ where
|
||||
self.setup_method,
|
||||
self.client_store.key_store(),
|
||||
self.client_store.gateway_details_store(),
|
||||
self.derivation_material,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 nym_task::TaskClient;
|
||||
use rand::{rngs::OsRng, CryptoRng, Rng};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
@@ -64,6 +65,8 @@ where
|
||||
packet_type: PacketType,
|
||||
|
||||
stats_tx: ClientStatsSender,
|
||||
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> Stream for LoopCoverTrafficStream<R>
|
||||
@@ -110,6 +113,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
traffic_config: config::Traffic,
|
||||
cover_config: config::CoverTraffic,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
let rng = OsRng;
|
||||
|
||||
@@ -128,6 +132,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
secondary_packet_size: traffic_config.secondary_packet_size,
|
||||
packet_type: traffic_config.packet_type,
|
||||
stats_tx,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +180,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
}
|
||||
};
|
||||
|
||||
let cover_message = generate_loop_cover_packet(
|
||||
let cover_message = match generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
topology_ref,
|
||||
&self.ack_key,
|
||||
@@ -184,8 +189,15 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
self.cover_traffic.loop_cover_traffic_average_delay,
|
||||
cover_traffic_packet_size,
|
||||
self.packet_type,
|
||||
)
|
||||
.expect("Somehow failed to generate a loop cover message with a valid topology");
|
||||
) {
|
||||
Ok(cover_message) => cover_message,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Somehow failed to generate a loop cover message with a valid topology: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
|
||||
match err {
|
||||
@@ -217,7 +229,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub fn start(mut self) {
|
||||
if self.cover_traffic.disable_loop_cover_traffic_stream {
|
||||
// we should have never got here in the first place - the task should have never been created to begin with
|
||||
// so panic and review the code that lead to this branch
|
||||
@@ -231,6 +243,8 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
);
|
||||
self.set_next_delay(sampled);
|
||||
|
||||
let mut shutdown = self.task_client.fork("select");
|
||||
|
||||
spawn_future(async move {
|
||||
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::key_manager::persistence::KeyStore;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_crypto::{
|
||||
asymmetric::{encryption, identity},
|
||||
hkdf::{DerivationMaterial, InvalidLength},
|
||||
};
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
@@ -10,6 +13,7 @@ use std::sync::Arc;
|
||||
use zeroize::ZeroizeOnDrop;
|
||||
|
||||
pub mod persistence;
|
||||
mod test;
|
||||
|
||||
// Note: to support key rotation in the future, all keys will require adding an extra smart pointer,
|
||||
// most likely an AtomicCell, or if it doesn't work as I think it does, a Mutex. Although I think
|
||||
@@ -43,6 +47,24 @@ impl ClientKeys {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_master_key<R>(
|
||||
rng: &mut R,
|
||||
derivation_material: &DerivationMaterial,
|
||||
) -> Result<Self, InvalidLength>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let secret = derivation_material.derive_secret()?;
|
||||
Ok(ClientKeys {
|
||||
identity_keypair: Arc::new(identity::KeyPair::from_secret(
|
||||
secret,
|
||||
derivation_material.index(),
|
||||
)),
|
||||
encryption_keypair: Arc::new(encryption::KeyPair::new(rng)),
|
||||
ack_key: Arc::new(AckKey::new(rng)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_keys(
|
||||
id_keypair: identity::KeyPair,
|
||||
enc_keypair: encryption::KeyPair,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::client::key_manager::ClientKeys;
|
||||
use nym_crypto::hkdf::DerivationMaterial;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
#[test]
|
||||
fn test_from_master_key_success() {
|
||||
// Set up a deterministic RNG.
|
||||
let seed = [33u8; 32];
|
||||
let mut rng = ChaCha20Rng::from_seed(seed);
|
||||
|
||||
// Set up the derivation material.
|
||||
let master_key = b"this is a secret master key";
|
||||
let salt = b"unique-salt";
|
||||
let derivation_material = DerivationMaterial::new(master_key, 0, salt);
|
||||
|
||||
// Generate ClientKeys from the master key.
|
||||
let client_keys = ClientKeys::from_master_key(&mut rng, &derivation_material)
|
||||
.expect("Failed to create client keys");
|
||||
|
||||
assert_eq!(
|
||||
client_keys.identity_keypair().public_key().to_string(),
|
||||
String::from("FX4Undr5LPPBA7zThWWpAKXKQTXSbW1C28PnxbCqUkU4")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client_keys.identity_keypair().private_key().to_string(),
|
||||
String::from("6S3uMi2rU5SwyUUYCiMrF5qqdcYnEDMYLggBSvavVzEt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_master_key_deterministic_identity() {
|
||||
// Using identical derivation material should result in the exactly same identity keypair.
|
||||
let seed = [1u8; 32];
|
||||
let mut rng1 = ChaCha20Rng::from_seed(seed);
|
||||
let mut rng2 = ChaCha20Rng::from_seed(seed);
|
||||
|
||||
let master_key = b"another secret master key";
|
||||
let salt = b"deterministic-salt";
|
||||
let index = 7u32;
|
||||
let derivation_material = DerivationMaterial::new(master_key, index, salt);
|
||||
|
||||
let client_keys1 = ClientKeys::from_master_key(&mut rng1, &derivation_material)
|
||||
.expect("Failed to create client keys (first instance)");
|
||||
let client_keys2 = ClientKeys::from_master_key(&mut rng2, &derivation_material)
|
||||
.expect("Failed to create client keys (second instance)");
|
||||
|
||||
assert_eq!(
|
||||
client_keys1.identity_keypair().public_key().to_string(),
|
||||
client_keys2.identity_keypair().public_key().to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client_keys1.identity_keypair().private_key().to_string(),
|
||||
client_keys2.identity_keypair().private_key().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_master_key_different_indices() {
|
||||
// Changing the index should yield a different identity key.
|
||||
let seed = [5u8; 32];
|
||||
let mut rng = ChaCha20Rng::from_seed(seed);
|
||||
|
||||
let master_key = b"same secret key";
|
||||
let salt = b"same-salt";
|
||||
|
||||
let derivation_material1 = DerivationMaterial::new(master_key, 1, salt);
|
||||
let derivation_material2 = DerivationMaterial::new(master_key, 2, salt);
|
||||
|
||||
let client_keys1 = ClientKeys::from_master_key(&mut rng, &derivation_material1)
|
||||
.expect("Failed to create client keys for index 1");
|
||||
let client_keys2 = ClientKeys::from_master_key(&mut rng, &derivation_material2)
|
||||
.expect("Failed to create client keys for index 2");
|
||||
|
||||
assert_ne!(
|
||||
client_keys1.identity_keypair().public_key().to_string(),
|
||||
client_keys2.identity_keypair().public_key().to_string()
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
client_keys1.identity_keypair().private_key().to_string(),
|
||||
client_keys2.identity_keypair().private_key().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use crate::spawn_future;
|
||||
use log::*;
|
||||
use nym_gateway_requests::ClientRequest;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_task::TaskClient;
|
||||
use transceiver::ErasedGatewayError;
|
||||
|
||||
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
|
||||
@@ -32,11 +33,14 @@ pub struct MixTrafficController {
|
||||
// TODO: this is temporary work-around.
|
||||
// in long run `gateway_client` will be moved away from `MixTrafficController` anyway.
|
||||
consecutive_gateway_failure_count: usize,
|
||||
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl MixTrafficController {
|
||||
pub fn new<T>(
|
||||
gateway_transceiver: T,
|
||||
task_client: TaskClient,
|
||||
) -> (
|
||||
MixTrafficController,
|
||||
BatchMixMessageSender,
|
||||
@@ -56,6 +60,7 @@ impl MixTrafficController {
|
||||
mix_rx: message_receiver,
|
||||
client_rx: client_receiver,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
task_client,
|
||||
},
|
||||
message_sender,
|
||||
client_sender,
|
||||
@@ -64,6 +69,7 @@ impl MixTrafficController {
|
||||
|
||||
pub fn new_dynamic(
|
||||
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
|
||||
task_client: TaskClient,
|
||||
) -> (
|
||||
MixTrafficController,
|
||||
BatchMixMessageSender,
|
||||
@@ -78,6 +84,7 @@ impl MixTrafficController {
|
||||
mix_rx: message_receiver,
|
||||
client_rx: client_receiver,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
task_client,
|
||||
},
|
||||
message_sender,
|
||||
client_sender,
|
||||
@@ -109,11 +116,11 @@ impl MixTrafficController {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
debug!("Started MixTrafficController with graceful shutdown support");
|
||||
|
||||
loop {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
Some(mix_packets) => {
|
||||
@@ -125,7 +132,7 @@ impl MixTrafficController {
|
||||
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
||||
// Do we need to handle the embedded mixnet client case
|
||||
// separately?
|
||||
shutdown.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
||||
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -146,13 +153,13 @@ impl MixTrafficController {
|
||||
log::trace!("MixTrafficController, client request channel closed");
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
log::debug!("MixTrafficController: Exiting");
|
||||
});
|
||||
|
||||
+15
-6
@@ -11,6 +11,7 @@ use nym_sphinx::{
|
||||
acknowledgements::{identifier::recover_identifier, AckKey},
|
||||
chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID},
|
||||
};
|
||||
use nym_task::TaskClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Module responsible for listening for any data resembling acknowledgements from the network
|
||||
@@ -20,6 +21,7 @@ pub(super) struct AcknowledgementListener {
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
action_sender: AckActionSender,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl AcknowledgementListener {
|
||||
@@ -28,12 +30,14 @@ impl AcknowledgementListener {
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
action_sender: AckActionSender,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
AcknowledgementListener {
|
||||
ack_key,
|
||||
ack_receiver,
|
||||
action_sender,
|
||||
stats_tx,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +68,14 @@ impl AcknowledgementListener {
|
||||
trace!("Received {} from the mix network", frag_id);
|
||||
self.stats_tx
|
||||
.report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into());
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_remove(frag_id))
|
||||
.unwrap();
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send remove action to action controller: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ack_receiver_item(&mut self, item: Vec<Vec<u8>>) {
|
||||
@@ -76,10 +85,10 @@ impl AcknowledgementListener {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started AcknowledgementListener with graceful shutdown support");
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
acks = self.ack_receiver.next() => match acks {
|
||||
Some(acks) => self.handle_ack_receiver_item(acks).await,
|
||||
@@ -88,12 +97,12 @@ impl AcknowledgementListener {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("AcknowledgementListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("AcknowledgementListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-16
@@ -9,6 +9,7 @@ use log::*;
|
||||
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use nym_sphinx::Delay as SphinxDelay;
|
||||
use nym_task::TaskClient;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -101,6 +102,8 @@ pub(super) struct ActionController {
|
||||
|
||||
/// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements.
|
||||
retransmission_sender: RetransmissionRequestSender,
|
||||
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl ActionController {
|
||||
@@ -108,6 +111,7 @@ impl ActionController {
|
||||
config: Config,
|
||||
retransmission_sender: RetransmissionRequestSender,
|
||||
incoming_actions: AckActionReceiver,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
ActionController {
|
||||
config,
|
||||
@@ -115,6 +119,7 @@ impl ActionController {
|
||||
pending_acks_timers: NonExhaustiveDelayQueue::new(),
|
||||
incoming_actions,
|
||||
retransmission_sender,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,11 +221,7 @@ impl ActionController {
|
||||
}
|
||||
|
||||
// note: when the entry expires it's automatically removed from pending_acks_timers
|
||||
fn handle_expired_ack_timer(
|
||||
&mut self,
|
||||
expired_ack: Expired<FragmentIdentifier>,
|
||||
task_client: &mut nym_task::TaskClient,
|
||||
) {
|
||||
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
|
||||
// I'm honestly not sure how to handle it, because getting it means other things in our
|
||||
// system are already misbehaving. If we ever see this panic, then I guess we should worry
|
||||
// about it. Perhaps just reschedule it at later point?
|
||||
@@ -238,15 +239,13 @@ impl ActionController {
|
||||
// downgrading an arc and then upgrading vs cloning is difference of 30ns vs 15ns
|
||||
// so it's literally a NO difference while it might prevent us from unnecessarily
|
||||
// resending data (in maybe 1 in 1 million cases, but it's something)
|
||||
if self
|
||||
if let Err(err) = self
|
||||
.retransmission_sender
|
||||
.unbounded_send(Arc::downgrade(pending_ack_data))
|
||||
.is_err()
|
||||
{
|
||||
assert!(
|
||||
task_client.is_shutdown_poll(),
|
||||
"Failed to send pending ack for retransmission"
|
||||
);
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
log::error!("Failed to send pending ack for retransmission: {err}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// this shouldn't cause any issues but shouldn't have happened to begin with!
|
||||
@@ -265,10 +264,10 @@ impl ActionController {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started ActionController with graceful shutdown support");
|
||||
|
||||
loop {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
action = self.incoming_actions.next() => match action {
|
||||
Some(action) => self.process_action(action),
|
||||
@@ -280,19 +279,19 @@ impl ActionController {
|
||||
}
|
||||
},
|
||||
expired_ack = self.pending_acks_timers.next() => match expired_ack {
|
||||
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack, &mut shutdown),
|
||||
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack),
|
||||
None => {
|
||||
log::trace!("ActionController: Stopping since ack channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("ActionController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("ActionController: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -11,6 +11,7 @@ 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::TaskClient;
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
|
||||
@@ -23,6 +24,7 @@ where
|
||||
input_receiver: InputMessageReceiver,
|
||||
message_handler: MessageHandler<R>,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> InputMessageListener<R>
|
||||
@@ -36,11 +38,13 @@ where
|
||||
input_receiver: InputMessageReceiver,
|
||||
message_handler: MessageHandler<R>,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
InputMessageListener {
|
||||
input_receiver,
|
||||
message_handler,
|
||||
reply_controller_sender,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +67,14 @@ where
|
||||
lane: TransmissionLane,
|
||||
) {
|
||||
// offload reply handling to the dedicated task
|
||||
self.reply_controller_sender
|
||||
if let Err(err) = self
|
||||
.reply_controller_sender
|
||||
.send_reply(recipient_tag, data, lane)
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("failed to send a reply - {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_plain_message(
|
||||
@@ -164,10 +174,10 @@ where
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started InputMessageListener with graceful shutdown support");
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
input_msg = self.input_receiver.recv() => match input_msg {
|
||||
Some(input_msg) => {
|
||||
@@ -178,12 +188,12 @@ where
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("InputMessageListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("InputMessageListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-27
@@ -24,6 +24,7 @@ use nym_sphinx::{
|
||||
Delay as SphinxDelay,
|
||||
};
|
||||
use nym_statistics_common::clients::ClientStatsSender;
|
||||
use nym_task::TaskClient;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
@@ -66,7 +67,7 @@ pub(crate) enum PacketDestination {
|
||||
|
||||
/// Structure representing a data `Fragment` that is on-route to the specified `Recipient`
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PendingAcknowledgement {
|
||||
pub struct PendingAcknowledgement {
|
||||
message_chunk: Fragment,
|
||||
delay: SphinxDelay,
|
||||
destination: PacketDestination,
|
||||
@@ -216,6 +217,7 @@ where
|
||||
message_handler: MessageHandler<R>,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
let (retransmission_tx, retransmission_rx) = mpsc::unbounded();
|
||||
|
||||
@@ -225,6 +227,7 @@ where
|
||||
action_config,
|
||||
retransmission_tx,
|
||||
connectors.ack_action_receiver,
|
||||
task_client.fork("action_controller"),
|
||||
);
|
||||
|
||||
// will listen for any acks coming from the network
|
||||
@@ -233,6 +236,7 @@ where
|
||||
connectors.ack_receiver,
|
||||
connectors.ack_action_sender.clone(),
|
||||
stats_tx,
|
||||
task_client.fork("acknowledgement_listener"),
|
||||
);
|
||||
|
||||
// will listen for any new messages from the client
|
||||
@@ -240,6 +244,7 @@ where
|
||||
connectors.input_receiver,
|
||||
message_handler.clone(),
|
||||
reply_controller_sender.clone(),
|
||||
task_client.fork("input_message_listener"),
|
||||
);
|
||||
|
||||
// will listen for any ack timeouts and trigger retransmission
|
||||
@@ -249,12 +254,16 @@ where
|
||||
message_handler,
|
||||
retransmission_rx,
|
||||
reply_controller_sender,
|
||||
task_client.fork("retransmission_request_listener"),
|
||||
);
|
||||
|
||||
// will listen for events indicating the packet was sent through the network so that
|
||||
// the retransmission timer should be started.
|
||||
let sent_notification_listener =
|
||||
SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender);
|
||||
let sent_notification_listener = SentNotificationListener::new(
|
||||
connectors.sent_notifier,
|
||||
connectors.ack_action_sender,
|
||||
task_client.with_suffix("sent_notification_listener"),
|
||||
);
|
||||
|
||||
AcknowledgementController {
|
||||
acknowledgement_listener,
|
||||
@@ -265,53 +274,35 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn start_with_shutdown(
|
||||
self,
|
||||
shutdown: nym_task::TaskClient,
|
||||
packet_type: PacketType,
|
||||
) {
|
||||
pub(super) fn start(self, packet_type: PacketType) {
|
||||
let mut acknowledgement_listener = self.acknowledgement_listener;
|
||||
let mut input_message_listener = self.input_message_listener;
|
||||
let mut retransmission_request_listener = self.retransmission_request_listener;
|
||||
let mut sent_notification_listener = self.sent_notification_listener;
|
||||
let mut action_controller = self.action_controller;
|
||||
|
||||
let shutdown_handle = shutdown.fork("acknowledgement_listener");
|
||||
spawn_future(async move {
|
||||
acknowledgement_listener
|
||||
.run_with_shutdown(shutdown_handle)
|
||||
.await;
|
||||
acknowledgement_listener.run().await;
|
||||
debug!("The acknowledgement listener has finished execution!");
|
||||
});
|
||||
|
||||
let shutdown_handle = shutdown.fork("input_message_listener");
|
||||
spawn_future(async move {
|
||||
input_message_listener
|
||||
.run_with_shutdown(shutdown_handle)
|
||||
.await;
|
||||
input_message_listener.run().await;
|
||||
debug!("The input listener has finished execution!");
|
||||
});
|
||||
|
||||
let shutdown_handle = shutdown.fork("retransmission_request_listener");
|
||||
spawn_future(async move {
|
||||
retransmission_request_listener
|
||||
.run_with_shutdown(shutdown_handle, packet_type)
|
||||
.await;
|
||||
retransmission_request_listener.run(packet_type).await;
|
||||
debug!("The retransmission request listener has finished execution!");
|
||||
});
|
||||
|
||||
let shutdown_handle = shutdown.fork("sent_notification_listener");
|
||||
spawn_future(async move {
|
||||
sent_notification_listener
|
||||
.run_with_shutdown(shutdown_handle)
|
||||
.await;
|
||||
sent_notification_listener.run().await;
|
||||
debug!("The sent notification listener has finished execution!");
|
||||
});
|
||||
|
||||
spawn_future(async move {
|
||||
action_controller
|
||||
.run_with_shutdown(shutdown.with_suffix("action_controller"))
|
||||
.await;
|
||||
action_controller.run().await;
|
||||
debug!("The controller has finished execution!");
|
||||
});
|
||||
}
|
||||
|
||||
+32
-17
@@ -14,7 +14,7 @@ use log::*;
|
||||
use nym_sphinx::chunking::fragment::Fragment;
|
||||
use nym_sphinx::preparer::PreparedFragment;
|
||||
use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
|
||||
use nym_task::connections::TransmissionLane;
|
||||
use nym_task::{connections::TransmissionLane, TaskClient};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
@@ -25,6 +25,7 @@ pub(super) struct RetransmissionRequestListener<R> {
|
||||
message_handler: MessageHandler<R>,
|
||||
request_receiver: RetransmissionRequestReceiver,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> RetransmissionRequestListener<R>
|
||||
@@ -37,6 +38,7 @@ where
|
||||
message_handler: MessageHandler<R>,
|
||||
request_receiver: RetransmissionRequestReceiver,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
RetransmissionRequestListener {
|
||||
maximum_retransmissions,
|
||||
@@ -44,6 +46,7 @@ where
|
||||
message_handler,
|
||||
request_receiver,
|
||||
reply_controller_sender,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +82,12 @@ where
|
||||
if let Some(limit) = self.maximum_retransmissions {
|
||||
if timed_out_ack.retransmissions >= limit {
|
||||
warn!("reached maximum number of allowed retransmissions for the packet");
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_remove(frag_id))
|
||||
.unwrap();
|
||||
{
|
||||
error!("Failed to send remove action to the controller: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -93,11 +99,16 @@ where
|
||||
} => {
|
||||
// if this is retransmission for reply, offload it to the dedicated task
|
||||
// that deals with all the surbs
|
||||
return self.reply_controller_sender.send_retransmission_data(
|
||||
if let Err(err) = self.reply_controller_sender.send_retransmission_data(
|
||||
*recipient_tag,
|
||||
weak_timed_out_ack,
|
||||
*extra_surb_request,
|
||||
);
|
||||
) {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send retransmission data to the reply controller: {err}");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
PacketDestination::KnownRecipient(recipient) => {
|
||||
self.prepare_normal_retransmission_chunk(
|
||||
@@ -114,9 +125,12 @@ where
|
||||
Err(err) => {
|
||||
warn!("Could not retransmit the packet - {err}");
|
||||
// we NEED to start timer here otherwise we will have this guy permanently stuck in memory
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_start_timer(frag_id))
|
||||
.unwrap();
|
||||
{
|
||||
error!("Failed to send start timer action to the controller: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -141,9 +155,14 @@ where
|
||||
// is sent to the `OutQueueControl` and has gone through its internal queue
|
||||
// with the additional poisson delay.
|
||||
// And since Actions are executed in order `UpdateTimer` will HAVE TO be executed before `StartTimer`
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_update_pending_ack(frag_id, new_delay))
|
||||
.unwrap();
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send update pending ack action to the controller: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
// send to `OutQueueControl` to eventually send to the mix network
|
||||
self.message_handler
|
||||
@@ -157,14 +176,10 @@ where
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(
|
||||
&mut self,
|
||||
mut shutdown: nym_task::TaskClient,
|
||||
packet_type: PacketType,
|
||||
) {
|
||||
pub(super) async fn run(&mut self, packet_type: PacketType) {
|
||||
debug!("Started RetransmissionRequestListener with graceful shutdown support");
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
|
||||
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
|
||||
@@ -173,12 +188,12 @@ where
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("RetransmissionRequestListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("RetransmissionRequestListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -6,6 +6,7 @@ use super::SentPacketNotificationReceiver;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
|
||||
use nym_task::TaskClient;
|
||||
|
||||
/// Module responsible for starting up retransmission timers.
|
||||
/// It is required because when we send our packet to the `real traffic stream` controlled
|
||||
@@ -14,16 +15,19 @@ use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
|
||||
pub(super) struct SentNotificationListener {
|
||||
sent_notifier: SentPacketNotificationReceiver,
|
||||
action_sender: AckActionSender,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl SentNotificationListener {
|
||||
pub(super) fn new(
|
||||
sent_notifier: SentPacketNotificationReceiver,
|
||||
action_sender: AckActionSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
SentNotificationListener {
|
||||
sent_notifier,
|
||||
action_sender,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +36,20 @@ impl SentNotificationListener {
|
||||
trace!("sent off a cover message - no need to start retransmission timer!");
|
||||
return;
|
||||
}
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_start_timer(frag_id))
|
||||
.unwrap();
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send start timer action to action controller: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started SentNotificationListener with graceful shutdown support");
|
||||
|
||||
loop {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
frag_id = self.sent_notifier.next() => match frag_id {
|
||||
Some(frag_id) => {
|
||||
@@ -51,13 +60,13 @@ impl SentNotificationListener {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("SentNotificationListener: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(shutdown.is_shutdown_poll());
|
||||
assert!(self.task_client.is_shutdown_poll());
|
||||
log::debug!("SentNotificationListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use nym_sphinx::params::{PacketSize, PacketType};
|
||||
use nym_sphinx::preparer::{MessagePreparer, PreparedFragment};
|
||||
use nym_sphinx::Delay;
|
||||
use nym_task::connections::TransmissionLane;
|
||||
use nym_task::TaskClient;
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::collections::HashMap;
|
||||
@@ -149,12 +150,14 @@ pub(crate) struct MessageHandler<R> {
|
||||
topology_access: TopologyAccessor,
|
||||
reply_key_storage: SentReplyKeys,
|
||||
tag_storage: UsedSenderTags,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> MessageHandler<R>
|
||||
where
|
||||
R: CryptoRng + Rng,
|
||||
{
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
config: Config,
|
||||
rng: R,
|
||||
@@ -163,6 +166,7 @@ where
|
||||
topology_access: TopologyAccessor,
|
||||
reply_key_storage: SentReplyKeys,
|
||||
tag_storage: UsedSenderTags,
|
||||
task_client: TaskClient,
|
||||
) -> Self
|
||||
where
|
||||
R: Copy,
|
||||
@@ -183,6 +187,7 @@ where
|
||||
topology_access,
|
||||
reply_key_storage,
|
||||
tag_storage,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,15 +614,25 @@ where
|
||||
}
|
||||
|
||||
pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) {
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::UpdatePendingAck(id, new_delay))
|
||||
.expect("action control task has died")
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send update action to the controller: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert_pending_acks(&self, pending_acks: Vec<PendingAcknowledgement>) {
|
||||
self.action_sender
|
||||
if let Err(err) = self
|
||||
.action_sender
|
||||
.unbounded_send(Action::new_insert(pending_acks))
|
||||
.expect("action control task has died")
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to send insert action to the controller: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tells real message sender (with the poisson timer) to send this to the mix network
|
||||
@@ -631,9 +646,9 @@ where
|
||||
.send((messages, transmission_lane))
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to forward messages to the real message sender (OutQueueControl): {err}"
|
||||
);
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to forward messages to the real message sender: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::params::PacketType;
|
||||
use nym_statistics_common::clients::ClientStatsSender;
|
||||
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
|
||||
use nym_task::TaskClient;
|
||||
use rand::{rngs::OsRng, CryptoRng, Rng};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -147,6 +148,7 @@ impl RealMessagesController<OsRng> {
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
client_connection_rx: ConnectionCommandReceiver,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
let rng = OsRng;
|
||||
|
||||
@@ -177,6 +179,7 @@ impl RealMessagesController<OsRng> {
|
||||
topology_access.clone(),
|
||||
reply_storage.key_storage(),
|
||||
reply_storage.tags_storage(),
|
||||
task_client.fork("message_handler"),
|
||||
);
|
||||
|
||||
let ack_control = AcknowledgementController::new(
|
||||
@@ -186,6 +189,7 @@ impl RealMessagesController<OsRng> {
|
||||
message_handler.clone(),
|
||||
reply_controller_sender,
|
||||
stats_tx.clone(),
|
||||
task_client.fork("ack_control"),
|
||||
);
|
||||
|
||||
let reply_control = ReplyController::new(
|
||||
@@ -193,6 +197,7 @@ impl RealMessagesController<OsRng> {
|
||||
message_handler,
|
||||
reply_storage,
|
||||
reply_controller_receiver,
|
||||
task_client.fork("reply_controller"),
|
||||
);
|
||||
|
||||
let out_queue_control = OutQueueControl::new(
|
||||
@@ -205,6 +210,7 @@ impl RealMessagesController<OsRng> {
|
||||
lane_queue_lengths,
|
||||
client_connection_rx,
|
||||
stats_tx,
|
||||
task_client.with_suffix("out_queue_control"),
|
||||
);
|
||||
|
||||
RealMessagesController {
|
||||
@@ -214,22 +220,20 @@ impl RealMessagesController<OsRng> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(self, shutdown: nym_task::TaskClient, packet_type: PacketType) {
|
||||
pub fn start(self, packet_type: PacketType) {
|
||||
let mut out_queue_control = self.out_queue_control;
|
||||
let ack_control = self.ack_control;
|
||||
let mut reply_control = self.reply_control;
|
||||
|
||||
let shutdown_handle = shutdown.fork("out_queue_control");
|
||||
spawn_future(async move {
|
||||
out_queue_control.run_with_shutdown(shutdown_handle).await;
|
||||
out_queue_control.run().await;
|
||||
debug!("The out queue controller has finished execution!");
|
||||
});
|
||||
let shutdown_handle = shutdown.fork("reply_control");
|
||||
spawn_future(async move {
|
||||
reply_control.run_with_shutdown(shutdown_handle).await;
|
||||
reply_control.run().await;
|
||||
debug!("The reply controller has finished execution!");
|
||||
});
|
||||
|
||||
ack_control.start_with_shutdown(shutdown.with_suffix("ack_control"), packet_type);
|
||||
ack_control.start(packet_type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C
|
||||
use nym_task::connections::{
|
||||
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use nym_task::TaskClient;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
@@ -117,6 +118,8 @@ where
|
||||
|
||||
/// Channel used for sending metrics events (specifically `PacketStatistics` events) to the metrics tracker.
|
||||
stats_tx: ClientStatsSender,
|
||||
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -176,6 +179,7 @@ where
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
client_connection_rx: ConnectionCommandReceiver,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
OutQueueControl {
|
||||
config,
|
||||
@@ -190,6 +194,7 @@ where
|
||||
client_connection_rx,
|
||||
lane_queue_lengths,
|
||||
stats_tx,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +203,9 @@ where
|
||||
// queues and client load rather than the required delay. So realistically we can treat
|
||||
// whatever is about to happen as negligible additional delay.
|
||||
trace!("{} is about to get sent to the mixnet", frag_id);
|
||||
self.sent_notifier.unbounded_send(frag_id).unwrap();
|
||||
if let Err(err) = self.sent_notifier.unbounded_send(frag_id) {
|
||||
error!("Failed to notify about sent message: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn loop_cover_message_size(&mut self) -> PacketSize {
|
||||
@@ -271,7 +278,9 @@ where
|
||||
};
|
||||
|
||||
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
|
||||
log::error!("Failed to send: {err}");
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
log::error!("Failed to send: {err}");
|
||||
}
|
||||
} else {
|
||||
let event = if fragment_id.is_some() {
|
||||
PacketStatisticsEvent::RealPacketSent(packet_size)
|
||||
@@ -504,21 +513,29 @@ where
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn log_status(&self, shutdown: &mut nym_task::TaskClient) {
|
||||
fn log_status(&self, shutdown: &mut TaskClient) {
|
||||
use crate::error::ClientCoreStatusMessage;
|
||||
|
||||
let packets = self.transmission_buffer.total_size();
|
||||
let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0;
|
||||
let lanes = self.transmission_buffer.num_lanes();
|
||||
let lanes = self.transmission_buffer.lanes();
|
||||
let mult = self.sending_delay_controller.current_multiplier();
|
||||
let delay = self.current_average_message_sending_delay().as_millis();
|
||||
|
||||
let lane_status = lanes
|
||||
.iter()
|
||||
.map(|lane_name| {
|
||||
let lane_length = self.transmission_buffer.lane_length(lane_name).unwrap_or(0);
|
||||
format!("{lane_name:?}: {lane_length}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
let status_str = if self.config.traffic.disable_main_poisson_packet_distribution {
|
||||
format!("Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, no delay")
|
||||
format!("Packet backlog: {lane_status}, no delay")
|
||||
} else {
|
||||
format!(
|
||||
"Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, avg delay: {delay}ms ({mult})"
|
||||
)
|
||||
format!("Packet backlog: {lane_status}, avg delay: {delay}ms ({mult})")
|
||||
};
|
||||
|
||||
if packets > 1000 {
|
||||
log::warn!("{status_str}");
|
||||
} else if packets > 0 {
|
||||
@@ -535,14 +552,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started OutQueueControl with graceful shutdown support");
|
||||
|
||||
let mut shutdown = self.task_client.fork("select");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let mut status_timer = tokio::time::interval(Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ 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_task::TaskClient;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -152,6 +153,7 @@ struct ReceivedMessagesBuffer<R: MessageReceiver> {
|
||||
inner: Arc<Mutex<ReceivedMessagesBufferInner<R>>>,
|
||||
reply_key_storage: SentReplyKeys,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
@@ -160,6 +162,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
reply_key_storage: SentReplyKeys,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
stats_tx: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
ReceivedMessagesBuffer {
|
||||
inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner {
|
||||
@@ -172,6 +175,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
})),
|
||||
reply_key_storage,
|
||||
reply_controller_sender,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +261,15 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
}
|
||||
};
|
||||
|
||||
self.reply_controller_sender.send_additional_surbs(
|
||||
if let Err(err) = self.reply_controller_sender.send_additional_surbs(
|
||||
msg.sender_tag,
|
||||
reply_surbs,
|
||||
from_surb_request,
|
||||
)
|
||||
) {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
reconstructed
|
||||
}
|
||||
@@ -276,8 +284,14 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
ReplyMessageContent::Data { message } => reconstructed.push(message.into()),
|
||||
ReplyMessageContent::SurbRequest { recipient, amount } => {
|
||||
debug!("received request for {amount} additional reply SURBs from {recipient}");
|
||||
self.reply_controller_sender
|
||||
.send_additional_surbs_request(*recipient, amount);
|
||||
if let Err(err) = self
|
||||
.reply_controller_sender
|
||||
.send_additional_surbs_request(*recipient, amount)
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,16 +413,19 @@ pub enum ReceivedBufferMessage {
|
||||
struct RequestReceiver<R: MessageReceiver> {
|
||||
received_buffer: ReceivedMessagesBuffer<R>,
|
||||
query_receiver: ReceivedBufferRequestReceiver,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R: MessageReceiver> RequestReceiver<R> {
|
||||
fn new(
|
||||
received_buffer: ReceivedMessagesBuffer<R>,
|
||||
query_receiver: ReceivedBufferRequestReceiver,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
RequestReceiver {
|
||||
received_buffer,
|
||||
query_receiver,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,12 +440,12 @@ impl<R: MessageReceiver> RequestReceiver<R> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
async fn run(&mut self) {
|
||||
debug!("Started RequestReceiver with graceful shutdown support");
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("RequestReceiver: Received shutdown");
|
||||
}
|
||||
request = self.query_receiver.next() => {
|
||||
@@ -441,7 +458,7 @@ impl<R: MessageReceiver> RequestReceiver<R> {
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv().await;
|
||||
log::debug!("RequestReceiver: Exiting");
|
||||
}
|
||||
}
|
||||
@@ -449,25 +466,25 @@ impl<R: MessageReceiver> RequestReceiver<R> {
|
||||
struct FragmentedMessageReceiver<R: MessageReceiver> {
|
||||
received_buffer: ReceivedMessagesBuffer<R>,
|
||||
mixnet_packet_receiver: MixnetMessageReceiver,
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
|
||||
fn new(
|
||||
received_buffer: ReceivedMessagesBuffer<R>,
|
||||
mixnet_packet_receiver: MixnetMessageReceiver,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
FragmentedMessageReceiver {
|
||||
received_buffer,
|
||||
mixnet_packet_receiver,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_with_shutdown(
|
||||
&mut self,
|
||||
mut shutdown: nym_task::TaskClient,
|
||||
) -> Result<(), MessageRecoveryError> {
|
||||
async fn run(&mut self) -> Result<(), MessageRecoveryError> {
|
||||
debug!("Started FragmentedMessageReceiver with graceful shutdown support");
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
new_messages = self.mixnet_packet_receiver.next() => {
|
||||
if let Some(new_messages) = new_messages {
|
||||
@@ -477,12 +494,12 @@ impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = self.task_client.recv_with_delay() => {
|
||||
log::trace!("FragmentedMessageReceiver: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("FragmentedMessageReceiver: Exiting");
|
||||
Ok(())
|
||||
}
|
||||
@@ -501,41 +518,42 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
|
||||
reply_key_storage: SentReplyKeys,
|
||||
reply_controller_sender: ReplyControllerSender,
|
||||
metrics_reporter: ClientStatsSender,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
let received_buffer = ReceivedMessagesBuffer::new(
|
||||
local_encryption_keypair,
|
||||
reply_key_storage,
|
||||
reply_controller_sender,
|
||||
metrics_reporter,
|
||||
task_client.fork("received_messages_buffer"),
|
||||
);
|
||||
|
||||
ReceivedMessagesBufferController {
|
||||
fragmented_message_receiver: FragmentedMessageReceiver::new(
|
||||
received_buffer.clone(),
|
||||
mixnet_packet_receiver,
|
||||
task_client.fork("fragmented_message_receiver"),
|
||||
),
|
||||
request_receiver: RequestReceiver::new(
|
||||
received_buffer,
|
||||
query_receiver,
|
||||
task_client.with_suffix("request_receiver"),
|
||||
),
|
||||
request_receiver: RequestReceiver::new(received_buffer, query_receiver),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(self, shutdown: nym_task::TaskClient) {
|
||||
pub fn start(self) {
|
||||
let mut fragmented_message_receiver = self.fragmented_message_receiver;
|
||||
let mut request_receiver = self.request_receiver;
|
||||
|
||||
let shutdown_handle = shutdown.fork("fragmented_message_receiver");
|
||||
spawn_future(async move {
|
||||
match fragmented_message_receiver
|
||||
.run_with_shutdown(shutdown_handle)
|
||||
.await
|
||||
{
|
||||
match fragmented_message_receiver.run().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
});
|
||||
spawn_future(async move {
|
||||
request_receiver
|
||||
.run_with_shutdown(shutdown.with_suffix("request_receiver"))
|
||||
.await;
|
||||
request_receiver.run().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurb;
|
||||
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
|
||||
use nym_task::connections::{ConnectionId, TransmissionLane};
|
||||
use nym_task::TaskClient;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::btree_map::Entry;
|
||||
@@ -68,6 +69,9 @@ pub struct ReplyController<R> {
|
||||
|
||||
message_handler: MessageHandler<R>,
|
||||
full_reply_storage: CombinedReplyStorage,
|
||||
|
||||
// Listen for shutdown signals
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> ReplyController<R>
|
||||
@@ -79,6 +83,7 @@ where
|
||||
message_handler: MessageHandler<R>,
|
||||
full_reply_storage: CombinedReplyStorage,
|
||||
request_receiver: ReplyControllerReceiver,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
ReplyController {
|
||||
config,
|
||||
@@ -87,6 +92,7 @@ where
|
||||
pending_retransmissions: HashMap::new(),
|
||||
message_handler,
|
||||
full_reply_storage,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +102,7 @@ where
|
||||
fragments: I,
|
||||
lane: TransmissionLane,
|
||||
) {
|
||||
trace!("buffering pending replies for {recipient}");
|
||||
self.pending_replies
|
||||
.entry(*recipient)
|
||||
.or_insert_with(TransmissionBuffer::new)
|
||||
@@ -107,6 +114,7 @@ where
|
||||
recipient: &AnonymousSenderTag,
|
||||
fragments: Vec<(TransmissionLane, Fragment)>,
|
||||
) {
|
||||
trace!("re-inserting pending replies for {recipient}");
|
||||
// the buffer should ALWAYS exist at this point, if it doesn't, it's a bug...
|
||||
self.pending_replies
|
||||
.entry(*recipient)
|
||||
@@ -119,6 +127,7 @@ where
|
||||
recipient: &AnonymousSenderTag,
|
||||
data: Vec<Arc<PendingAcknowledgement>>,
|
||||
) {
|
||||
trace!("re-inserting pending retransmissions for {recipient}");
|
||||
// the underlying entry MUST exist as we've just got data from there
|
||||
let map_entry = self
|
||||
.pending_retransmissions
|
||||
@@ -136,7 +145,7 @@ where
|
||||
}
|
||||
|
||||
fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool {
|
||||
trace!("checking if we should request more surbs from {:?}", target);
|
||||
trace!("checking if we should request more surbs from {target}");
|
||||
|
||||
let pending_queue_size = self
|
||||
.pending_replies
|
||||
@@ -152,11 +161,6 @@ where
|
||||
|
||||
let total_queue = pending_queue_size + retransmission_queue;
|
||||
|
||||
// simple as that - there's absolutely nothing to retransmit
|
||||
if total_queue == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let available_surbs = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
@@ -173,11 +177,27 @@ where
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.max_surb_threshold();
|
||||
let min_surbs_threshold_buffer =
|
||||
self.config.reply_surbs.minimum_reply_surb_threshold_buffer;
|
||||
|
||||
debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}");
|
||||
// After clearing the queue, we want to have at least `min_surbs_threshold` surbs available
|
||||
// and reserved for requesting additional surbs, and in addition to that we also want to
|
||||
// have `min_surbs_threshold_buffer` surbs available proactively.
|
||||
let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer;
|
||||
|
||||
(pending_surbs + available_surbs) < max_surbs_threshold
|
||||
&& (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold)
|
||||
// Check if we have enough surbs to handle the total queue and maintain minimum thresholds
|
||||
let total_required_surbs = total_queue + target_surbs_after_clearing_queue;
|
||||
let total_available_surbs = pending_surbs + available_surbs;
|
||||
|
||||
debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, 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
|
||||
// 2. We don't have enough surbs to handle the queue plus minimum thresholds
|
||||
let is_below_max_threshold = total_available_surbs < max_surbs_threshold;
|
||||
let is_below_required_surbs = total_available_surbs < total_required_surbs;
|
||||
|
||||
is_below_max_threshold && is_below_required_surbs
|
||||
}
|
||||
|
||||
async fn handle_send_reply(
|
||||
@@ -238,6 +258,10 @@ where
|
||||
&recipient_tag,
|
||||
);
|
||||
warn!("failed to send reply to {recipient_tag}: {err}");
|
||||
info!(
|
||||
"buffering {no_fragments} fragments for {recipient_tag}",
|
||||
no_fragments = to_send.len()
|
||||
);
|
||||
self.insert_pending_replies(&recipient_tag, to_send, lane);
|
||||
}
|
||||
}
|
||||
@@ -245,6 +269,13 @@ where
|
||||
|
||||
// if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it
|
||||
if !fragments.is_empty() {
|
||||
// Ideally we should have enough surbs above the minimum threshold to handle sending
|
||||
// new replies without having to first request more surbs. That's why I'd like to log
|
||||
// these cases as they might indicate a problem with the surb management.
|
||||
debug!(
|
||||
"buffering {no_fragments} fragments for {recipient_tag}",
|
||||
no_fragments = fragments.len()
|
||||
);
|
||||
self.insert_pending_replies(&recipient_tag, fragments, lane);
|
||||
}
|
||||
|
||||
@@ -259,6 +290,7 @@ where
|
||||
target: AnonymousSenderTag,
|
||||
amount: u32,
|
||||
) -> Result<(), PreparationError> {
|
||||
debug!("requesting {amount} additional reply surbs for {target}");
|
||||
let reply_surb = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
@@ -680,7 +712,7 @@ where
|
||||
// it should take into consideration the average latency, sending rate and queue size.
|
||||
// it should request as many surbs as it takes to saturate its sending rate before next batch arrives
|
||||
async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) {
|
||||
trace!("requesting surbs for queues clearing");
|
||||
trace!("requesting surbs for queue clearing");
|
||||
|
||||
let pending_queue_size = self
|
||||
.pending_replies
|
||||
@@ -694,17 +726,18 @@ where
|
||||
.map(|pending_queue| pending_queue.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32;
|
||||
|
||||
let total_queue = (pending_queue_size + retransmission_queue) as u32;
|
||||
|
||||
if total_queue == 0 {
|
||||
trace!("the pending queues for {:?} are already empty", target);
|
||||
return;
|
||||
}
|
||||
// To proactively request additional surbs, we aim to have a buffer of extra surbs in our
|
||||
// storage.
|
||||
let total_queue_with_buffer = total_queue + min_surbs_buffer;
|
||||
|
||||
let request_size = min(
|
||||
self.config.reply_surbs.maximum_reply_surb_request_size,
|
||||
max(
|
||||
total_queue,
|
||||
total_queue_with_buffer,
|
||||
self.config.reply_surbs.minimum_reply_surb_request_size,
|
||||
),
|
||||
);
|
||||
@@ -846,9 +879,11 @@ where
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub(crate) async fn run(&mut self) {
|
||||
debug!("Started ReplyController with graceful shutdown support");
|
||||
|
||||
let mut shutdown = self.task_client.fork("select");
|
||||
|
||||
let polling_rate = Duration::from_secs(5);
|
||||
let mut stale_inspection = new_interval_stream(polling_rate);
|
||||
|
||||
@@ -860,7 +895,7 @@ where
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv_with_delay() => {
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("ReplyController: Received shutdown");
|
||||
},
|
||||
req = self.request_receiver.next() => match req {
|
||||
|
||||
@@ -15,6 +15,27 @@ pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerR
|
||||
(tx.into(), rx)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ReplyControllerSenderError {
|
||||
#[error("failed to send retransmission data to reply controller")]
|
||||
SendRetransmissionData(#[source] mpsc::TrySendError<ReplyControllerMessage>),
|
||||
|
||||
#[error("failed to send reply to reply controller")]
|
||||
SendReply(#[source] mpsc::TrySendError<ReplyControllerMessage>),
|
||||
|
||||
#[error("failed to send additional surbs to reply controller")]
|
||||
AdditionalSurbs(#[source] mpsc::TrySendError<ReplyControllerMessage>),
|
||||
|
||||
#[error("failed to send additional surbs request to reply controller")]
|
||||
AdditionalSurbsRequest(#[source] mpsc::TrySendError<ReplyControllerMessage>),
|
||||
|
||||
#[error("failed to request lane queue length from reply controller")]
|
||||
LaneQueueLength(#[source] mpsc::TrySendError<ReplyControllerMessage>),
|
||||
|
||||
#[error("response channel was dropped before we could receive the response")]
|
||||
ResponseChannelDropped(#[source] oneshot::Canceled),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplyControllerSender(mpsc::UnboundedSender<ReplyControllerMessage>);
|
||||
|
||||
@@ -30,14 +51,14 @@ impl ReplyControllerSender {
|
||||
recipient: AnonymousSenderTag,
|
||||
timed_out_ack: Weak<PendingAcknowledgement>,
|
||||
extra_surb_request: bool,
|
||||
) {
|
||||
) -> Result<(), ReplyControllerSenderError> {
|
||||
self.0
|
||||
.unbounded_send(ReplyControllerMessage::RetransmitReply {
|
||||
recipient,
|
||||
timed_out_ack,
|
||||
extra_surb_request,
|
||||
})
|
||||
.expect("ReplyControllerReceiver has died!")
|
||||
.map_err(ReplyControllerSenderError::SendRetransmissionData)
|
||||
}
|
||||
|
||||
pub(crate) fn send_reply(
|
||||
@@ -45,14 +66,14 @@ impl ReplyControllerSender {
|
||||
recipient: AnonymousSenderTag,
|
||||
message: Vec<u8>,
|
||||
lane: TransmissionLane,
|
||||
) {
|
||||
) -> Result<(), ReplyControllerSenderError> {
|
||||
self.0
|
||||
.unbounded_send(ReplyControllerMessage::SendReply {
|
||||
recipient,
|
||||
message,
|
||||
lane,
|
||||
})
|
||||
.expect("ReplyControllerReceiver has died!")
|
||||
.map_err(ReplyControllerSenderError::SendReply)
|
||||
}
|
||||
|
||||
pub(crate) fn send_additional_surbs(
|
||||
@@ -60,42 +81,47 @@ impl ReplyControllerSender {
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
from_surb_request: bool,
|
||||
) {
|
||||
) -> Result<(), ReplyControllerSenderError> {
|
||||
self.0
|
||||
.unbounded_send(ReplyControllerMessage::AdditionalSurbs {
|
||||
sender_tag,
|
||||
reply_surbs,
|
||||
from_surb_request,
|
||||
})
|
||||
.expect("ReplyControllerReceiver has died!")
|
||||
.map_err(ReplyControllerSenderError::AdditionalSurbs)
|
||||
}
|
||||
|
||||
pub(crate) fn send_additional_surbs_request(&self, recipient: Recipient, amount: u32) {
|
||||
pub(crate) fn send_additional_surbs_request(
|
||||
&self,
|
||||
recipient: Recipient,
|
||||
amount: u32,
|
||||
) -> Result<(), ReplyControllerSenderError> {
|
||||
self.0
|
||||
.unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest {
|
||||
recipient: Box::new(recipient),
|
||||
amount,
|
||||
})
|
||||
.expect("ReplyControllerReceiver has died!")
|
||||
.map_err(ReplyControllerSenderError::AdditionalSurbsRequest)
|
||||
}
|
||||
|
||||
pub async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> usize {
|
||||
pub async fn get_lane_queue_length(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<usize, ReplyControllerSenderError> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.0
|
||||
if let Err(err) = self
|
||||
.0
|
||||
.unbounded_send(ReplyControllerMessage::LaneQueueLength {
|
||||
connection_id,
|
||||
response_channel: response_tx,
|
||||
})
|
||||
.expect("ReplyControllerReceiver has died!");
|
||||
|
||||
match response_rx.await {
|
||||
Ok(length) => length,
|
||||
Err(_) => {
|
||||
error!("The reply controller has dropped our response channel!");
|
||||
// TODO: should we panic here instead? this message implies something weird and unrecoverable has happened
|
||||
0
|
||||
}
|
||||
{
|
||||
return Err(ReplyControllerSenderError::LaneQueueLength(err));
|
||||
}
|
||||
|
||||
response_rx
|
||||
.await
|
||||
.map_err(ReplyControllerSenderError::ResponseChannelDropped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +136,10 @@ impl ReplyQueueLengths {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> usize {
|
||||
pub async fn get_lane_queue_length(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<usize, ReplyControllerSenderError> {
|
||||
self.reply_controller_sender
|
||||
.get_lane_queue_length(connection_id)
|
||||
.await
|
||||
@@ -120,7 +149,7 @@ impl ReplyQueueLengths {
|
||||
pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver<ReplyControllerMessage>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ReplyControllerMessage {
|
||||
pub enum ReplyControllerMessage {
|
||||
RetransmitReply {
|
||||
recipient: AnonymousSenderTag,
|
||||
timed_out_ack: Weak<PendingAcknowledgement>,
|
||||
|
||||
@@ -22,7 +22,7 @@ use nym_sphinx::addressing::Recipient;
|
||||
use nym_statistics_common::clients::{
|
||||
ClientStatsController, ClientStatsReceiver, ClientStatsSender,
|
||||
};
|
||||
use nym_task::connections::TransmissionLane;
|
||||
use nym_task::{connections::TransmissionLane, TaskClient};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
@@ -51,6 +51,9 @@ pub(crate) struct StatisticsControl {
|
||||
|
||||
/// Config for stats reporting (enabled, address, interval)
|
||||
reporting_config: StatsReporting,
|
||||
|
||||
/// Task client for listening for shutdown
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl StatisticsControl {
|
||||
@@ -59,19 +62,24 @@ impl StatisticsControl {
|
||||
client_type: String,
|
||||
client_stats_id: String,
|
||||
report_tx: InputMessageSender,
|
||||
task_client: TaskClient,
|
||||
) -> (Self, ClientStatsSender) {
|
||||
let (stats_tx, stats_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let stats = ClientStatsController::new(client_stats_id, client_type);
|
||||
|
||||
let mut task_client_stats_sender = task_client.fork("stats_sender");
|
||||
task_client_stats_sender.disarm();
|
||||
|
||||
(
|
||||
StatisticsControl {
|
||||
stats,
|
||||
stats_rx,
|
||||
report_tx,
|
||||
reporting_config,
|
||||
task_client,
|
||||
},
|
||||
ClientStatsSender::new(Some(stats_tx)),
|
||||
ClientStatsSender::new(Some(stats_tx), task_client_stats_sender),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,7 +99,7 @@ impl StatisticsControl {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_with_shutdown(&mut self, mut task_client: nym_task::TaskClient) {
|
||||
async fn run(&mut self) {
|
||||
log::debug!("Started StatisticsControl with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -121,10 +129,10 @@ impl StatisticsControl {
|
||||
let mut snapshot_interval =
|
||||
gloo_timers::future::IntervalStream::new(SNAPSHOT_INTERVAL.as_millis() as u32);
|
||||
|
||||
loop {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = task_client.recv() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("StatisticsControl: Received shutdown");
|
||||
break;
|
||||
},
|
||||
@@ -149,29 +157,34 @@ impl StatisticsControl {
|
||||
}
|
||||
|
||||
_ = local_report_interval.next() => {
|
||||
self.stats.local_report(&mut task_client);
|
||||
self.stats.local_report(&mut self.task_client);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("StatisticsControl: Exiting");
|
||||
}
|
||||
|
||||
pub(crate) fn start_with_shutdown(mut self, task_client: nym_task::TaskClient) {
|
||||
pub(crate) fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
self.run_with_shutdown(task_client).await;
|
||||
self.run().await;
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_start_with_shutdown(
|
||||
pub(crate) fn create_and_start(
|
||||
reporting_config: StatsReporting,
|
||||
client_type: String,
|
||||
client_stats_id: String,
|
||||
report_tx: InputMessageSender,
|
||||
task_client: nym_task::TaskClient,
|
||||
task_client: TaskClient,
|
||||
) -> ClientStatsSender {
|
||||
let (controller, sender) =
|
||||
Self::create(reporting_config, client_type, client_stats_id, report_tx);
|
||||
controller.start_with_shutdown(task_client);
|
||||
let (controller, sender) = Self::create(
|
||||
reporting_config,
|
||||
client_type,
|
||||
client_stats_id,
|
||||
report_tx,
|
||||
task_client,
|
||||
);
|
||||
controller.start();
|
||||
sender
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_task::TaskClient;
|
||||
use nym_topology::NymTopologyError;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -43,6 +44,8 @@ pub struct TopologyRefresher {
|
||||
|
||||
refresh_rate: Duration,
|
||||
consecutive_failure_count: usize,
|
||||
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl TopologyRefresher {
|
||||
@@ -50,12 +53,14 @@ impl TopologyRefresher {
|
||||
cfg: TopologyRefresherConfig,
|
||||
topology_accessor: TopologyAccessor,
|
||||
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
TopologyRefresher {
|
||||
topology_provider,
|
||||
topology_accessor,
|
||||
refresh_rate: cfg.refresh_rate,
|
||||
consecutive_failure_count: 0,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +147,7 @@ impl TopologyRefresher {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
|
||||
pub fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
@@ -155,17 +160,17 @@ impl TopologyRefresher {
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = shutdown.recv() => {
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ impl<T> TransmissionBuffer<T> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn num_lanes(&self) -> usize {
|
||||
self.buffer.keys().count()
|
||||
pub(crate) fn lanes(&self) -> Vec<TransmissionLane> {
|
||||
self.buffer.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option<usize> {
|
||||
@@ -83,6 +83,7 @@ impl<T> TransmissionBuffer<T> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn total_size_in_bytes(&self) -> usize
|
||||
where
|
||||
T: SizedData,
|
||||
|
||||
@@ -222,6 +222,9 @@ pub enum ClientCoreError {
|
||||
"fresh registration with gateway {gateway_id} somehow requires an additional key upgrade!"
|
||||
)]
|
||||
UnexpectedKeyUpgrade { gateway_id: String },
|
||||
|
||||
#[error("failed to derive keys from master key")]
|
||||
HkdfDerivationError {},
|
||||
}
|
||||
|
||||
/// Set of messages that the client can send to listeners via the task manager
|
||||
|
||||
@@ -1053,7 +1053,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
connection: SocketState::NotConnected,
|
||||
packet_router,
|
||||
bandwidth_controller: None,
|
||||
stats_reporter: ClientStatsSender::new(None),
|
||||
stats_reporter: ClientStatsSender::new(None, task_client.clone()),
|
||||
negotiated_protocol: None,
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback: None,
|
||||
|
||||
@@ -28,7 +28,6 @@ use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::time::SystemTime;
|
||||
use tendermint_rpc::endpoint::block::Response as BlockResponse;
|
||||
use tendermint_rpc::endpoint::*;
|
||||
use tendermint_rpc::{Error as TendermintRpcError, Order};
|
||||
use url::Url;
|
||||
@@ -63,6 +62,7 @@ pub use cw3;
|
||||
pub use cw4;
|
||||
pub use cw_controllers;
|
||||
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
pub use tendermint_rpc::endpoint::block::Response as BlockResponse;
|
||||
pub use tendermint_rpc::{
|
||||
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
|
||||
query::Query,
|
||||
|
||||
@@ -24,6 +24,7 @@ ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
serde_bytes = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
subtle-encoding = { workspace = true, features = ["bech32-preview"] }
|
||||
thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||
@@ -36,11 +37,13 @@ nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
outfox = ["nym-sphinx-types/outfox"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -202,6 +202,18 @@ impl PemStorableKey for PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<x25519_dalek::PublicKey> for PublicKey {
|
||||
fn from(public_key: x25519_dalek::PublicKey) -> Self {
|
||||
PublicKey(public_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for x25519_dalek::PublicKey {
|
||||
fn from(public_key: PublicKey) -> Self {
|
||||
public_key.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
@@ -308,109 +320,15 @@ impl PemStorableKey for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
// compatibility with sphinx keys:
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<PublicKey> for nym_sphinx_types::PublicKey {
|
||||
fn from(key: PublicKey) -> Self {
|
||||
nym_sphinx_types::PublicKey::from(key.to_bytes())
|
||||
impl From<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
fn from(secret: x25519_dalek::StaticSecret) -> Self {
|
||||
PrivateKey(secret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl<'a> From<&'a PublicKey> for nym_sphinx_types::PublicKey {
|
||||
fn from(key: &'a PublicKey) -> Self {
|
||||
nym_sphinx_types::PublicKey::from((*key).to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<nym_sphinx_types::PublicKey> for PublicKey {
|
||||
fn from(pub_key: nym_sphinx_types::PublicKey) -> Self {
|
||||
Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<PrivateKey> for nym_sphinx_types::PrivateKey {
|
||||
fn from(key: PrivateKey) -> Self {
|
||||
nym_sphinx_types::PrivateKey::from(key.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl<'a> From<&'a PrivateKey> for nym_sphinx_types::PrivateKey {
|
||||
fn from(key: &'a PrivateKey) -> Self {
|
||||
nym_sphinx_types::PrivateKey::from(key.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
impl From<nym_sphinx_types::PrivateKey> for PrivateKey {
|
||||
fn from(private_key: nym_sphinx_types::PrivateKey) -> Self {
|
||||
let private_key_bytes = private_key.to_bytes();
|
||||
assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE);
|
||||
Self::from_bytes(&private_key_bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sphinx_key_conversion {
|
||||
use super::*;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
pub(super) fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
}
|
||||
|
||||
const NUM_ITERATIONS: usize = 100;
|
||||
|
||||
#[test]
|
||||
fn works_for_forward_conversion() {
|
||||
let mut rng = test_rng();
|
||||
|
||||
for _ in 0..NUM_ITERATIONS {
|
||||
let keys = KeyPair::new(&mut rng);
|
||||
let private = &keys.private_key;
|
||||
let public = &keys.public_key;
|
||||
|
||||
let dummy_remote = KeyPair::new(&mut rng);
|
||||
let dh1 = private.diffie_hellman(&dummy_remote.public_key);
|
||||
|
||||
let public_bytes = public.to_bytes();
|
||||
|
||||
let sphinx_private: nym_sphinx_types::PrivateKey = private.into();
|
||||
let recovered_private = PrivateKey::from(sphinx_private);
|
||||
|
||||
let dh2 = recovered_private.diffie_hellman(&dummy_remote.public_key);
|
||||
|
||||
let sphinx_public: nym_sphinx_types::PublicKey = public.into();
|
||||
let recovered_public = PublicKey::from(sphinx_public);
|
||||
assert_eq!(public_bytes, recovered_public.to_bytes());
|
||||
|
||||
// even though the byte representation of the private key changed, the resultant DH is the same
|
||||
// which is what matters
|
||||
assert_eq!(dh1, dh2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works_for_backward_conversion() {
|
||||
for _ in 0..NUM_ITERATIONS {
|
||||
let (sphinx_private, sphinx_public) = nym_sphinx_types::crypto::keygen();
|
||||
|
||||
let private_bytes = sphinx_private.to_bytes();
|
||||
let public_bytes = sphinx_public.as_bytes();
|
||||
|
||||
let private: PrivateKey = sphinx_private.into();
|
||||
let recovered_sphinx_private: nym_sphinx_types::PrivateKey = private.into();
|
||||
|
||||
let public: PublicKey = sphinx_public.into();
|
||||
let recovered_sphinx_public: nym_sphinx_types::PublicKey = public.into();
|
||||
assert_eq!(private_bytes, recovered_sphinx_private.to_bytes());
|
||||
assert_eq!(public_bytes, recovered_sphinx_public.as_bytes());
|
||||
}
|
||||
impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
fn as_ref(&self) -> &x25519_dalek::StaticSecret {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub use ed25519_dalek::SignatureError;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use ed25519_dalek::{SecretKey, Signer, SigningKey};
|
||||
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
@@ -18,7 +18,7 @@ pub mod serde_helpers;
|
||||
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
|
||||
|
||||
#[cfg(feature = "rand")]
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand::{CryptoRng, Rng, RngCore};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::Error as SerdeError;
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -62,16 +62,33 @@ pub struct KeyPair {
|
||||
// nothing secret about public key
|
||||
#[zeroize(skip)]
|
||||
public_key: PublicKey,
|
||||
|
||||
#[zeroize(skip)]
|
||||
index: u32,
|
||||
}
|
||||
|
||||
/// All keys will always have an index field populated this is to prevent anyone from figuring out if
|
||||
/// the keys are derived or random, and alter their behaviour based on that.
|
||||
impl KeyPair {
|
||||
#[cfg(feature = "rand")]
|
||||
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let index = rng.gen();
|
||||
let ed25519_signing_key = ed25519_dalek::SigningKey::generate(rng);
|
||||
|
||||
KeyPair {
|
||||
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
|
||||
public_key: PublicKey(ed25519_signing_key.verifying_key()),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_secret(secret: SecretKey, index: u32) -> Self {
|
||||
let ed25519_signing_key = SigningKey::from(secret);
|
||||
|
||||
KeyPair {
|
||||
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
|
||||
public_key: PublicKey(ed25519_signing_key.verifying_key()),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +104,31 @@ impl KeyPair {
|
||||
Ok(KeyPair {
|
||||
private_key: PrivateKey::from_bytes(priv_bytes)?,
|
||||
public_key: PublicKey::from_bytes(pub_bytes)?,
|
||||
index: fake_index(pub_bytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Reduces a byte slice into a u32 value by XOR-ing all its bytes into a 4-byte accumulator.
|
||||
/// The process iterates over every byte in the input slice, XOR-ing each one into a slot based on its index modulo 4.
|
||||
/// If the input slice contains fewer than 4 bytes, the remaining positions in the accumulator remain zero.
|
||||
/// Finally, the accumulator is interpreted in big-endian order to produce the resulting u32.
|
||||
/// Index is used to verify deterministic identity key, master key and salt are also requried for verification.
|
||||
fn fake_index(input: &[u8]) -> u32 {
|
||||
let mut accumulator = [0u8; 4];
|
||||
for (i, &byte) in input.iter().enumerate() {
|
||||
accumulator[i % 4] ^= byte;
|
||||
}
|
||||
u32::from_be_bytes(accumulator)
|
||||
}
|
||||
|
||||
impl From<PrivateKey> for KeyPair {
|
||||
fn from(private_key: PrivateKey) -> Self {
|
||||
let public_key = (&private_key).into();
|
||||
KeyPair {
|
||||
public_key: (&private_key).into(),
|
||||
public_key,
|
||||
private_key,
|
||||
index: fake_index(public_key.to_bytes().as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +148,7 @@ impl PemStorableKeyPair for KeyPair {
|
||||
KeyPair {
|
||||
private_key,
|
||||
public_key,
|
||||
index: fake_index(public_key.to_bytes().as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ use hkdf::{
|
||||
},
|
||||
Hkdf,
|
||||
};
|
||||
use sha2::{Sha256, Sha512};
|
||||
|
||||
pub use hkdf::InvalidLength;
|
||||
use zeroize::ZeroizeOnDrop;
|
||||
|
||||
/// Perform HKDF `extract` then `expand` as a single step.
|
||||
pub fn extract_then_expand<D>(
|
||||
@@ -28,3 +32,80 @@ where
|
||||
|
||||
Ok(okm)
|
||||
}
|
||||
|
||||
/// `DerivationMaterial` encapsulates parameters for deterministic key derivation using
|
||||
/// HKDF (SHA-512).
|
||||
///
|
||||
/// It consists of:
|
||||
/// - A master key (`master_key`): the base secret.
|
||||
/// - An index (`index`): ensures unique derivations.
|
||||
/// - A salt (`salt`): adds additional uniqueness, should be application specific.
|
||||
///
|
||||
/// Use the `derive_secret()` method to generate a 32-byte secret. To prepare for a new derivation,
|
||||
/// call the `next()` method, which increments the index. **It is the caller's responsibility to
|
||||
/// track and persist the derivation index if keys need to be rederived.**
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use nym_crypto::hkdf::DerivationMaterial;
|
||||
///
|
||||
/// let master_key = [0u8; 32]; // your secret master key
|
||||
/// let salt = b"unique-salt-value";
|
||||
/// let material = DerivationMaterial::new(master_key, 0, salt);
|
||||
///
|
||||
/// // Derive a secret
|
||||
/// let secret = material.derive_secret().expect("Failed to derive secret");
|
||||
///
|
||||
/// // Prepare for the next derivation
|
||||
/// let next_material = material.next();
|
||||
/// ```
|
||||
#[derive(ZeroizeOnDrop)]
|
||||
pub struct DerivationMaterial {
|
||||
master_key: [u8; 32],
|
||||
index: u32,
|
||||
salt: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DerivationMaterial {
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
pub fn salt(&self) -> &[u8] {
|
||||
&self.salt
|
||||
}
|
||||
|
||||
/// Derives a 32-byte seed from a master seed and an index using HKDF (with SHA-512).
|
||||
///
|
||||
/// The `salt` and the use of the index (as info) bind this derivation to an application/client.
|
||||
pub fn derive_secret(&self) -> Result<[u8; 32], hkdf::InvalidLength> {
|
||||
let salt = &self.salt;
|
||||
let info = self.index.to_be_bytes(); // Use the index as info
|
||||
let hk = Hkdf::<Sha512>::new(Some(salt), &self.master_key);
|
||||
let mut okm = [0u8; 32];
|
||||
hk.expand(&info, &mut okm)?;
|
||||
Ok(okm)
|
||||
}
|
||||
|
||||
pub fn new<T: AsRef<[u8]>>(master_key: T, index: u32, salt: &[u8]) -> Self {
|
||||
// Coerce master_key to [u8; 32]
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(master_key.as_ref());
|
||||
let master_key = hasher.finalize().into();
|
||||
|
||||
Self {
|
||||
master_key,
|
||||
index,
|
||||
salt: salt.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Self {
|
||||
Self {
|
||||
master_key: self.master_key,
|
||||
index: self.index + 1,
|
||||
salt: self.salt.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ pub fn compute_keyed_hmac<D>(key: &[u8], data: &[u8]) -> HmacOutput<D>
|
||||
where
|
||||
D: Digest + BlockSizeUser,
|
||||
{
|
||||
let mut hmac = SimpleHmac::<D>::new_from_slice(key)
|
||||
.expect("HMAC was instantiated with a key of an invalid size!");
|
||||
// SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying
|
||||
// digest, they're padded with 0. if they're larger they're hashed and padded
|
||||
// the reason for `Result` return type is due to the trait definition
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mut hmac = SimpleHmac::<D>::new_from_slice(key).unwrap();
|
||||
hmac.update(data);
|
||||
hmac.finalize()
|
||||
}
|
||||
@@ -27,8 +30,11 @@ pub fn recompute_keyed_hmac_and_verify_tag<D>(key: &[u8], data: &[u8], tag: &[u8
|
||||
where
|
||||
D: Digest + BlockSizeUser,
|
||||
{
|
||||
let mut hmac = SimpleHmac::<D>::new_from_slice(key)
|
||||
.expect("HMAC was instantiated with a key of an invalid size!");
|
||||
// SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying
|
||||
// digest, they're padded with 0. if they're larger they're hashed and padded
|
||||
// the reason for `Result` return type is due to the trait definition
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mut hmac = SimpleHmac::<D>::new_from_slice(key).unwrap();
|
||||
hmac.update(data);
|
||||
|
||||
let tag_arr = Output::<D>::from_slice(tag);
|
||||
|
||||
@@ -27,12 +27,16 @@ where
|
||||
// after performing diffie-hellman we don't care about the private component anymore
|
||||
let dh_result = ephemeral_keypair.private_key().diffie_hellman(remote_key);
|
||||
|
||||
// there is no reason for this to fail as our okm is expected to be only C::KeySize bytes
|
||||
// SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()`
|
||||
// larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used)
|
||||
#[allow(clippy::expect_used)]
|
||||
let okm = hkdf::extract_then_expand::<D>(None, &dh_result, None, C::key_size())
|
||||
.expect("somehow too long okm was provided");
|
||||
|
||||
let derived_shared_key =
|
||||
Key::<C>::from_exact_iter(okm).expect("okm was expanded to incorrect length!");
|
||||
// SAFETY: the generated okm has exactly `C::key_size()` elements,
|
||||
// so this call is safe
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let derived_shared_key = Key::<C>::from_exact_iter(okm).unwrap();
|
||||
|
||||
(ephemeral_keypair, derived_shared_key)
|
||||
}
|
||||
@@ -48,9 +52,14 @@ where
|
||||
{
|
||||
let dh_result = local_key.diffie_hellman(remote_key);
|
||||
|
||||
// there is no reason for this to fail as our okm is expected to be only C::KeySize bytes
|
||||
// SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()`
|
||||
// larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used)
|
||||
#[allow(clippy::expect_used)]
|
||||
let okm = hkdf::extract_then_expand::<D>(None, &dh_result, None, C::key_size())
|
||||
.expect("somehow too long okm was provided");
|
||||
|
||||
Key::<C>::from_exact_iter(okm).expect("okm was expanded to incorrect length!")
|
||||
// SAFETY: the generated okm has exactly `C::key_size()` elements,
|
||||
// so this call is safe
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Key::<C>::from_exact_iter(okm).unwrap()
|
||||
}
|
||||
|
||||
@@ -60,20 +60,15 @@ where
|
||||
Iv::<C>::default()
|
||||
}
|
||||
|
||||
pub fn iv_from_slice<C>(b: &[u8]) -> &IV<C>
|
||||
pub fn try_iv_from_slice<C>(b: &[u8]) -> Option<&IV<C>>
|
||||
where
|
||||
C: IvSizeUser,
|
||||
{
|
||||
if b.len() != C::iv_size() {
|
||||
// `from_slice` would have caused a panic about this issue anyway.
|
||||
// Now we at least have slightly more information
|
||||
panic!(
|
||||
"Tried to convert {} bytes to IV. Expected {}",
|
||||
b.len(),
|
||||
C::iv_size()
|
||||
)
|
||||
None
|
||||
} else {
|
||||
Some(IV::<C>::from_slice(b))
|
||||
}
|
||||
IV::<C>::from_slice(b)
|
||||
}
|
||||
|
||||
// TODO: there's really no way to use more parts of the keystream if it was required at some point.
|
||||
|
||||
@@ -12,7 +12,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
|
||||
use crate::ClientBuilder;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use hickory_resolver::lookup_ip::LookupIp;
|
||||
use hickory_resolver::{
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
error::ResolveError,
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
TokioAsyncResolver,
|
||||
};
|
||||
use hickory_resolver::{error::ResolveErrorKind, lookup_ip::LookupIp};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use tracing::warn;
|
||||
@@ -38,6 +41,14 @@ struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
|
||||
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
|
||||
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
|
||||
static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
|
||||
tracing::debug!("Initializing shared DNS resolver");
|
||||
HickoryDnsResolver::default()
|
||||
});
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("hickory-dns resolver error: {hickory_error}")]
|
||||
/// Error occurring while resolving a hostname into an IP address.
|
||||
@@ -47,29 +58,62 @@ pub struct HickoryDnsError {
|
||||
}
|
||||
|
||||
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
|
||||
///
|
||||
/// Typical use involves instantiating using the `Default` implementation and then resolving using
|
||||
/// methods or trait implementations.
|
||||
///
|
||||
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
|
||||
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
|
||||
/// internal `AsyncResolver`.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct HickoryDnsResolver {
|
||||
/// Since we might not have been called in the context of a
|
||||
/// Tokio Runtime in initialization, so we must delay the actual
|
||||
/// construction of the resolver.
|
||||
// Since we might not have been called in the context of a
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioAsyncResolver>>,
|
||||
fallback: Arc<OnceCell<TokioAsyncResolver>>,
|
||||
dont_use_shared: bool,
|
||||
}
|
||||
|
||||
impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let resolver = self.state.clone();
|
||||
let fallback = self.fallback.clone();
|
||||
let independent = self.dont_use_shared;
|
||||
Box::pin(async move {
|
||||
let resolver = resolver.get_or_try_init(new_resolver)?;
|
||||
let resolver = resolver.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
})?;
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name.as_str()).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
let resolver = fallback.get_or_try_init(new_resolver_system)?;
|
||||
match e.kind() {
|
||||
ResolveErrorKind::NoRecordsFound { .. } => {}
|
||||
_ => {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
}
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
})?;
|
||||
resolver.lookup_ip(name.as_str()).await?
|
||||
}
|
||||
};
|
||||
@@ -93,21 +137,55 @@ impl Iterator for SocketAddrs {
|
||||
impl HickoryDnsResolver {
|
||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
|
||||
let resolver = self.state.get_or_try_init(new_resolver)?;
|
||||
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
|
||||
|
||||
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
|
||||
let lookup = match resolver.lookup_ip(name).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
let resolver = self.fallback.get_or_try_init(new_resolver_system)?;
|
||||
match e.kind() {
|
||||
ResolveErrorKind::NoRecordsFound { .. } => {}
|
||||
_ => {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
}
|
||||
let resolver = self
|
||||
.fallback
|
||||
.get_or_try_init(|| self.new_resolver_system())?;
|
||||
resolver.lookup_ip(name).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(lookup)
|
||||
}
|
||||
|
||||
/// Create a (lazy-initialized) resolver that is not shared across threads.
|
||||
pub fn thread_resolver() -> Self {
|
||||
Self {
|
||||
dont_use_shared: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
new_resolver()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver_system(&self) -> Result<TokioAsyncResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
|
||||
|
||||
+424
-335
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,16 @@ use http::HeaderValue;
|
||||
use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Characteristic elements sent to the API providing basic context information of the requesting client.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct UserAgent {
|
||||
/// The internal crate / application / subsystem making use of API client
|
||||
pub application: String,
|
||||
/// version of the calling crate / application / subsystem
|
||||
pub version: String,
|
||||
/// client platform
|
||||
pub platform: String,
|
||||
/// source commit version for the calling calling crate / subsystem
|
||||
pub git_commit: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ mime = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_yaml = { workspace = true }
|
||||
subtle.workspace = true
|
||||
tower = { workspace = true }
|
||||
tracing.workspace = true
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::{extract::Request, response::Response};
|
||||
use futures::future::BoxFuture;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tower::{Layer, Service};
|
||||
use tracing::{debug, instrument, trace};
|
||||
use zeroize::Zeroizing;
|
||||
@@ -76,7 +77,7 @@ impl<S> RequireAuth<S> {
|
||||
return Err("`Authorization` header must contain non-empty `Bearer` token");
|
||||
}
|
||||
|
||||
if self.bearer_token.as_str() != bearer_token {
|
||||
if bool::from(self.bearer_token.as_bytes().ct_ne(bearer_token.as_bytes())) {
|
||||
return Err("`Authorization` header does not contain the correct `Bearer` token");
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ bincode = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-service-provider-requests-common = { path = "../service-provider-requests-common" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -2,24 +2,18 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
// The current version of the protocol.
|
||||
// The idea here is that we add new request response types at least one version before we start
|
||||
// using them.
|
||||
// Also, depending on the version in the client connect message the IPR could respond with a
|
||||
// matching older version.
|
||||
pub use v6::request;
|
||||
pub use v6::response;
|
||||
|
||||
pub mod codec;
|
||||
pub mod sign;
|
||||
pub mod v6;
|
||||
pub mod v7;
|
||||
pub mod v8;
|
||||
|
||||
// version 3: initial version
|
||||
// version 4: IPv6 support
|
||||
// version 5: Add severity level to info response
|
||||
// version 6: Increase the available IPs
|
||||
// version 7: Add signature support (for the future)
|
||||
pub const CURRENT_VERSION: u8 = 6;
|
||||
// version 8: Anonymous sends
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct IpPair {
|
||||
@@ -45,3 +39,9 @@ fn make_bincode_serializer() -> impl bincode::Options {
|
||||
.with_big_endian()
|
||||
.with_varint_encoding()
|
||||
}
|
||||
|
||||
fn generate_random() -> u64 {
|
||||
use rand::RngCore;
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
rng.next_u64()
|
||||
}
|
||||
|
||||
+18
-9
@@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// For reply protection, if a request is older than this, it will be rejected
|
||||
const MAX_REQUEST_AGE: Duration = Duration::from_secs(10);
|
||||
@@ -22,29 +23,37 @@ pub enum SignatureError {
|
||||
#[error("signature verification failed")]
|
||||
VerificationFailed {
|
||||
message: String,
|
||||
error: identity::SignatureError,
|
||||
error: ed25519::SignatureError,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait SignedRequest {
|
||||
fn identity(&self) -> &identity::PublicKey;
|
||||
fn identity(&self) -> Option<&ed25519::PublicKey>;
|
||||
|
||||
fn request(&self) -> Result<Vec<u8>, SignatureError>;
|
||||
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError>;
|
||||
|
||||
fn signature(&self) -> Option<&identity::Signature>;
|
||||
fn signature(&self) -> Option<&ed25519::Signature>;
|
||||
|
||||
fn timestamp(&self) -> time::OffsetDateTime;
|
||||
fn timestamp(&self) -> OffsetDateTime;
|
||||
|
||||
fn verify(&self) -> Result<(), SignatureError> {
|
||||
let identity = match self.identity() {
|
||||
Some(identity) => identity,
|
||||
None => {
|
||||
// If we are not revealing our identity, we don't need to verify anything
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(signature) = self.signature() {
|
||||
// First check that the request is recent enough
|
||||
if time::OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE {
|
||||
if OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE {
|
||||
return Err(SignatureError::RequestOutOfDate);
|
||||
}
|
||||
|
||||
let request_as_bytes = self.request()?;
|
||||
let request_as_bytes = self.request_as_bytes()?;
|
||||
|
||||
self.identity()
|
||||
identity
|
||||
.verify(request_as_bytes, signature)
|
||||
.map_err(|error| SignatureError::VerificationFailed {
|
||||
message: "signature verification failed".to_string(),
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{v6, v7};
|
||||
|
||||
impl From<v7::response::StaticConnectFailureReason> for v6::response::StaticConnectFailureReason {
|
||||
fn from(failure: v7::response::StaticConnectFailureReason) -> Self {
|
||||
match failure {
|
||||
v7::response::StaticConnectFailureReason::RequestedIpAlreadyInUse => {
|
||||
v6::response::StaticConnectFailureReason::RequestedIpAlreadyInUse
|
||||
}
|
||||
v7::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse => {
|
||||
v6::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse
|
||||
}
|
||||
v7::response::StaticConnectFailureReason::OutOfDateTimestamp => {
|
||||
v6::response::StaticConnectFailureReason::Other("out of date timestamp".to_string())
|
||||
}
|
||||
v7::response::StaticConnectFailureReason::Other(reason) => {
|
||||
v6::response::StaticConnectFailureReason::Other(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v7::response::DynamicConnectFailureReason> for v6::response::DynamicConnectFailureReason {
|
||||
fn from(failure: v7::response::DynamicConnectFailureReason) -> Self {
|
||||
match failure {
|
||||
v7::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse => {
|
||||
v6::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse
|
||||
}
|
||||
v7::response::DynamicConnectFailureReason::NoAvailableIp => {
|
||||
v6::response::DynamicConnectFailureReason::NoAvailableIp
|
||||
}
|
||||
v7::response::DynamicConnectFailureReason::Other(err) => {
|
||||
v6::response::DynamicConnectFailureReason::Other(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v7::response::InfoResponseReply> for v6::response::InfoResponseReply {
|
||||
fn from(reply: v7::response::InfoResponseReply) -> Self {
|
||||
match reply {
|
||||
v7::response::InfoResponseReply::Generic { msg } => {
|
||||
v6::response::InfoResponseReply::Generic { msg }
|
||||
}
|
||||
v7::response::InfoResponseReply::VersionMismatch {
|
||||
request_version,
|
||||
response_version,
|
||||
} => v6::response::InfoResponseReply::VersionMismatch {
|
||||
request_version,
|
||||
response_version,
|
||||
},
|
||||
v7::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => {
|
||||
v6::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v7::response::InfoLevel> for v6::response::InfoLevel {
|
||||
fn from(level: v7::response::InfoLevel) -> Self {
|
||||
match level {
|
||||
v7::response::InfoLevel::Info => v6::response::InfoLevel::Info,
|
||||
v7::response::InfoLevel::Warn => v6::response::InfoLevel::Warn,
|
||||
v7::response::InfoLevel::Error => v6::response::InfoLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod conversion;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{v6, v7};
|
||||
|
||||
impl From<v6::request::IpPacketRequest> for v7::request::IpPacketRequest {
|
||||
fn from(ip_packet_request: v6::request::IpPacketRequest) -> Self {
|
||||
Self {
|
||||
version: 7,
|
||||
data: ip_packet_request.data.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::IpPacketRequestData> for v7::request::IpPacketRequestData {
|
||||
fn from(ip_packet_request_data: v6::request::IpPacketRequestData) -> Self {
|
||||
match ip_packet_request_data {
|
||||
v6::request::IpPacketRequestData::StaticConnect(r) => {
|
||||
v7::request::IpPacketRequestData::StaticConnect(
|
||||
v7::request::SignedStaticConnectRequest {
|
||||
request: r.into(),
|
||||
signature: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
v6::request::IpPacketRequestData::DynamicConnect(r) => {
|
||||
v7::request::IpPacketRequestData::DynamicConnect(
|
||||
v7::request::SignedDynamicConnectRequest {
|
||||
request: r.into(),
|
||||
signature: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
v6::request::IpPacketRequestData::Disconnect(r) => {
|
||||
v7::request::IpPacketRequestData::Disconnect(v7::request::SignedDisconnectRequest {
|
||||
request: r.into(),
|
||||
signature: None,
|
||||
})
|
||||
}
|
||||
v6::request::IpPacketRequestData::Data(r) => {
|
||||
v7::request::IpPacketRequestData::Data(r.into())
|
||||
}
|
||||
v6::request::IpPacketRequestData::Ping(r) => {
|
||||
v7::request::IpPacketRequestData::Ping(r.into())
|
||||
}
|
||||
v6::request::IpPacketRequestData::Health(r) => {
|
||||
v7::request::IpPacketRequestData::Health(r.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::StaticConnectRequest> for v7::request::StaticConnectRequest {
|
||||
fn from(static_connect_request: v6::request::StaticConnectRequest) -> Self {
|
||||
Self {
|
||||
request_id: static_connect_request.request_id,
|
||||
ips: static_connect_request.ips,
|
||||
reply_to: static_connect_request.reply_to,
|
||||
reply_to_hops: static_connect_request.reply_to_hops,
|
||||
reply_to_avg_mix_delays: static_connect_request.reply_to_avg_mix_delays,
|
||||
buffer_timeout: static_connect_request.buffer_timeout,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl From<v6::request::DynamicConnectRequest> for v7::request::DynamicConnectRequest {
|
||||
fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self {
|
||||
Self {
|
||||
request_id: dynamic_connect_request.request_id,
|
||||
reply_to: dynamic_connect_request.reply_to,
|
||||
reply_to_hops: dynamic_connect_request.reply_to_hops,
|
||||
reply_to_avg_mix_delays: dynamic_connect_request.reply_to_avg_mix_delays,
|
||||
buffer_timeout: dynamic_connect_request.buffer_timeout,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::DisconnectRequest> for v7::request::SignedDisconnectRequest {
|
||||
fn from(disconnect_request: v6::request::DisconnectRequest) -> Self {
|
||||
Self {
|
||||
request: disconnect_request.into(),
|
||||
signature: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::DisconnectRequest> for v7::request::DisconnectRequest {
|
||||
fn from(disconnect_request: v6::request::DisconnectRequest) -> Self {
|
||||
Self {
|
||||
request_id: disconnect_request.request_id,
|
||||
reply_to: disconnect_request.reply_to,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::DataRequest> for v7::request::DataRequest {
|
||||
fn from(data_request: v6::request::DataRequest) -> Self {
|
||||
Self {
|
||||
ip_packets: data_request.ip_packets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::PingRequest> for v7::request::PingRequest {
|
||||
fn from(ping_request: v6::request::PingRequest) -> Self {
|
||||
Self {
|
||||
request_id: ping_request.request_id,
|
||||
reply_to: ping_request.reply_to,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v6::request::HealthRequest> for v7::request::HealthRequest {
|
||||
fn from(health_request: v6::request::HealthRequest) -> Self {
|
||||
Self {
|
||||
request_id: health_request.request_id,
|
||||
reply_to: health_request.reply_to,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
pub mod conversion;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod signature;
|
||||
|
||||
pub const VERSION: u8 = 7;
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
use std::fmt;
|
||||
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{make_bincode_serializer, IpPair};
|
||||
|
||||
use super::{
|
||||
signature::{SignatureError, SignedRequest},
|
||||
VERSION,
|
||||
use crate::{
|
||||
sign::{SignatureError, SignedRequest},
|
||||
IpPair,
|
||||
};
|
||||
|
||||
fn generate_random() -> u64 {
|
||||
use rand::RngCore;
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
rng.next_u64()
|
||||
}
|
||||
use super::VERSION;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IpPacketRequest {
|
||||
pub version: u8,
|
||||
pub data: IpPacketRequestData,
|
||||
@@ -30,7 +26,7 @@ impl IpPacketRequest {
|
||||
reply_to_avg_mix_delays: Option<f64>,
|
||||
buffer_timeout: Option<u64>,
|
||||
) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
let request_id = crate::generate_random();
|
||||
(
|
||||
Self {
|
||||
version: VERSION,
|
||||
@@ -58,7 +54,7 @@ impl IpPacketRequest {
|
||||
reply_to_avg_mix_delays: Option<f64>,
|
||||
buffer_timeout: Option<u64>,
|
||||
) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
let request_id = crate::generate_random();
|
||||
(
|
||||
Self {
|
||||
version: VERSION,
|
||||
@@ -79,7 +75,7 @@ impl IpPacketRequest {
|
||||
}
|
||||
|
||||
pub fn new_disconnect_request(reply_to: Recipient) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
let request_id = crate::generate_random();
|
||||
(
|
||||
Self {
|
||||
version: VERSION,
|
||||
@@ -104,7 +100,7 @@ impl IpPacketRequest {
|
||||
}
|
||||
|
||||
pub fn new_ping(reply_to: Recipient) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
let request_id = crate::generate_random();
|
||||
(
|
||||
Self {
|
||||
version: VERSION,
|
||||
@@ -119,7 +115,7 @@ impl IpPacketRequest {
|
||||
}
|
||||
|
||||
pub fn new_health_request(reply_to: Recipient) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
let request_id = crate::generate_random();
|
||||
(
|
||||
Self {
|
||||
version: VERSION,
|
||||
@@ -155,16 +151,27 @@ impl IpPacketRequest {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<(), SignatureError> {
|
||||
match &self.data {
|
||||
IpPacketRequestData::StaticConnect(request) => request.verify(),
|
||||
IpPacketRequestData::DynamicConnect(request) => request.verify(),
|
||||
IpPacketRequestData::Disconnect(request) => request.verify(),
|
||||
IpPacketRequestData::Data(_) => Ok(()),
|
||||
IpPacketRequestData::Ping(_) => Ok(()),
|
||||
IpPacketRequestData::Health(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(&message.message)
|
||||
crate::make_bincode_serializer().deserialize(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +186,19 @@ pub enum IpPacketRequestData {
|
||||
Health(HealthRequest),
|
||||
}
|
||||
|
||||
impl fmt::Display for IpPacketRequestData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IpPacketRequestData::StaticConnect(_) => write!(f, "StaticConnect"),
|
||||
IpPacketRequestData::DynamicConnect(_) => write!(f, "DynamicConnect"),
|
||||
IpPacketRequestData::Disconnect(_) => write!(f, "Disconnect"),
|
||||
IpPacketRequestData::Data(_) => write!(f, "Data"),
|
||||
IpPacketRequestData::Ping(_) => write!(f, "Ping"),
|
||||
IpPacketRequestData::Health(_) => write!(f, "Health"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpPacketRequestData {
|
||||
pub fn add_signature(&mut self, signature: identity::Signature) -> Option<identity::Signature> {
|
||||
match self {
|
||||
@@ -202,9 +222,9 @@ impl IpPacketRequestData {
|
||||
|
||||
pub fn signable_request(&self) -> Option<Result<Vec<u8>, SignatureError>> {
|
||||
match self {
|
||||
IpPacketRequestData::StaticConnect(request) => Some(request.request()),
|
||||
IpPacketRequestData::DynamicConnect(request) => Some(request.request()),
|
||||
IpPacketRequestData::Disconnect(request) => Some(request.request()),
|
||||
IpPacketRequestData::StaticConnect(request) => Some(request.request_as_bytes()),
|
||||
IpPacketRequestData::DynamicConnect(request) => Some(request.request_as_bytes()),
|
||||
IpPacketRequestData::Disconnect(request) => Some(request.request_as_bytes()),
|
||||
IpPacketRequestData::Data(_) => None,
|
||||
IpPacketRequestData::Ping(_) => None,
|
||||
IpPacketRequestData::Health(_) => None,
|
||||
@@ -242,7 +262,7 @@ pub struct StaticConnectRequest {
|
||||
impl StaticConnectRequest {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,11 +273,11 @@ pub struct SignedStaticConnectRequest {
|
||||
}
|
||||
|
||||
impl SignedRequest for SignedStaticConnectRequest {
|
||||
fn identity(&self) -> &identity::PublicKey {
|
||||
self.request.reply_to.identity()
|
||||
fn identity(&self) -> Option<&identity::PublicKey> {
|
||||
Some(self.request.reply_to.identity())
|
||||
}
|
||||
|
||||
fn request(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
self.request
|
||||
.to_bytes()
|
||||
.map_err(|error| SignatureError::RequestSerializationError {
|
||||
@@ -306,7 +326,7 @@ pub struct DynamicConnectRequest {
|
||||
impl DynamicConnectRequest {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,11 +337,11 @@ pub struct SignedDynamicConnectRequest {
|
||||
}
|
||||
|
||||
impl SignedRequest for SignedDynamicConnectRequest {
|
||||
fn identity(&self) -> &identity::PublicKey {
|
||||
self.request.reply_to.identity()
|
||||
fn identity(&self) -> Option<&identity::PublicKey> {
|
||||
Some(self.request.reply_to.identity())
|
||||
}
|
||||
|
||||
fn request(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
self.request
|
||||
.to_bytes()
|
||||
.map_err(|error| SignatureError::RequestSerializationError {
|
||||
@@ -355,7 +375,7 @@ pub struct DisconnectRequest {
|
||||
impl DisconnectRequest {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,11 +386,11 @@ pub struct SignedDisconnectRequest {
|
||||
}
|
||||
|
||||
impl SignedRequest for SignedDisconnectRequest {
|
||||
fn identity(&self) -> &identity::PublicKey {
|
||||
self.request.reply_to.identity()
|
||||
fn identity(&self) -> Option<&identity::PublicKey> {
|
||||
Some(self.request.reply_to.identity())
|
||||
}
|
||||
|
||||
fn request(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
fn request_as_bytes(&self) -> Result<Vec<u8>, SignatureError> {
|
||||
self.request
|
||||
.to_bytes()
|
||||
.map_err(|error| SignatureError::RequestSerializationError {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub const VERSION: u8 = 8;
|
||||
@@ -0,0 +1,304 @@
|
||||
use std::fmt;
|
||||
|
||||
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::VERSION;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct IpPacketRequest {
|
||||
pub protocol: Protocol,
|
||||
pub data: IpPacketRequestData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum IpPacketRequestData {
|
||||
Data(DataRequest),
|
||||
Control(Box<ControlRequest>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ControlRequest {
|
||||
Connect(ConnectRequest),
|
||||
Disconnect(DisconnectRequest),
|
||||
Ping(PingRequest),
|
||||
Health(HealthRequest),
|
||||
}
|
||||
|
||||
// A data request is when the client wants to send an IP packet to a destination.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DataRequest {
|
||||
pub ip_packets: bytes::Bytes,
|
||||
}
|
||||
|
||||
// A dynamic connect request is when the client does not provide the internal IP address it will use
|
||||
// on the ip packet router, and instead requests one to be assigned to it.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ConnectRequest {
|
||||
pub request_id: u64,
|
||||
|
||||
// The maximum time in milliseconds the IPR should wait when filling up a mix packet
|
||||
// with ip packets.
|
||||
pub buffer_timeout: Option<u64>,
|
||||
|
||||
// Timestamp of when the request was sent by the client.
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
// A disconnect request is when the client wants to disconnect from the ip packet router and free
|
||||
// up the allocated IP address.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DisconnectRequest {
|
||||
pub request_id: u64,
|
||||
|
||||
// Timestamp of when the request was sent by the client.
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
// A ping request is when the client wants to check if the ip packet router is still alive.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PingRequest {
|
||||
pub request_id: u64,
|
||||
|
||||
// Timestamp of when the request was sent by the client.
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct HealthRequest {
|
||||
pub request_id: u64,
|
||||
|
||||
// Timestamp of when the request was sent by the client.
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl IpPacketRequest {
|
||||
pub fn new_connect_request(buffer_timeout: Option<u64>) -> (Self, u64) {
|
||||
let protocol = Protocol {
|
||||
version: VERSION,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
};
|
||||
let request_id = rand::random();
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let connect = ConnectRequest {
|
||||
request_id,
|
||||
buffer_timeout,
|
||||
timestamp,
|
||||
};
|
||||
let request = Self {
|
||||
protocol,
|
||||
data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(connect))),
|
||||
};
|
||||
(request, request_id)
|
||||
}
|
||||
|
||||
pub fn new_disconnect_request() -> (Self, u64) {
|
||||
let protocol = Protocol {
|
||||
version: VERSION,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
};
|
||||
let request_id = rand::random();
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let disconnect = DisconnectRequest {
|
||||
request_id,
|
||||
timestamp,
|
||||
};
|
||||
let request = Self {
|
||||
protocol,
|
||||
data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect(disconnect))),
|
||||
};
|
||||
(request, request_id)
|
||||
}
|
||||
|
||||
pub fn new_data_request(ip_packets: bytes::Bytes) -> Self {
|
||||
Self {
|
||||
protocol: Protocol {
|
||||
version: VERSION,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
},
|
||||
data: IpPacketRequestData::Data(DataRequest { ip_packets }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_ping() -> (Self, u64) {
|
||||
let protocol = Protocol {
|
||||
version: VERSION,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
};
|
||||
let request_id = rand::random();
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let ping_request = PingRequest {
|
||||
request_id,
|
||||
timestamp,
|
||||
};
|
||||
let request = Self {
|
||||
protocol,
|
||||
data: IpPacketRequestData::Control(Box::new(ControlRequest::Ping(ping_request))),
|
||||
};
|
||||
(request, request_id)
|
||||
}
|
||||
|
||||
pub fn new_health_request() -> (Self, u64) {
|
||||
let protocol = Protocol {
|
||||
version: VERSION,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
};
|
||||
let request_id = rand::random();
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let health_request = HealthRequest {
|
||||
request_id,
|
||||
timestamp,
|
||||
};
|
||||
let request = Self {
|
||||
protocol,
|
||||
data: IpPacketRequestData::Control(Box::new(ControlRequest::Health(health_request))),
|
||||
};
|
||||
(request, request_id)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<u64> {
|
||||
match self.data {
|
||||
IpPacketRequestData::Control(ref c) => Some(c.id()),
|
||||
IpPacketRequestData::Data(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
crate::make_bincode_serializer().deserialize(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IpPacketRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"IpPacketRequest {{ version: {}, data: {} }}",
|
||||
self.protocol.version, self.data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IpPacketRequestData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IpPacketRequestData::Data(_) => write!(f, "Data"),
|
||||
IpPacketRequestData::Control(c) => write!(f, "Control({})", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlRequest {
|
||||
fn id(&self) -> u64 {
|
||||
match self {
|
||||
ControlRequest::Connect(request) => request.request_id,
|
||||
ControlRequest::Disconnect(request) => request.request_id,
|
||||
ControlRequest::Ping(request) => request.request_id,
|
||||
ControlRequest::Health(request) => request.request_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ControlRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ControlRequest::Connect(_) => write!(f, "Connect"),
|
||||
ControlRequest::Disconnect(_) => write!(f, "Disconnect"),
|
||||
ControlRequest::Ping(_) => write!(f, "Ping"),
|
||||
ControlRequest::Health(_) => write!(f, "Health"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectRequest {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DisconnectRequest {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
crate::make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::macros::datetime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_size_of_request() {
|
||||
let connect = IpPacketRequest {
|
||||
protocol: Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
},
|
||||
data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(ConnectRequest {
|
||||
request_id: 123,
|
||||
buffer_timeout: None,
|
||||
timestamp: datetime!(2024-01-01 12:59:59.5 UTC),
|
||||
}))),
|
||||
};
|
||||
assert_eq!(connect.to_bytes().unwrap().len(), 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_size_of_data() {
|
||||
let data = IpPacketRequest {
|
||||
protocol: Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
},
|
||||
data: IpPacketRequestData::Data(DataRequest {
|
||||
ip_packets: bytes::Bytes::from(vec![1u8; 32]),
|
||||
}),
|
||||
};
|
||||
assert_eq!(data.to_bytes().unwrap().len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_and_deserialize_data_request() {
|
||||
let data = IpPacketRequest {
|
||||
protocol: Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::IpPacketRouter,
|
||||
},
|
||||
data: IpPacketRequestData::Data(DataRequest {
|
||||
ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
|
||||
}),
|
||||
};
|
||||
|
||||
let serialized = data.to_bytes().unwrap();
|
||||
let deserialized = IpPacketRequest::from_reconstructed_message(
|
||||
&nym_sphinx::receiver::ReconstructedMessage {
|
||||
message: serialized,
|
||||
sender_tag: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(deserialized.protocol.version, 4);
|
||||
assert_eq!(
|
||||
deserialized.protocol.service_provider_type,
|
||||
ServiceProviderType::IpPacketRouter
|
||||
);
|
||||
assert_eq!(
|
||||
deserialized.data,
|
||||
IpPacketRequestData::Data(DataRequest {
|
||||
ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{make_bincode_serializer, IpPair};
|
||||
|
||||
use super::VERSION;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct IpPacketResponse {
|
||||
pub version: u8,
|
||||
pub data: IpPacketResponseData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum IpPacketResponseData {
|
||||
Data(DataResponse),
|
||||
Control(Box<ControlResponse>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DataResponse {
|
||||
pub ip_packet: bytes::Bytes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ControlResponse {
|
||||
// Response for a connect request
|
||||
Connect(ConnectResponse),
|
||||
|
||||
// Response for a disconnect initiqated by the client
|
||||
Disconnect(DisconnectResponse),
|
||||
|
||||
// Message from the server that the client got disconnected without the client initiating it
|
||||
UnrequestedDisconnect(UnrequestedDisconnect),
|
||||
|
||||
// Response to ping request
|
||||
Pong(PongResponse),
|
||||
|
||||
// Response for a health request
|
||||
Health(Box<HealthResponse>),
|
||||
|
||||
// Info response. This can be anything from informative messages to errors
|
||||
Info(InfoResponse),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConnectResponse {
|
||||
pub request_id: u64,
|
||||
pub reply: ConnectResponseReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ConnectResponseReply {
|
||||
Success(ConnectSuccess),
|
||||
Failure(ConnectFailureReason),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConnectSuccess {
|
||||
pub ips: IpPair,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum ConnectFailureReason {
|
||||
#[error("client is already connected")]
|
||||
ClientAlreadyConnected,
|
||||
|
||||
#[error("no available ip address")]
|
||||
NoAvailableIp,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DisconnectResponse {
|
||||
pub request_id: u64,
|
||||
pub reply: DisconnectResponseReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DisconnectResponseReply {
|
||||
Success,
|
||||
Failure(DisconnectFailureReason),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum DisconnectFailureReason {
|
||||
#[error("client is not connected")]
|
||||
ClientNotConnected,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UnrequestedDisconnect {
|
||||
pub reason: UnrequestedDisconnectReason,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum UnrequestedDisconnectReason {
|
||||
#[error("client mixnet traffic timeout")]
|
||||
ClientMixnetTrafficTimeout,
|
||||
|
||||
#[error("client tun traffic timeout")]
|
||||
ClientTunTrafficTimeout,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PongResponse {
|
||||
pub request_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HealthResponse {
|
||||
pub request_id: u64,
|
||||
pub reply: HealthResponseReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HealthResponseReply {
|
||||
// Return the binary build information of the IPR
|
||||
pub build_info: BinaryBuildInformationOwned,
|
||||
|
||||
// Return if the IPR has performed a successful routing test.
|
||||
pub routable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InfoResponse {
|
||||
pub request_id: u64,
|
||||
pub reply: InfoResponseReply,
|
||||
pub level: InfoLevel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum InfoResponseReply {
|
||||
#[error("{msg}")]
|
||||
Generic { msg: String },
|
||||
|
||||
#[error(
|
||||
"version mismatch: response is v{request_version} and response is v{response_version}"
|
||||
)]
|
||||
VersionMismatch {
|
||||
request_version: u8,
|
||||
response_version: u8,
|
||||
},
|
||||
|
||||
#[error("destination failed exit policy filter check: {dst}")]
|
||||
ExitPolicyFilterCheckFailed { dst: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum InfoLevel {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl IpPacketResponse {
|
||||
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
|
||||
Self {
|
||||
version: VERSION,
|
||||
data: IpPacketResponseData::Data(DataResponse { ip_packet }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<u64> {
|
||||
match &self.data {
|
||||
IpPacketResponseData::Data(_) => None,
|
||||
IpPacketResponseData::Control(response) => response.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl IpPacketResponseData {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlResponse {
|
||||
fn id(&self) -> Option<u64> {
|
||||
match self {
|
||||
ControlResponse::Connect(response) => Some(response.request_id),
|
||||
ControlResponse::Disconnect(response) => Some(response.request_id),
|
||||
ControlResponse::UnrequestedDisconnect(_) => None,
|
||||
ControlResponse::Pong(response) => Some(response.request_id),
|
||||
ControlResponse::Health(response) => Some(response.request_id),
|
||||
ControlResponse::Info(response) => Some(response.request_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectResponseReply {
|
||||
pub fn is_success(&self) -> bool {
|
||||
match self {
|
||||
ConnectResponseReply::Success(_) => true,
|
||||
ConnectResponseReply::Failure(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,4 @@ utoipa = [ "dep:utoipa" ]
|
||||
|
||||
[build-dependencies]
|
||||
regex = { workspace = true }
|
||||
cargo_metadata = { version = "0.18" }
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
@@ -31,7 +31,7 @@ nym-pemstore = { path = "../pemstore" }
|
||||
nym-network-defaults = { path = "../network-defaults", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
criterion = { workspace = true, features = ["html_reports"] }
|
||||
|
||||
|
||||
[[bench]]
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
[package]
|
||||
name = "nym-coconut"
|
||||
version = "0.5.0"
|
||||
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>", "Ania Piotrowska <ania@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
itertools = { workspace = true }
|
||||
digest = "0.9"
|
||||
rand = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
sha2 = "0.9"
|
||||
zeroize = { workspace = true, optional = true }
|
||||
|
||||
nym-dkg = { path = "../dkg" }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
|
||||
[dependencies.ff]
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.group]
|
||||
workspace = true
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true, features = ["html_reports"] }
|
||||
doc-comment = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
key-zeroize = ["zeroize", "bls12_381/zeroize"]
|
||||
default = []
|
||||
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version="0.2", features=["js"] }
|
||||
@@ -1 +0,0 @@
|
||||
This project was partially funded through the NGI0 PET Fund, a fund established by NL.net with financial support from the European Commission's NGI programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310.
|
||||
@@ -1,360 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, Scalar};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ff::Field;
|
||||
use group::{Curve, Group};
|
||||
use nym_coconut::{
|
||||
aggregate_signature_shares_and_verify, aggregate_verification_keys, blind_sign,
|
||||
prepare_blind_sign, prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen,
|
||||
verify_credential, verify_partial_blind_signature, Attribute, BlindedSignature, Parameters,
|
||||
Signature, SignatureShare, VerificationKey,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use std::ops::Neg;
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(unused)]
|
||||
fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
|
||||
let gt1 = bls12_381::pairing(g11, g21);
|
||||
let gt2 = bls12_381::pairing(g12, g22);
|
||||
assert_eq!(gt1, gt2)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
|
||||
let miller_loop_result = multi_miller_loop(&[
|
||||
(g11, &G2Prepared::from(*g21)),
|
||||
(&g12.neg(), &G2Prepared::from(*g22)),
|
||||
]);
|
||||
assert!(bool::from(
|
||||
miller_loop_result.final_exponentiation().is_identity()
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn bench_pairings(c: &mut Criterion) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let g1 = G1Affine::generator();
|
||||
let g2 = G2Affine::generator();
|
||||
let r = Scalar::random(&mut rng);
|
||||
let s = Scalar::random(&mut rng);
|
||||
|
||||
let g11 = (g1 * r).to_affine();
|
||||
let g21 = (g2 * s).to_affine();
|
||||
let g21_prep = G2Prepared::from(g21);
|
||||
|
||||
let g12 = (g1 * s).to_affine();
|
||||
let g22 = (g2 * r).to_affine();
|
||||
let g22_prep = G2Prepared::from(g22);
|
||||
|
||||
c.bench_function("double pairing", |b| {
|
||||
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
|
||||
});
|
||||
|
||||
c.bench_function("multi miller in affine", |b| {
|
||||
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
|
||||
});
|
||||
|
||||
c.bench_function("multi miller with prepared g2", |b| {
|
||||
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
|
||||
});
|
||||
|
||||
c.bench_function("multi miller with semi-prepared g2", |b| {
|
||||
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn multi_miller_pairing_with_prepared(
|
||||
g11: &G1Affine,
|
||||
g21: &G2Prepared,
|
||||
g12: &G1Affine,
|
||||
g22: &G2Prepared,
|
||||
) {
|
||||
let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]);
|
||||
assert!(bool::from(
|
||||
miller_loop_result.final_exponentiation().is_identity()
|
||||
))
|
||||
}
|
||||
|
||||
// the case of being able to prepare G2 generator
|
||||
#[allow(unused)]
|
||||
fn multi_miller_pairing_with_semi_prepared(
|
||||
g11: &G1Affine,
|
||||
g21: &G2Affine,
|
||||
g12: &G1Affine,
|
||||
g22: &G2Prepared,
|
||||
) {
|
||||
let miller_loop_result =
|
||||
multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]);
|
||||
assert!(bool::from(
|
||||
miller_loop_result.final_exponentiation().is_identity()
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn unblind_and_aggregate(
|
||||
params: &Parameters,
|
||||
blinded_signatures: &[BlindedSignature],
|
||||
partial_verification_keys: &[VerificationKey],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
verification_key: &VerificationKey,
|
||||
) -> Signature {
|
||||
// Unblind all partial signatures
|
||||
let unblinded_signatures: Vec<Signature> = blinded_signatures
|
||||
.iter()
|
||||
.zip(partial_verification_keys.iter())
|
||||
.map(|(signature, partial_verification_key)| {
|
||||
signature
|
||||
.unblind_and_verify(
|
||||
params,
|
||||
partial_verification_key,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
commitment_hash,
|
||||
pedersen_commitments_openings,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unblinded_signature_shares: Vec<SignatureShare> = unblinded_signatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
|
||||
.collect();
|
||||
|
||||
let mut attributes = vec![];
|
||||
attributes.extend_from_slice(private_attributes);
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
aggregate_signature_shares_and_verify(
|
||||
params,
|
||||
verification_key,
|
||||
&attributes,
|
||||
&unblinded_signature_shares,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
struct BenchCase {
|
||||
num_authorities: u64,
|
||||
threshold_p: f32,
|
||||
num_public_attrs: u32,
|
||||
num_private_attrs: u32,
|
||||
}
|
||||
|
||||
impl BenchCase {
|
||||
fn threshold(&self) -> u64 {
|
||||
(self.num_authorities as f32 * self.threshold_p).round() as u64
|
||||
}
|
||||
|
||||
fn num_attrs(&self) -> u32 {
|
||||
self.num_public_attrs + self.num_private_attrs
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_coconut(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("benchmark-coconut");
|
||||
group.measurement_time(Duration::from_secs(1000));
|
||||
let case = BenchCase {
|
||||
num_authorities: 100,
|
||||
threshold_p: 0.7,
|
||||
num_public_attrs: 2,
|
||||
num_private_attrs: 2,
|
||||
};
|
||||
|
||||
let params = setup(case.num_public_attrs + case.num_private_attrs).unwrap();
|
||||
|
||||
random_scalars_refs!(public_attributes, params, case.num_public_attrs as usize);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
// The prepare blind sign is performed by the user
|
||||
let (pedersen_commitments_openings, blind_sign_request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
// CLIENT BENCHMARK: Data needed to ask for a credential
|
||||
// Let's benchmark the operations the client has to perform
|
||||
// to ask for a credential
|
||||
group.bench_function(
|
||||
format!(
|
||||
"[Client] prepare_blind_sign_{}_authorities_{}_attributes_{}_threshold",
|
||||
case.num_authorities,
|
||||
case.num_attrs(),
|
||||
case.threshold_p,
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap())
|
||||
},
|
||||
);
|
||||
|
||||
// keys for the validators
|
||||
let coconut_keypairs = ttp_keygen(¶ms, case.threshold(), case.num_authorities).unwrap();
|
||||
|
||||
// VALIDATOR BENCHMARK: Issue partial credential
|
||||
// we pick only one key pair, as we want to validate how much does it
|
||||
// take for a single validator to issue a partial credential
|
||||
let mut rng = rand::thread_rng();
|
||||
let keypair = coconut_keypairs.choose(&mut rng).unwrap();
|
||||
|
||||
group.bench_function(
|
||||
format!(
|
||||
"[Validator] compute_single_blind_sign_for_credential_with_{}_attributes",
|
||||
case.num_attrs(),
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
blind_sign(
|
||||
¶ms,
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// computing all partial credentials
|
||||
// NOTE: in reality, each validator computes only single signature
|
||||
let mut blinded_signatures = Vec::new();
|
||||
for keypair in coconut_keypairs.iter() {
|
||||
let blinded_signature = blind_sign(
|
||||
¶ms,
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
blinded_signatures.push(blinded_signature)
|
||||
}
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// verify a random partial blind signature
|
||||
let rand_idx = 1;
|
||||
let random_blind_signature = blinded_signatures.get(rand_idx).unwrap();
|
||||
let partial_verification_key = verification_keys.get(rand_idx).unwrap();
|
||||
|
||||
group.bench_function(
|
||||
format!(
|
||||
"verify_partial_blind_signature_{}_private_attributes_{}_public_attributes",
|
||||
case.num_private_attrs, case.num_public_attrs
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
verify_partial_blind_signature(
|
||||
¶ms,
|
||||
blind_sign_request.get_private_attributes_pedersen_commitments(),
|
||||
&public_attributes,
|
||||
random_blind_signature,
|
||||
partial_verification_key,
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Lets bench worse case, ie aggregating all
|
||||
let indices: Vec<u64> = (1..=case.num_authorities).collect();
|
||||
// aggregate verification keys
|
||||
let aggr_verification_key =
|
||||
aggregate_verification_keys(&verification_keys, Some(&indices)).unwrap();
|
||||
|
||||
// CLIENT OPERATION: Unblind partial singatures and aggregate into single signature
|
||||
let aggregated_signature = unblind_and_aggregate(
|
||||
¶ms,
|
||||
&blinded_signatures,
|
||||
&verification_keys,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
&pedersen_commitments_openings,
|
||||
&aggr_verification_key,
|
||||
);
|
||||
|
||||
// CLIENT BENCHMARK: aggregate all partial credentials
|
||||
group.bench_function(
|
||||
format!(
|
||||
"[Client] unblind_and_aggregate_partial_credentials_{}_authorities_{}_attributes_{}_threshold",
|
||||
case.num_authorities,
|
||||
case.num_attrs(),
|
||||
case.threshold_p,
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
unblind_and_aggregate(
|
||||
¶ms,
|
||||
&blinded_signatures,
|
||||
&verification_keys,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
&pedersen_commitments_openings,
|
||||
&aggr_verification_key)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// CLIENT OPERATION: Randomize credentials and generate any cryptographic material to verify them
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_verification_key,
|
||||
&aggregated_signature,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// CLIENT BENCHMARK
|
||||
group.bench_function(
|
||||
format!(
|
||||
"[Client] randomize_and_prove_credential_{}_authorities_{}_attributes_{}_threshold",
|
||||
case.num_authorities,
|
||||
case.num_attrs(),
|
||||
case.threshold_p,
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_verification_key,
|
||||
&aggregated_signature,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// VERIFIER OPERATION
|
||||
// Verify credentials
|
||||
verify_credential(¶ms, &aggr_verification_key, &theta, &public_attributes);
|
||||
|
||||
// VERIFICATION BENCHMARK
|
||||
group.bench_function(
|
||||
format!(
|
||||
"[Verifier] verify_credentials_{}_authorities_{}_attributes_{}_threshold",
|
||||
case.num_authorities,
|
||||
case.num_attrs(),
|
||||
case.threshold_p,
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
verify_credential(¶ms, &aggr_verification_key, &theta, &public_attributes)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
criterion_group!(benches, bench_coconut);
|
||||
criterion_main!(benches);
|
||||
@@ -1,354 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::ops::{Deref, Mul};
|
||||
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use group::Curve;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar};
|
||||
use crate::Attribute;
|
||||
|
||||
/// Type alias for the ephemeral key generated during ElGamal encryption
|
||||
pub type EphemeralKey = Scalar;
|
||||
|
||||
/// Two G1 points representing ElGamal ciphertext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
impl TryFrom<&[u8]> for Ciphertext {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Ciphertext must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
|
||||
|
||||
let c1 = try_deserialize_g1_projective(
|
||||
c1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()),
|
||||
)?;
|
||||
let c2 = try_deserialize_g1_projective(
|
||||
c2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Ciphertext(c1, c2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ciphertext {
|
||||
pub fn c1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn c2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
Ciphertext::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct PrivateKey(pub(crate) Scalar);
|
||||
|
||||
impl PrivateKey {
|
||||
/// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve
|
||||
/// that represents original h^m.
|
||||
pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective {
|
||||
let (c1, c2) = &(ciphertext.0, ciphertext.1);
|
||||
|
||||
// (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d
|
||||
c2 - c1 * self.0
|
||||
}
|
||||
|
||||
pub fn public_key(&self, params: &Parameters) -> PublicKey {
|
||||
PublicKey(params.gen1() * self.0)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 32]) -> Result<PrivateKey> {
|
||||
try_deserialize_scalar(
|
||||
bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize ElGamal private key - it was not in the canonical form"
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
.map(PrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PrivateKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "elgamal::PrivateKey".to_string(),
|
||||
received,
|
||||
expected: 32,
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey::from_bytes(arr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PrivateKey {}
|
||||
|
||||
// TODO: perhaps be more explicit and apart from gamma also store generator and group order?
|
||||
/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct PublicKey(G1Projective);
|
||||
|
||||
impl PublicKey {
|
||||
/// Encrypt encrypts the given message in the form of h^m,
|
||||
/// where h is a point on the G1 curve using the given public key.
|
||||
/// The random k is returned alongside the encryption
|
||||
/// as it is required by the Coconut Scheme to create proofs of knowledge.
|
||||
pub fn encrypt(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
h: &G1Projective,
|
||||
msg: &Scalar,
|
||||
) -> (Ciphertext, EphemeralKey) {
|
||||
let k = params.random_scalar();
|
||||
// c1 = g1^k
|
||||
let c1 = params.gen1() * k;
|
||||
// c2 = gamma^k * h^m
|
||||
let c2 = self.0 * k + h * msg;
|
||||
|
||||
(Ciphertext(c1, c2), k)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
self.0.to_affine().to_compressed()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
|
||||
try_deserialize_g1_projective(
|
||||
bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed ElGamal public key".to_string(),
|
||||
),
|
||||
)
|
||||
.map(PublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PublicKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().into()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "elgamal::PublicKey".to_string(),
|
||||
received,
|
||||
expected: 48,
|
||||
});
|
||||
};
|
||||
|
||||
PublicKey::from_bytes(arr)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for PublicKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(slice: &[u8]) -> Result<PublicKey> {
|
||||
PublicKey::try_from_byte_slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PublicKey {}
|
||||
|
||||
impl Deref for PublicKey {
|
||||
type Target = G1Projective;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Mul<&'a Scalar> for &PublicKey {
|
||||
type Output = G1Projective;
|
||||
|
||||
fn mul(self, rhs: &'a Scalar) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
/// A convenient wrapper for both keys of the ElGamal keypair
|
||||
pub struct ElGamalKeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl ElGamalKeyPair {
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters]
|
||||
pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
|
||||
let private_key = params.random_scalar();
|
||||
let gamma = params.gen1() * private_key;
|
||||
|
||||
ElGamalKeyPair {
|
||||
private_key: PrivateKey(private_key),
|
||||
public_key: PublicKey(gamma),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_attribute_encryption(
|
||||
params: &Parameters,
|
||||
private_attributes: &[&Attribute],
|
||||
pub_key: &PublicKey,
|
||||
commitment_hash: G1Projective,
|
||||
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
|
||||
private_attributes
|
||||
.iter()
|
||||
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
|
||||
.unzip()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keygen() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let expected = params.gen1() * keypair.private_key.0;
|
||||
let gamma = keypair.public_key.0;
|
||||
assert_eq!(
|
||||
expected, gamma,
|
||||
"Public key, gamma, should be equal to g1^d, where d is the private key"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
|
||||
let expected_c1 = params.gen1() * ephemeral_key;
|
||||
assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k");
|
||||
|
||||
let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m;
|
||||
assert_eq!(
|
||||
expected_c2, ciphertext.1,
|
||||
"c2 should be equal to gamma^k * h^m"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, _) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
let dec = keypair.private_key.decrypt(&ciphertext);
|
||||
|
||||
let expected = h * m;
|
||||
assert_eq!(
|
||||
expected, dec,
|
||||
"after ElGamal decryption, original h^m should be obtained"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let private_key = PrivateKey(params.random_scalar());
|
||||
let bytes = private_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal scalar's bytes
|
||||
assert_eq!(private_key.0.to_bytes(), bytes);
|
||||
assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let public_key = PublicKey(params.gen1() * r);
|
||||
let bytes = public_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes
|
||||
assert_eq!(public_key.0.to_affine().to_compressed(), bytes);
|
||||
assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ciphertext_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = ciphertext.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
ciphertext.0.to_affine().to_compressed(),
|
||||
ciphertext.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// A `Result` alias where the `Err` case is `coconut_rs::Error`.
|
||||
pub type Result<T> = std::result::Result<T, CoconutError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CoconutError {
|
||||
#[error("Setup error: {0}")]
|
||||
Setup(String),
|
||||
|
||||
#[error("encountered error during keygen")]
|
||||
Keygen,
|
||||
|
||||
#[error("Issuance related error: {0}")]
|
||||
Issuance(String),
|
||||
|
||||
#[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)]
|
||||
IssuanceMaxAttributes { max: usize, requested: usize },
|
||||
|
||||
#[error("Interpolation error: {0}")]
|
||||
Interpolation(String),
|
||||
|
||||
#[error("Aggregation error: {0}")]
|
||||
Aggregation(String),
|
||||
|
||||
#[error("Unblind error: {0}")]
|
||||
Unblind(String),
|
||||
|
||||
#[error("Verification error: {0}")]
|
||||
Verification(String),
|
||||
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
#[error(
|
||||
"Deserailization error, expected at least {} bytes, got {}",
|
||||
min,
|
||||
actual
|
||||
)]
|
||||
DeserializationMinLength { min: usize, actual: usize },
|
||||
|
||||
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")]
|
||||
DeserializationInvalidLength {
|
||||
actual: usize,
|
||||
target: usize,
|
||||
modulus_target: usize,
|
||||
modulus: usize,
|
||||
object: String,
|
||||
},
|
||||
|
||||
#[error("received an array of unexpected size for deserialization of {typ}. got {received} but expected {expected}")]
|
||||
UnexpectedArrayLength {
|
||||
typ: String,
|
||||
received: usize,
|
||||
expected: usize,
|
||||
},
|
||||
|
||||
#[error("failed to decode the base58 representation: {0}")]
|
||||
Base58DecodingFailure(#[from] bs58::decode::Error),
|
||||
|
||||
#[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")]
|
||||
ScalarDeserializationFailure,
|
||||
|
||||
#[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")]
|
||||
G1ProjectiveDeserializationFailure,
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
use crate::{BlindSignRequest, BlindedSignature, Bytable, VerifyCredentialRequest};
|
||||
|
||||
macro_rules! impl_clone {
|
||||
($struct:ident) => {
|
||||
impl Clone for $struct {
|
||||
fn clone(&self) -> Self {
|
||||
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_clone!(BlindSignRequest);
|
||||
impl_clone!(BlindedSignature);
|
||||
impl_clone!(VerifyCredentialRequest);
|
||||
@@ -1,2 +0,0 @@
|
||||
mod clone;
|
||||
mod serde;
|
||||
@@ -1,57 +0,0 @@
|
||||
use crate::elgamal::PrivateKey;
|
||||
use crate::scheme::SecretKey;
|
||||
use crate::{
|
||||
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, VerificationKey,
|
||||
VerifyCredentialRequest,
|
||||
};
|
||||
use serde::de::Unexpected;
|
||||
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! impl_serde {
|
||||
($struct:ident, $visitor:ident) => {
|
||||
pub struct $visitor {}
|
||||
|
||||
impl Serialize for $struct {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_bs58())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for $visitor {
|
||||
type Value = $struct;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "A base58 encoded struct")
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||
match $struct::try_from_bs58(s) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $struct {
|
||||
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str($visitor {})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_serde!(SecretKey, V1);
|
||||
impl_serde!(VerificationKey, V2);
|
||||
impl_serde!(PublicKey, V3);
|
||||
impl_serde!(PrivateKey, V4);
|
||||
impl_serde!(BlindSignRequest, V5);
|
||||
impl_serde!(BlindedSignature, V6);
|
||||
impl_serde!(Signature, V7);
|
||||
impl_serde!(VerifyCredentialRequest, V8);
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
pub use bls12_381::Scalar;
|
||||
pub use elgamal::elgamal_keygen;
|
||||
pub use elgamal::ElGamalKeyPair;
|
||||
pub use elgamal::PublicKey;
|
||||
pub use error::CoconutError;
|
||||
pub use scheme::aggregation::aggregate_key_shares;
|
||||
pub use scheme::aggregation::aggregate_signature_shares;
|
||||
pub use scheme::aggregation::aggregate_signature_shares_and_verify;
|
||||
pub use scheme::aggregation::aggregate_verification_keys;
|
||||
pub use scheme::issuance::blind_sign;
|
||||
pub use scheme::issuance::prepare_blind_sign;
|
||||
pub use scheme::issuance::sign;
|
||||
pub use scheme::issuance::verify_partial_blind_signature;
|
||||
pub use scheme::issuance::BlindSignRequest;
|
||||
pub use scheme::keygen::keygen;
|
||||
pub use scheme::keygen::ttp_keygen;
|
||||
pub use scheme::keygen::KeyPair;
|
||||
pub use scheme::keygen::SecretKey;
|
||||
pub use scheme::keygen::VerificationKey;
|
||||
pub use scheme::keygen::VerificationKeyShare;
|
||||
pub use scheme::setup::setup;
|
||||
pub use scheme::setup::Parameters;
|
||||
pub use scheme::verification::check_vk_pairing;
|
||||
pub use scheme::verification::prove_bandwidth_credential;
|
||||
pub use scheme::verification::verify;
|
||||
pub use scheme::verification::verify_credential;
|
||||
pub use scheme::verification::BlindedSerialNumber;
|
||||
pub use scheme::verification::VerifyCredentialRequest;
|
||||
pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
pub use scheme::SignatureShare;
|
||||
pub use scheme::SignerIndex;
|
||||
pub use traits::Base58;
|
||||
pub use traits::Bytable;
|
||||
pub use utils::hash_to_scalar;
|
||||
|
||||
pub mod elgamal;
|
||||
mod error;
|
||||
mod impls;
|
||||
mod proofs;
|
||||
mod scheme;
|
||||
pub mod tests;
|
||||
mod traits;
|
||||
pub mod utils;
|
||||
|
||||
pub type Attribute = bls12_381::Scalar;
|
||||
pub type PrivateAttribute = Attribute;
|
||||
pub type PublicAttribute = Attribute;
|
||||
|
||||
pub use bls12_381::G1Projective;
|
||||
@@ -1,619 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// TODO: look at https://crates.io/crates/merlin to perhaps use it instead?
|
||||
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use digest::generic_array::typenum::Unsigned;
|
||||
use digest::Digest;
|
||||
use group::GroupEncoding;
|
||||
use itertools::izip;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::issuance::compute_hash;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::utils::{try_deserialize_scalar, try_deserialize_scalar_vec};
|
||||
use crate::Attribute;
|
||||
|
||||
// as per the reference python implementation
|
||||
type ChallengeDigest = Sha256;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct ProofCmCs {
|
||||
challenge: Scalar,
|
||||
response_opening: Scalar,
|
||||
response_openings: Vec<Scalar>,
|
||||
response_attributes: Vec<Scalar>,
|
||||
}
|
||||
|
||||
// note: this is slightly different from the reference python implementation
|
||||
// as we omit the unnecessary string conversion. Instead we concatenate byte
|
||||
// representations together and hash that.
|
||||
// note2: G1 and G2 elements are using their compressed representations
|
||||
// and as per the bls12-381 library all elements are using big-endian form
|
||||
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
|
||||
fn compute_challenge<D, I, B>(iter: I) -> Scalar
|
||||
where
|
||||
D: Digest,
|
||||
I: Iterator<Item = B>,
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
let mut h = D::new();
|
||||
for point_representation in iter {
|
||||
h.update(point_representation);
|
||||
}
|
||||
let digest = h.finalize();
|
||||
|
||||
// TODO: I don't like the 0 padding here (though it's what we've been using before,
|
||||
// but we never had a security audit anyway...)
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
Scalar::from_bytes_wide(&bytes)
|
||||
}
|
||||
|
||||
fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
|
||||
witness - challenge * secret
|
||||
}
|
||||
|
||||
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
|
||||
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
|
||||
where
|
||||
S: Borrow<Scalar>,
|
||||
{
|
||||
debug_assert_eq!(witnesses.len(), secrets.len());
|
||||
|
||||
witnesses
|
||||
.iter()
|
||||
.zip(secrets.iter())
|
||||
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProofCmCs {
|
||||
/// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment
|
||||
/// using the Fiat-Shamir heuristic.
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
commitment: &G1Projective,
|
||||
commitment_opening: &Scalar,
|
||||
commitments: &[G1Projective],
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Self {
|
||||
// note: this is only called from `prepare_blind_sign` that already checks
|
||||
// whether private attributes are non-empty and whether we don't have too many
|
||||
// attributes in total to sign.
|
||||
// we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len()
|
||||
|
||||
// witness creation
|
||||
let witness_commitment_opening = params.random_scalar();
|
||||
let witness_pedersen_commitments_openings =
|
||||
params.n_random_scalars(pedersen_commitments_openings.len());
|
||||
let witness_attributes = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
// recompute h
|
||||
let h = compute_hash(*commitment, public_attributes);
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let g1 = params.gen1();
|
||||
|
||||
// compute commitments
|
||||
|
||||
// zkp commitment for the attributes commitment cm
|
||||
// Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i])
|
||||
let commitment_attributes = g1 * witness_commitment_opening
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(wm_i, hs_i)| hs_i * wm_i)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
// zkp commitments for the individual attributes
|
||||
let commitments_attributes = witness_pedersen_commitments_openings
|
||||
.iter()
|
||||
.zip(witness_attributes.iter())
|
||||
.map(|(o_j, m_j)| g1 * o_j + h * m_j)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commitments_bytes = commitments
|
||||
.iter()
|
||||
.map(|cm| cm.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commitments_attributes_bytes = commitments_attributes
|
||||
.iter()
|
||||
.map(|cm| cm.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// compute challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(commitments_bytes.iter().map(|cm| cm.as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())),
|
||||
);
|
||||
|
||||
// Responses
|
||||
let response_opening =
|
||||
produce_response(&witness_commitment_opening, &challenge, commitment_opening);
|
||||
let response_openings = produce_responses(
|
||||
&witness_pedersen_commitments_openings,
|
||||
&challenge,
|
||||
&pedersen_commitments_openings.iter().collect::<Vec<_>>(),
|
||||
);
|
||||
let response_attributes =
|
||||
produce_responses(&witness_attributes, &challenge, private_attributes);
|
||||
|
||||
ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_openings,
|
||||
response_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
commitment: &G1Projective,
|
||||
commitments: &[G1Projective],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> bool {
|
||||
if self.response_attributes.len() != commitments.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// recompute h
|
||||
let h = compute_hash(*commitment, public_attributes);
|
||||
let g1 = params.gen1();
|
||||
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// recompute witnesses commitments
|
||||
// Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n])
|
||||
let commitment_attributes = (commitment
|
||||
- public_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter().skip(self.response_attributes.len()))
|
||||
.map(|(&pub_attr, hs)| hs * pub_attr)
|
||||
.sum::<G1Projective>())
|
||||
* self.challenge
|
||||
+ g1 * self.response_opening
|
||||
+ self
|
||||
.response_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(res_attr, hs)| hs * res_attr)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let commitments_attributes = izip!(
|
||||
commitments.iter(),
|
||||
self.response_openings.iter(),
|
||||
self.response_attributes.iter()
|
||||
)
|
||||
.map(|(cm_j, r_o_j, r_m_j)| cm_j * self.challenge + g1 * r_o_j + h * r_m_j)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commitments_bytes = commitments
|
||||
.iter()
|
||||
.map(|cm| cm.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commitments_attributes_bytes = commitments_attributes
|
||||
.iter()
|
||||
.map(|cm| cm.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// re-compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(commitments_bytes.iter().map(|cm| cm.as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || response opening || openings len || response openings || attributes len ||
|
||||
// response attributes
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let openings_len = self.response_openings.len() as u64;
|
||||
let attributes_len = self.response_attributes.len() as u64;
|
||||
|
||||
let mut bytes = Vec::with_capacity(16 + (2 + openings_len + attributes_len) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_opening.to_bytes());
|
||||
|
||||
bytes.extend_from_slice(&openings_len.to_le_bytes());
|
||||
for ro in &self.response_openings {
|
||||
bytes.extend_from_slice(&ro.to_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&attributes_len.to_le_bytes());
|
||||
for rm in &self.response_attributes {
|
||||
bytes.extend_from_slice(&rm.to_bytes());
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 4 + 16 || (bytes.len() - 16) % 32 != 0 {
|
||||
return Err(CoconutError::Deserialization(
|
||||
"tried to deserialize proof of commitments with bytes of invalid length"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
let response_opening = try_deserialize_scalar(
|
||||
&response_opening_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize the response to the random".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ro_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
|
||||
idx += 8;
|
||||
if bytes[idx..].len() < ro_len as usize * 32 + 8 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let ro_end = idx + ro_len as usize * 32;
|
||||
let response_openings = try_deserialize_scalar_vec(
|
||||
ro_len,
|
||||
&bytes[idx..ro_end],
|
||||
CoconutError::Deserialization("Failed to deserialize openings response".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let rm_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap());
|
||||
let response_attributes = try_deserialize_scalar_vec(
|
||||
rm_len,
|
||||
&bytes[ro_end + 8..],
|
||||
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_openings,
|
||||
response_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ProofKappaZeta {
|
||||
// c
|
||||
challenge: Scalar,
|
||||
|
||||
// responses
|
||||
response_serial_number: Scalar,
|
||||
response_binding_number: Scalar,
|
||||
response_blinder: Scalar,
|
||||
}
|
||||
|
||||
impl ProofKappaZeta {
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
blinding_factor: &Scalar,
|
||||
blinded_message: &G2Projective,
|
||||
blinded_serial_number: &G2Projective,
|
||||
) -> Self {
|
||||
// create the witnesses
|
||||
let witness_blinder = params.random_scalar();
|
||||
let witness_serial_number = params.random_scalar();
|
||||
let witness_binding_number = params.random_scalar();
|
||||
let witness_attributes = [witness_serial_number, witness_binding_number];
|
||||
|
||||
let beta_bytes = verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// witnesses commitments
|
||||
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
|
||||
let commitment_kappa = params.gen2() * witness_blinder
|
||||
+ verification_key.alpha
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(wm_i, beta_i)| beta_i * wm_i)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = params.gen2() * witness_serial_number;
|
||||
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(blinded_message.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(blinded_serial_number.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
// responses
|
||||
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
|
||||
let response_serial_number =
|
||||
produce_response(&witness_serial_number, &challenge, serial_number);
|
||||
let response_binding_number =
|
||||
produce_response(&witness_binding_number, &challenge, binding_number);
|
||||
|
||||
ProofKappaZeta {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn private_attributes_len(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
kappa: &G2Projective,
|
||||
zeta: &G2Projective,
|
||||
) -> bool {
|
||||
let beta_bytes = verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let response_attributes = [self.response_serial_number, self.response_binding_number];
|
||||
// re-compute witnesses commitments
|
||||
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
|
||||
let commitment_kappa = kappa * self.challenge
|
||||
+ params.gen2() * self.response_blinder
|
||||
+ verification_key.alpha * (Scalar::one() - self.challenge)
|
||||
+ response_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number;
|
||||
|
||||
// compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zeta.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || response serial number || response binding number || repose blinder
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let attributes_len = 2; // because we have serial number and the binding number
|
||||
let mut bytes = Vec::with_capacity((1 + attributes_len + 1) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_serial_number.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_binding_number.to_bytes());
|
||||
|
||||
bytes.extend_from_slice(&self.response_blinder.to_bytes());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() != 128 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len(),
|
||||
modulus: 32,
|
||||
object: "kappa and zeta".to_string(),
|
||||
target: 32 * 4,
|
||||
});
|
||||
}
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let challenge_bytes = bytes[..32].try_into().unwrap();
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
|
||||
let response_serial_number = try_deserialize_scalar(
|
||||
serial_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
|
||||
let response_binding_number = try_deserialize_scalar(
|
||||
binding_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
|
||||
)?;
|
||||
|
||||
// safety: bound checked + constant offset
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let blinder_bytes = bytes[96..].try_into().unwrap();
|
||||
let response_blinder = try_deserialize_scalar(
|
||||
&blinder_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofKappaZeta {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// proof builder:
|
||||
// - commitment
|
||||
// - challenge
|
||||
// - responses
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
use crate::scheme::verification::{compute_kappa, compute_zeta};
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
use group::Group;
|
||||
use rand::thread_rng;
|
||||
|
||||
#[test]
|
||||
fn proof_cm_cs_bytes_roundtrip() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let params = setup(1).unwrap();
|
||||
let cm = G1Projective::random(&mut rng);
|
||||
let r = params.random_scalar();
|
||||
let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)];
|
||||
let rs = params.n_random_scalars(1);
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
|
||||
// 0 public 1 private
|
||||
let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
|
||||
let params = setup(2).unwrap();
|
||||
let cm = G1Projective::random(&mut rng);
|
||||
let r = params.random_scalar();
|
||||
let cms: [G1Projective; 2] = [
|
||||
G1Projective::random(&mut rng),
|
||||
G1Projective::random(&mut rng),
|
||||
];
|
||||
let rs = params.n_random_scalars(2);
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
// 0 public 2 privates
|
||||
let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_kappa_zeta_bytes_roundtrip() {
|
||||
let params = setup(4).unwrap();
|
||||
|
||||
let keypair = keygen(¶ms);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let serial_number = ¶ms.random_scalar();
|
||||
let binding_number = ¶ms.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let r = params.random_scalar();
|
||||
let kappa = compute_kappa(¶ms, keypair.verification_key(), &private_attributes, r);
|
||||
let zeta = compute_zeta(¶ms, serial_number);
|
||||
|
||||
// 0 public 2 private
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
¶ms,
|
||||
keypair.verification_key(),
|
||||
serial_number,
|
||||
binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let proof_bytes = pi_v.to_bytes();
|
||||
|
||||
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
|
||||
assert_eq!(proof_from_bytes, pi_v);
|
||||
|
||||
// 2 public 2 private
|
||||
let params = setup(4).unwrap();
|
||||
let keypair = keygen(¶ms);
|
||||
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
¶ms,
|
||||
keypair.verification_key(),
|
||||
serial_number,
|
||||
binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let proof_bytes = pi_v.to_bytes();
|
||||
|
||||
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
|
||||
assert_eq!(proof_from_bytes, pi_v);
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
|
||||
use bls12_381::{G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
|
||||
use crate::utils::perform_lagrangian_interpolation_at_origin;
|
||||
use crate::{Attribute, Parameters, VerificationKeyShare};
|
||||
|
||||
pub(crate) trait Aggregatable: Sized {
|
||||
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
|
||||
|
||||
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
|
||||
// if aggregation is a threshold one, all indices should be unique
|
||||
indices.iter().unique_by(|&index| index).count() == indices.len()
|
||||
}
|
||||
}
|
||||
|
||||
// includes `VerificationKey`
|
||||
impl<T> Aggregatable for T
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> T: Sum<&'a T>,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
|
||||
if aggregatable.is_empty() {
|
||||
return Err(CoconutError::Aggregation("Empty set of values".to_string()));
|
||||
}
|
||||
|
||||
if let Some(indices) = indices {
|
||||
if !Self::check_unique_indices(indices) {
|
||||
return Err(CoconutError::Aggregation("Non-unique indices".to_string()));
|
||||
}
|
||||
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
|
||||
} else {
|
||||
// non-threshold
|
||||
Ok(aggregatable.iter().sum())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Aggregatable for PartialSignature {
|
||||
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
|
||||
let h = sigs
|
||||
.first()
|
||||
.ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))?
|
||||
.sig1();
|
||||
|
||||
// TODO: is it possible to avoid this allocation?
|
||||
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
|
||||
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
|
||||
|
||||
Ok(Signature(*h, aggr_sigma))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures all provided verification keys were generated to verify the same number of attributes.
|
||||
fn check_same_key_size(keys: &[VerificationKey]) -> bool {
|
||||
keys.iter().map(|vk| vk.beta_g1.len()).all_equal()
|
||||
&& keys.iter().map(|vk| vk.beta_g2.len()).all_equal()
|
||||
}
|
||||
|
||||
pub fn aggregate_verification_keys(
|
||||
keys: &[VerificationKey],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<VerificationKey> {
|
||||
if !check_same_key_size(keys) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification keys are of different sizes".to_string(),
|
||||
));
|
||||
}
|
||||
Aggregatable::aggregate(keys, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_key_shares(shares: &[VerificationKeyShare]) -> Result<VerificationKey> {
|
||||
let (keys, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (share.key.clone(), share.index))
|
||||
.unzip();
|
||||
|
||||
aggregate_verification_keys(&keys, Some(&indices))
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures(
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
Aggregatable::aggregate(signatures, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures_and_verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[&Attribute],
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
// aggregate the signature
|
||||
let signature = aggregate_signatures(signatures, indices)?;
|
||||
|
||||
// Verify the signature
|
||||
let alpha = verification_key.alpha;
|
||||
|
||||
let tmp = attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(&attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
if bool::from(signature.0.is_identity()) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification of the aggregated signature failed - h is an identity point".to_string(),
|
||||
));
|
||||
}
|
||||
if !check_bilinear_pairing(
|
||||
&signature.0.to_affine(),
|
||||
&G2Prepared::from((alpha + tmp).to_affine()),
|
||||
&signature.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification of the aggregated signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares(shares: &[SignatureShare]) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures(&signatures, Some(&indices))
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares_and_verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[&Attribute],
|
||||
shares: &[SignatureShare],
|
||||
) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures_and_verify(
|
||||
params,
|
||||
verification_key,
|
||||
attributes,
|
||||
&signatures,
|
||||
Some(&indices),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::issuance::sign;
|
||||
use crate::scheme::keygen::ttp_keygen;
|
||||
use crate::scheme::verification::verify;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
use bls12_381::G1Projective;
|
||||
use group::Group;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_works_for_any_subset_of_keys() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
|
||||
assert_eq!(aggr_vk1, aggr_vk2);
|
||||
|
||||
// TODO: should those two actually work or not?
|
||||
// aggregating threshold+1
|
||||
let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_vk1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_all, aggr_vk1);
|
||||
|
||||
// not taking enough points (threshold was 3)
|
||||
let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_vk1);
|
||||
|
||||
// taking wrong index
|
||||
let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap();
|
||||
assert_ne!(aggr_vk1, aggr_bad);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_empty_set_of_keys() {
|
||||
let keys: Vec<VerificationKey> = vec![];
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let keys = vec![VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[])).is_err());
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_keys_of_different_size() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_works_for_any_subset_of_signatures() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
|
||||
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let sigs = sks
|
||||
.iter()
|
||||
.map(|sk| sign(sk, &attributes).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// aggregating (any) threshold works
|
||||
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_sig1 = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[..3],
|
||||
Some(&[1, 2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
let aggr_sig2 = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_sig2);
|
||||
|
||||
// verify credential for good measure
|
||||
assert!(verify(¶ms, &aggr_vk_1, &attributes, &aggr_sig1));
|
||||
assert!(verify(¶ms, &aggr_vk_2, &attributes, &aggr_sig2));
|
||||
|
||||
// aggregating threshold+1 works
|
||||
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
let aggr_more = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_more,
|
||||
&attributes,
|
||||
&sigs[1..],
|
||||
Some(&[2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
let aggr_all = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&sigs,
|
||||
Some(&[1, 2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_all, aggr_sig1);
|
||||
|
||||
// not taking enough points (threshold was 3) should fail
|
||||
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_not_enough = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_not_enough,
|
||||
&attributes,
|
||||
&sigs[..2],
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_sig1);
|
||||
|
||||
// taking wrong index should fail
|
||||
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_bad,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[42, 123, 100]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
fn random_signature() -> Signature {
|
||||
let mut rng = rand::thread_rng();
|
||||
Signature(
|
||||
G1Projective::random(&mut rng),
|
||||
G1Projective::random(&mut rng),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
|
||||
let signatures: Vec<Signature> = vec![];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
None
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let signatures = vec![random_signature()];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[])
|
||||
)
|
||||
.is_err());
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let signatures = vec![random_signature(), random_signature()];
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
let keypairs = ttp_keygen(¶ms, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| {
|
||||
(
|
||||
keypair.secret_key().clone(),
|
||||
keypair.verification_key().clone(),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 1]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// TODO: test for aggregating non-threshold keys
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g2_projective;
|
||||
use bls12_381::{G2Affine, G2Projective};
|
||||
use group::Curve;
|
||||
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub struct BlindedSerialNumber(G2Projective);
|
||||
|
||||
// use custom Debug implementation to show base58 encoding (rather than raw curve elements)
|
||||
impl Debug for BlindedSerialNumber {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("BlindedSerialNumber")
|
||||
.field(&self.to_bs58())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<G2Projective> for BlindedSerialNumber {
|
||||
fn from(value: G2Projective) -> Self {
|
||||
BlindedSerialNumber(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<G2Affine> for BlindedSerialNumber {
|
||||
fn from(value: G2Affine) -> Self {
|
||||
BlindedSerialNumber(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BlindedSerialNumber {
|
||||
type Target = G2Projective;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindedSerialNumber {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize blinded serial number with incorrect number of bytes, expected 96, got {}", bytes.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we've just made a check for 96 bytes
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let inner = try_deserialize_g2_projective(
|
||||
&bytes.try_into().unwrap(),
|
||||
CoconutError::Deserialization(
|
||||
"failed to deserialize the blinded serial number (zeta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(BlindedSerialNumber(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for BlindedSerialNumber {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.0.to_affine().to_compressed().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Self::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindedSerialNumber {}
|
||||
@@ -1,660 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::ops::Neg;
|
||||
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Prepared, Scalar};
|
||||
use group::{Curve, Group, GroupEncoding};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofCmCs;
|
||||
use crate::scheme::keygen::VerificationKey;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::BlindedSignature;
|
||||
use crate::scheme::SecretKey;
|
||||
use crate::Attribute;
|
||||
use crate::Signature;
|
||||
|
||||
// TODO: possibly completely remove those two functions.
|
||||
// They only exist to have a simpler and smaller code snippets to test
|
||||
// basic functionalities.
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{hash_g1, try_deserialize_g1_projective};
|
||||
|
||||
// TODO NAMING: double check this one
|
||||
// Lambda
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct BlindSignRequest {
|
||||
// cm
|
||||
commitment: G1Projective,
|
||||
// h
|
||||
commitment_hash: G1Projective,
|
||||
// c
|
||||
private_attributes_commitments: Vec<G1Projective>,
|
||||
// pi_s
|
||||
pi_s: ProofCmCs,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
if bytes.len() < 48 + 48 + 8 + 48 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: 48 + 48 + 8 + 48,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut j = 0;
|
||||
let commitment_bytes_len = 48;
|
||||
let commitment_hash_bytes_len = 48;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
|
||||
let commitment = try_deserialize_g1_projective(
|
||||
&cm_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_bytes_len;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
|
||||
let commitment_hash = try_deserialize_g1_projective(
|
||||
&cm_hash_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment hash".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_hash_bytes_len;
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
|
||||
j += 8;
|
||||
if bytes[j..].len() < c_len as usize * 48 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: c_len as usize * 48,
|
||||
actual: bytes[56..].len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut private_attributes_commitments = Vec::with_capacity(c_len as usize);
|
||||
for i in 0..c_len as usize {
|
||||
let start = j + i * 48;
|
||||
let end = start + 48;
|
||||
|
||||
if bytes.len() < end {
|
||||
return Err(CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let private_attributes_commitment_bytes = bytes[start..end].try_into().unwrap();
|
||||
let private_attributes_commitment = try_deserialize_g1_projective(
|
||||
&private_attributes_commitment_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
private_attributes_commitments.push(private_attributes_commitment)
|
||||
}
|
||||
|
||||
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 48..])?;
|
||||
|
||||
Ok(BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_commitments,
|
||||
pi_s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for BlindSignRequest {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
let cm_bytes = self.commitment.to_affine().to_compressed();
|
||||
let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed();
|
||||
let c_len = self.private_attributes_commitments.len() as u64;
|
||||
let proof_bytes = self.pi_s.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 48 + proof_bytes.len());
|
||||
|
||||
bytes.extend_from_slice(&cm_bytes);
|
||||
bytes.extend_from_slice(&cm_hash_bytes);
|
||||
bytes.extend_from_slice(&c_len.to_le_bytes());
|
||||
for c in &self.private_attributes_commitments {
|
||||
bytes.extend_from_slice(&c.to_affine().to_compressed());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
BlindSignRequest::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindSignRequest {}
|
||||
|
||||
impl BlindSignRequest {
|
||||
fn verify_proof(&self, params: &Parameters, public_attributes: &[&Attribute]) -> bool {
|
||||
self.pi_s.verify(
|
||||
params,
|
||||
&self.commitment,
|
||||
&self.private_attributes_commitments,
|
||||
public_attributes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_commitment_hash(&self, public_attributes: &[&Attribute]) -> bool {
|
||||
self.commitment_hash == compute_hash(self.commitment, public_attributes)
|
||||
}
|
||||
|
||||
pub fn get_commitment_hash(&self) -> G1Projective {
|
||||
self.commitment_hash
|
||||
}
|
||||
|
||||
pub fn get_private_attributes_pedersen_commitments(&self) -> &[G1Projective] {
|
||||
&self.private_attributes_commitments
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
BlindSignRequest::try_from(bytes)
|
||||
}
|
||||
|
||||
pub fn num_private_attributes(&self) -> usize {
|
||||
self.private_attributes_commitments.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_attributes_commitment(
|
||||
params: &Parameters,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
hs: &[G1Affine],
|
||||
) -> (Scalar, G1Projective) {
|
||||
let commitment_opening = params.random_scalar();
|
||||
|
||||
// Produces h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
// where m0, m1, ...., mn are attributes
|
||||
let attr_cm = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(hs)
|
||||
.map(|(&m, h)| h * m)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
// Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
let commitment = params.gen1() * commitment_opening + attr_cm;
|
||||
(commitment_opening, commitment)
|
||||
}
|
||||
|
||||
pub fn compute_pedersen_commitments_for_private_attributes(
|
||||
params: &Parameters,
|
||||
private_attributes: &[&Attribute],
|
||||
h: &G1Projective,
|
||||
) -> (Vec<Scalar>, Vec<G1Projective>) {
|
||||
// Generate openings for Pedersen commitment for each private attribute
|
||||
let commitments_openings = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
// Compute Pedersen commitment for each private attribute
|
||||
let pedersen_commitments = commitments_openings
|
||||
.iter()
|
||||
.zip(private_attributes.iter())
|
||||
.map(|(o_j, &m_j)| params.gen1() * o_j + h * m_j)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(commitments_openings, pedersen_commitments)
|
||||
}
|
||||
|
||||
pub fn compute_hash(commitment: G1Projective, public_attributes: &[&Attribute]) -> G1Projective {
|
||||
let mut buff = Vec::new();
|
||||
buff.extend_from_slice(commitment.to_bytes().as_ref());
|
||||
for attr in public_attributes {
|
||||
buff.extend_from_slice(attr.to_bytes().as_ref());
|
||||
}
|
||||
hash_g1(buff)
|
||||
}
|
||||
|
||||
/// Builds cryptographic material required for blind sign.
|
||||
pub fn prepare_blind_sign(
|
||||
params: &Parameters,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<(Vec<Scalar>, BlindSignRequest)> {
|
||||
if private_attributes.is_empty() {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Tried to prepare blind sign request for an empty set of private attributes"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = params.gen_hs();
|
||||
if private_attributes.len() + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: private_attributes.len() + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut commitment_hash;
|
||||
let mut commitment;
|
||||
let mut commitment_opening;
|
||||
|
||||
loop {
|
||||
// Compute the attributes commitment
|
||||
let (c_opening, c) =
|
||||
compute_attributes_commitment(params, private_attributes, public_attributes, hs);
|
||||
commitment_opening = c_opening;
|
||||
commitment = c;
|
||||
|
||||
// Compute the commitment hash
|
||||
commitment_hash = compute_hash(commitment, public_attributes);
|
||||
|
||||
// Check if the commitment hash is not the identity point
|
||||
if !bool::from(commitment_hash.is_identity()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (pedersen_commitments_openings, pedersen_commitments) =
|
||||
compute_pedersen_commitments_for_private_attributes(
|
||||
params,
|
||||
private_attributes,
|
||||
&commitment_hash,
|
||||
);
|
||||
|
||||
let pi_s = ProofCmCs::construct(
|
||||
params,
|
||||
&commitment,
|
||||
&commitment_opening,
|
||||
&pedersen_commitments,
|
||||
&pedersen_commitments_openings,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
);
|
||||
|
||||
Ok((
|
||||
pedersen_commitments_openings,
|
||||
BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_commitments: pedersen_commitments,
|
||||
pi_s,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn blind_sign(
|
||||
params: &Parameters,
|
||||
signing_secret_key: &SecretKey,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[&Attribute],
|
||||
) -> Result<BlindedSignature> {
|
||||
let num_private = blind_sign_request.private_attributes_commitments.len();
|
||||
let hs = params.gen_hs();
|
||||
|
||||
if num_private + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: num_private + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the commitment hash
|
||||
let h = compute_hash(blind_sign_request.commitment, public_attributes);
|
||||
if bool::from(blind_sign_request.commitment_hash.is_identity()) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Commitment hash should not be an identity point".to_string(),
|
||||
));
|
||||
}
|
||||
if !(h == blind_sign_request.commitment_hash) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the commitment hash".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Verify the ZK proof
|
||||
if !blind_sign_request.verify_proof(params, public_attributes) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the proof of knowledge".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// in python implementation there are n^2 G1 multiplications, let's do it with a single one instead.
|
||||
// i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes)
|
||||
// rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately
|
||||
let signed_public = h * public_attributes
|
||||
.iter()
|
||||
.zip(signing_secret_key.ys.iter().skip(num_private))
|
||||
.map(|(&attr, yi)| attr * yi)
|
||||
.sum::<Scalar>();
|
||||
|
||||
// h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
|
||||
let sig = blind_sign_request
|
||||
.private_attributes_commitments
|
||||
.iter()
|
||||
.zip(signing_secret_key.ys.iter())
|
||||
.map(|(c, yi)| c * yi)
|
||||
.chain(std::iter::once(h * signing_secret_key.x))
|
||||
.chain(std::iter::once(signed_public))
|
||||
.sum();
|
||||
|
||||
Ok(BlindedSignature(h, sig))
|
||||
}
|
||||
|
||||
/// Verifies a partial blind signature using the provided parameters and validator's verification key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A reference to the cryptographic parameters.
|
||||
/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
|
||||
/// * `public_attributes` - A reference to the public attributes included in the client's request.
|
||||
/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
|
||||
/// * `partial_verification_key` - A reference to the validator's partial verification key.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// This function verifies the correctness and validity of a partial blind signature using
|
||||
/// the provided cryptographic parameters, blind signature request, blinded signature,
|
||||
/// and partial verification key.
|
||||
/// It calculates pairings based on the provided values and checks whether the partial blind signature
|
||||
/// is consistent with the verification key and commitments in the blind signature request.
|
||||
/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
|
||||
pub fn verify_partial_blind_signature(
|
||||
params: &Parameters,
|
||||
private_attribute_commitments: &[G1Projective],
|
||||
public_attributes: &[&Attribute],
|
||||
blind_sig: &BlindedSignature,
|
||||
partial_verification_key: &VerificationKey,
|
||||
) -> bool {
|
||||
let num_private_attributes = private_attribute_commitments.len();
|
||||
if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
|
||||
return false;
|
||||
}
|
||||
if bool::from(blind_sig.0.is_identity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: we're losing some memory here due to extra allocation,
|
||||
// but worst-case scenario (given SANE amount of attributes), it's just few kb at most
|
||||
let c_neg = blind_sig.1.to_affine().neg();
|
||||
let g2_prep = params.prepared_miller_g2();
|
||||
|
||||
let mut terms = vec![
|
||||
// (c^{-1}, g2)
|
||||
(c_neg, g2_prep.clone()),
|
||||
// (s, alpha)
|
||||
(
|
||||
blind_sig.0.to_affine(),
|
||||
G2Prepared::from(partial_verification_key.alpha.to_affine()),
|
||||
),
|
||||
];
|
||||
|
||||
// for each private attribute, add (cm_i, beta_i) to the miller terms
|
||||
for (private_attr_commit, beta_g2) in private_attribute_commitments
|
||||
.iter()
|
||||
.zip(&partial_verification_key.beta_g2)
|
||||
{
|
||||
// (cm_i, beta_i)
|
||||
terms.push((
|
||||
private_attr_commit.to_affine(),
|
||||
G2Prepared::from(beta_g2.to_affine()),
|
||||
))
|
||||
}
|
||||
|
||||
// for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
|
||||
for (&pub_attr, beta_g2) in public_attributes.iter().zip(
|
||||
partial_verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
.skip(num_private_attributes),
|
||||
) {
|
||||
// (s^pub_j, beta_j)
|
||||
terms.push((
|
||||
(blind_sig.0 * pub_attr).to_affine(),
|
||||
G2Prepared::from(beta_g2.to_affine()),
|
||||
))
|
||||
}
|
||||
|
||||
// get the references to all the terms to get the arguments the miller loop expects
|
||||
#[allow(clippy::map_identity)]
|
||||
let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
|
||||
|
||||
// since checking whether e(a, b) == e(c, d)
|
||||
// is equivalent to checking e(a, b) • e(c, d)^{-1} == id
|
||||
// and thus to e(a, b) • e(c^{-1}, d) == id
|
||||
//
|
||||
// compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
|
||||
multi_miller_loop(&terms_refs)
|
||||
.final_exponentiation()
|
||||
.is_identity()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
|
||||
pub fn sign(secret_key: &SecretKey, public_attributes: &[&Attribute]) -> Result<Signature> {
|
||||
if public_attributes.len() > secret_key.ys.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: secret_key.ys.len(),
|
||||
requested: public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
//Serialize the array structure of the public attributes into a byte array
|
||||
let mut serialized_attributes = Vec::new();
|
||||
//Prepend the length of the entire array (in bytes)
|
||||
let array_len = public_attributes.len() as u64;
|
||||
serialized_attributes.extend_from_slice(&array_len.to_le_bytes());
|
||||
//Serialize each attribute with its length
|
||||
for &attribute in public_attributes.iter() {
|
||||
let attr_bytes = attribute.to_bytes();
|
||||
let attr_len = attr_bytes.len() as u64;
|
||||
|
||||
// Prefix the attribute with its length
|
||||
serialized_attributes.extend_from_slice(&attr_len.to_le_bytes());
|
||||
serialized_attributes.extend_from_slice(&attr_bytes);
|
||||
}
|
||||
|
||||
//Hash the resulting byte array to derive the point H
|
||||
let h = hash_g1(serialized_attributes);
|
||||
|
||||
// x + m0 * y0 + m1 * y1 + ... mn * yn
|
||||
let exponent = secret_key.x
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(secret_key.ys.iter())
|
||||
.map(|(&m_i, y_i)| m_i * y_i)
|
||||
.sum::<Scalar>();
|
||||
|
||||
let sig2 = h * exponent;
|
||||
Ok(Signature(h, sig2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
|
||||
#[test]
|
||||
fn blind_sign_request_bytes_roundtrip() {
|
||||
// 0 public and 1 private attribute
|
||||
let params = Parameters::new(1).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
random_scalars_refs!(public_attributes, params, 0);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
|
||||
// 2 public and 2 private attributes
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_blind_sign_non_identity_commitment_hash() {
|
||||
let params = Parameters::new(1).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
random_scalars_refs!(public_attributes, params, 0);
|
||||
|
||||
// Call the function to prepare the blind sign
|
||||
let result = prepare_blind_sign(¶ms, &private_attributes, &public_attributes);
|
||||
|
||||
// Ensure the result is Ok
|
||||
assert!(result.is_ok(), "prepare_blind_sign should succeed");
|
||||
|
||||
let (_, blind_sign_request) = result.unwrap();
|
||||
|
||||
// Ensure the commitment_hash is not the identity point
|
||||
assert!(
|
||||
!bool::from(blind_sign_request.commitment_hash.is_identity()),
|
||||
"commitment_hash should not be the identity point"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blind_sign_with_identity_commitment_hash() {
|
||||
let params = Parameters::new(1).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 1);
|
||||
random_scalars_refs!(public_attributes, params, 0);
|
||||
|
||||
// Call the function to prepare the blind sign
|
||||
let (_commitments_openings, blind_sign_request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
let blind_sign_request = BlindSignRequest {
|
||||
commitment_hash: G1Projective::identity(),
|
||||
..blind_sign_request // This copies the other fields from the existing instance
|
||||
};
|
||||
|
||||
let signing_secret_key = SecretKey {
|
||||
x: params.random_scalar(),
|
||||
ys: vec![params.random_scalar()],
|
||||
};
|
||||
|
||||
// Call blind_sign and ensure it returns an error due to identity commitment hash
|
||||
let result = blind_sign(
|
||||
¶ms,
|
||||
&signing_secret_key,
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
);
|
||||
|
||||
// The result should be an error
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"blind_sign should return an error when commitment_hash is the identity point"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request.private_attributes_commitments,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature_no_public_attributes() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(¶ms, validator_keypair.secret_key(), &request, &[]).unwrap();
|
||||
|
||||
assert!(verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request.private_attributes_commitments,
|
||||
&[],
|
||||
&blind_sig,
|
||||
validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_verify_partial_blind_signature_with_wrong_key() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let validator2_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// this assertion should fail, as we try to verify with a wrong validator key
|
||||
assert!(!verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request.private_attributes_commitments,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
validator2_keypair.verification_key()
|
||||
),);
|
||||
}
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use core::iter::Sum;
|
||||
use core::ops::{Add, Mul};
|
||||
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::aggregation::aggregate_verification_keys;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
use crate::traits::Bytable;
|
||||
use crate::utils::{
|
||||
try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar,
|
||||
try_deserialize_scalar_vec, Polynomial,
|
||||
};
|
||||
use crate::Base58;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
|
||||
#[cfg_attr(
|
||||
feature = "key-zeroize",
|
||||
derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
|
||||
)]
|
||||
pub struct SecretKey {
|
||||
pub(crate) x: Scalar,
|
||||
pub(crate) ys: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl PemStorableKey for SecretKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"COCONUT SECRET KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SecretKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
|
||||
// There should be x and at least one y
|
||||
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8,
|
||||
target: 32 * 2 + 8,
|
||||
modulus: 32,
|
||||
object: "secret key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
||||
let actual_ys_len = (bytes.len() - 40) / 32;
|
||||
|
||||
if ys_len as usize != actual_ys_len {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Tried to deserialize secret key with inconsistent ys len (expected {ys_len}, got {actual_ys_len})"
|
||||
)));
|
||||
}
|
||||
|
||||
let x = try_deserialize_scalar(
|
||||
&x_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()),
|
||||
)?;
|
||||
let ys = try_deserialize_scalar_vec(
|
||||
ys_len,
|
||||
&bytes[40..],
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(SecretKey { x, ys })
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
/// Following a (distributed) key generation process, scalar values can be obtained
|
||||
/// outside of the normal key generation process.
|
||||
pub fn create_from_raw(x: Scalar, ys: Vec<Scalar>) -> Self {
|
||||
Self { x, ys }
|
||||
}
|
||||
|
||||
/// Extract the Scalar copy of the underlying secrets.
|
||||
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
|
||||
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
|
||||
(self.x, self.ys.clone())
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.ys.len()
|
||||
}
|
||||
|
||||
/// Derive verification key using this secret key.
|
||||
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
|
||||
let g1 = params.gen1();
|
||||
let g2 = params.gen2();
|
||||
VerificationKey {
|
||||
alpha: g2 * self.x,
|
||||
beta_g1: self.ys.iter().map(|y| g1 * y).collect(),
|
||||
beta_g2: self.ys.iter().map(|y| g2 * y).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// x || ys.len() || ys
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let ys_len = self.ys.len();
|
||||
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.x.to_bytes());
|
||||
bytes.extend_from_slice(&ys_len.to_le_bytes());
|
||||
for y in self.ys.iter() {
|
||||
bytes.extend_from_slice(&y.to_bytes())
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey> {
|
||||
SecretKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for SecretKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
SecretKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for SecretKey {}
|
||||
|
||||
// TODO: perhaps change points to affine representation
|
||||
// to make verification slightly more efficient?
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct VerificationKey {
|
||||
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
|
||||
pub(crate) alpha: G2Projective,
|
||||
pub(crate) beta_g1: Vec<G1Projective>,
|
||||
pub(crate) beta_g2: Vec<G2Projective>,
|
||||
}
|
||||
|
||||
impl PemStorableKey for VerificationKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"COCONUT VERIFICATION KEY"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for VerificationKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
// There should be at least alpha, one betaG1 and one betaG2 and their length
|
||||
if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8 - 96,
|
||||
target: 96 * 2 + 48 + 8,
|
||||
modulus: 96 + 48,
|
||||
object: "verification key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
|
||||
|
||||
let actual_betas_len = (bytes.len() - 104) / (96 + 48);
|
||||
|
||||
if betas_len as usize != actual_betas_len {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize verification key with inconsistent betas len (expected {betas_len}, got {actual_betas_len})"
|
||||
)));
|
||||
}
|
||||
|
||||
let alpha = try_deserialize_g2_projective(
|
||||
&alpha_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (alpha)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut beta_g1 = Vec::with_capacity(betas_len as usize);
|
||||
let mut beta_g1_end: u64 = 0;
|
||||
for i in 0..betas_len {
|
||||
let start = (104 + i * 48) as usize;
|
||||
let end = start + 48;
|
||||
// we're using a constant 48 byte offset (which is the size of G1 compressed) so unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g1_projective(
|
||||
&beta_i_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G1 point (beta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
beta_g1_end = end as u64;
|
||||
beta_g1.push(beta_i)
|
||||
}
|
||||
|
||||
let mut beta_g2 = Vec::with_capacity(betas_len as usize);
|
||||
for i in 0..betas_len {
|
||||
let start = (beta_g1_end + i * 96) as usize;
|
||||
let end = start + 96;
|
||||
// we're using a constant 96 byte offset (which is the size of G2 compressed) so unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g2_projective(
|
||||
&beta_i_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (beta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
beta_g2.push(beta_i)
|
||||
}
|
||||
|
||||
Ok(VerificationKey {
|
||||
alpha,
|
||||
beta_g1,
|
||||
beta_g2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Add<&'b VerificationKey> for VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: &'b VerificationKey) -> VerificationKey {
|
||||
// If you're trying to add two keys together that were created
|
||||
// for different number of attributes, just panic as it's a
|
||||
// nonsense operation.
|
||||
assert_eq!(
|
||||
self.beta_g1.len(),
|
||||
rhs.beta_g1.len(),
|
||||
"trying to add verification keys generated for different number of attributes [G1]"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
self.beta_g2.len(),
|
||||
rhs.beta_g2.len(),
|
||||
"trying to add verification keys generated for different number of attributes [G2]"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
self.beta_g1.len(),
|
||||
self.beta_g2.len(),
|
||||
"this key is incorrect - the number of elements G1 and G2 does not match"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rhs.beta_g1.len(),
|
||||
rhs.beta_g2.len(),
|
||||
"they key you want to add is incorrect - the number of elements G1 and G2 does not match"
|
||||
);
|
||||
|
||||
VerificationKey {
|
||||
alpha: self.alpha + rhs.alpha,
|
||||
beta_g1: self
|
||||
.beta_g1
|
||||
.iter()
|
||||
.zip(rhs.beta_g1.iter())
|
||||
.map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1)
|
||||
.collect(),
|
||||
beta_g2: self
|
||||
.beta_g2
|
||||
.iter()
|
||||
.zip(rhs.beta_g2.iter())
|
||||
.map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Scalar> for &VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: Scalar) -> Self::Output {
|
||||
VerificationKey {
|
||||
alpha: self.alpha * rhs,
|
||||
beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(),
|
||||
beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sum<T> for VerificationKey
|
||||
where
|
||||
T: Borrow<VerificationKey>,
|
||||
{
|
||||
#[inline]
|
||||
fn sum<I>(iter: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = T>,
|
||||
{
|
||||
let mut peekable = iter.peekable();
|
||||
let head_attributes = match peekable.peek() {
|
||||
Some(head) => head.borrow().beta_g2.len(),
|
||||
None => {
|
||||
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
|
||||
// of VerificationKey. So should it panic here or just return some nonsense value?
|
||||
return VerificationKey::identity(0);
|
||||
}
|
||||
};
|
||||
|
||||
peekable.fold(VerificationKey::identity(head_attributes), |acc, item| {
|
||||
acc + item.borrow()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VerificationKey {
|
||||
/// Create a (kinda) identity verification key using specified
|
||||
/// number of 'beta' elements
|
||||
pub(crate) fn identity(beta_size: usize) -> Self {
|
||||
VerificationKey {
|
||||
alpha: G2Projective::identity(),
|
||||
beta_g1: vec![G1Projective::identity(); beta_size],
|
||||
beta_g2: vec![G2Projective::identity(); beta_size],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
|
||||
aggregate_verification_keys(sigs, indices)
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> &G2Projective {
|
||||
&self.alpha
|
||||
}
|
||||
|
||||
pub fn beta_g1(&self) -> &Vec<G1Projective> {
|
||||
&self.beta_g1
|
||||
}
|
||||
|
||||
pub fn beta_g2(&self) -> &Vec<G2Projective> {
|
||||
&self.beta_g2
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let beta_g1_len = self.beta_g1.len();
|
||||
let beta_g2_len = self.beta_g2.len();
|
||||
let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96);
|
||||
|
||||
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
|
||||
|
||||
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
|
||||
|
||||
for beta_g1 in self.beta_g1.iter() {
|
||||
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
|
||||
}
|
||||
|
||||
for beta_g2 in self.beta_g2.iter() {
|
||||
bytes.extend_from_slice(&beta_g2.to_affine().to_compressed())
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
VerificationKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for VerificationKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
VerificationKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for VerificationKey {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerificationKeyShare {
|
||||
pub key: VerificationKey,
|
||||
pub index: SignerIndex,
|
||||
}
|
||||
|
||||
impl From<(VerificationKey, SignerIndex)> for VerificationKeyShare {
|
||||
fn from(value: (VerificationKey, SignerIndex)) -> Self {
|
||||
VerificationKeyShare {
|
||||
key: value.0,
|
||||
index: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq, Clone))]
|
||||
pub struct KeyPair {
|
||||
secret_key: SecretKey,
|
||||
verification_key: VerificationKey,
|
||||
|
||||
/// Optional index value specifying polynomial point used during threshold key generation.
|
||||
pub index: Option<SignerIndex>,
|
||||
}
|
||||
|
||||
impl From<KeyPair> for (SecretKey, VerificationKey) {
|
||||
fn from(value: KeyPair) -> Self {
|
||||
(value.secret_key, value.verification_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKeyPair for KeyPair {
|
||||
type PrivatePemKey = SecretKey;
|
||||
type PublicPemKey = VerificationKey;
|
||||
|
||||
fn private_key(&self) -> &Self::PrivatePemKey {
|
||||
&self.secret_key
|
||||
}
|
||||
|
||||
fn public_key(&self) -> &Self::PublicPemKey {
|
||||
&self.verification_key
|
||||
}
|
||||
|
||||
fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self {
|
||||
Self::from_keys(secret_key, verification_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
const MARKER_BYTES: &'static [u8] = b"coconutkeypair";
|
||||
|
||||
pub fn from_keys(secret_key: SecretKey, verification_key: VerificationKey) -> Self {
|
||||
Self {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret_key(&self) -> &SecretKey {
|
||||
&self.secret_key
|
||||
}
|
||||
|
||||
pub fn verification_key(&self) -> &VerificationKey {
|
||||
&self.verification_key
|
||||
}
|
||||
|
||||
pub fn to_verification_key_share(&self) -> Option<VerificationKeyShare> {
|
||||
self.index.map(|index| VerificationKeyShare {
|
||||
key: self.verification_key.clone(),
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from_byte_slice(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for KeyPair {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
let mut byts = vec![];
|
||||
let secret_key_bytes = self.secret_key.to_bytes();
|
||||
let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes();
|
||||
let verification_key_bytes = self.verification_key.to_bytes();
|
||||
let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes();
|
||||
byts.extend_from_slice(Self::MARKER_BYTES);
|
||||
byts.extend_from_slice(&secret_key_len);
|
||||
byts.extend_from_slice(&secret_key_bytes);
|
||||
byts.extend_from_slice(&verification_key_len);
|
||||
byts.extend_from_slice(&verification_key_bytes);
|
||||
if let Some(index) = self.index {
|
||||
byts.extend_from_slice(&index.to_le_bytes())
|
||||
}
|
||||
byts
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for KeyPair {}
|
||||
|
||||
impl TryFrom<&[u8]> for KeyPair {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<KeyPair> {
|
||||
let header_len = Self::MARKER_BYTES.len();
|
||||
|
||||
// we must be able to at the very least read the length of secret key which is past the header
|
||||
// and is 8 bytes long
|
||||
if bytes.len() < header_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: header_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// safety: we made bound check and we're using constant offest
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let secret_key_len =
|
||||
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
|
||||
let secret_key_start = header_len + 8;
|
||||
|
||||
let secret_key =
|
||||
SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?;
|
||||
|
||||
// we must be able to read the length of verification key
|
||||
if bytes.len() < secret_key_start + secret_key_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: secret_key_start + secret_key_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// safety: we made bound check
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let verification_key_len = u64::from_le_bytes(
|
||||
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
) as usize;
|
||||
let verification_key_start = secret_key_start + secret_key_len + 8;
|
||||
|
||||
let verification_key = VerificationKey::try_from(
|
||||
&bytes[verification_key_start..verification_key_start + verification_key_len],
|
||||
)?;
|
||||
let consumed_bytes = verification_key_start + verification_key_len;
|
||||
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Some(u64::from_le_bytes(
|
||||
bytes[consumed_bytes..consumed_bytes + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
|
||||
///
|
||||
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
|
||||
/// that are independent of each other.
|
||||
pub fn keygen(params: &Parameters) -> KeyPair {
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
let x = params.random_scalar();
|
||||
let ys = params.n_random_scalars(attributes);
|
||||
|
||||
let secret_key = SecretKey { x, ys };
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate Coconut keypairs.
|
||||
///
|
||||
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
|
||||
/// such that they support threshold aggregation by `threshold` number of parties.
|
||||
/// It is expected that this procedure is executed by a Trusted Third Party.
|
||||
pub fn ttp_keygen(
|
||||
params: &Parameters,
|
||||
threshold: u64,
|
||||
num_authorities: u64,
|
||||
) -> Result<Vec<KeyPair>> {
|
||||
if threshold == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to generate threshold keys with a 0 threshold value".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if threshold > num_authorities {
|
||||
return Err(
|
||||
CoconutError::Setup(
|
||||
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
// generate polynomials
|
||||
let v = Polynomial::new_random(params, threshold - 1);
|
||||
let ws = (0..attributes)
|
||||
.map(|_| Polynomial::new_random(params, threshold - 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: potentially if we had some known authority identifier we could use that instead
|
||||
// of the increasing (1,2,3,...) sequence
|
||||
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
|
||||
|
||||
// generate polynomial shares
|
||||
let x = polynomial_indices
|
||||
.iter()
|
||||
.map(|&id| v.evaluate(&Scalar::from(id)));
|
||||
let ys = polynomial_indices.iter().map(|&id| {
|
||||
ws.iter()
|
||||
.map(|w| w.evaluate(&Scalar::from(id)))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// finally set the keys
|
||||
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys });
|
||||
|
||||
let keypairs = secret_keys
|
||||
.zip(polynomial_indices.iter())
|
||||
.map(|(secret_key, index)| {
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: Some(*index),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keypair_bytes_roundtrip() {
|
||||
let params1 = setup(1).unwrap();
|
||||
let params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms1);
|
||||
let keypair5 = keygen(¶ms5);
|
||||
|
||||
let bytes1 = keypair1.to_bytes();
|
||||
let bytes5 = keypair5.to_bytes();
|
||||
|
||||
assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1);
|
||||
assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_key_bytes_roundtrip() {
|
||||
let params1 = setup(1).unwrap();
|
||||
let params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms1);
|
||||
let keypair5 = keygen(¶ms5);
|
||||
|
||||
let bytes1 = keypair1.secret_key.to_bytes();
|
||||
let bytes5 = keypair5.secret_key.to_bytes();
|
||||
|
||||
assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key);
|
||||
assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_key_bytes_roundtrip() {
|
||||
let params1 = setup(1).unwrap();
|
||||
let params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = &keygen(¶ms1);
|
||||
let keypair5 = &keygen(¶ms5);
|
||||
|
||||
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
|
||||
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
|
||||
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes1.as_slice()).unwrap(),
|
||||
keypair1.verification_key
|
||||
);
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes5.as_slice()).unwrap(),
|
||||
keypair5.verification_key
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,672 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// TODO: implement https://crates.io/crates/signature traits?
|
||||
|
||||
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
|
||||
pub use keygen::{SecretKey, VerificationKey};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g1_projective;
|
||||
use crate::Attribute;
|
||||
|
||||
pub mod aggregation;
|
||||
pub mod double_use;
|
||||
pub mod issuance;
|
||||
pub mod keygen;
|
||||
pub mod setup;
|
||||
pub mod verification;
|
||||
|
||||
pub type SignerIndex = u64;
|
||||
|
||||
// (h, s)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
pub type PartialSignature = Signature;
|
||||
|
||||
impl TryFrom<&[u8]> for Signature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Signature> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Signature must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let sig1 = try_deserialize_g1_projective(
|
||||
sig1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()),
|
||||
)?;
|
||||
|
||||
let sig2 = try_deserialize_g1_projective(
|
||||
sig2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Signature(sig1, sig2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub(crate) fn sig1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub(crate) fn sig2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn randomise_simple(&self, params: &Parameters) -> Signature {
|
||||
let r = params.random_scalar();
|
||||
Signature(self.0 * r, self.1 * r)
|
||||
}
|
||||
|
||||
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
|
||||
let r = params.random_scalar();
|
||||
let r_prime = params.random_scalar();
|
||||
let h_prime = self.0 * r_prime;
|
||||
let s_prime = (self.1 * r_prime) + (h_prime * r);
|
||||
(Signature(h_prime, s_prime), r)
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
|
||||
Signature::try_from(bytes)
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
) -> Result<()> {
|
||||
// Verify the commitment hash
|
||||
if bool::from(self.0.is_identity()) {
|
||||
return Err(CoconutError::Verification(
|
||||
"Commitment hash should not be an identity point".to_string(),
|
||||
));
|
||||
}
|
||||
if !(commitment_hash == &self.0) {
|
||||
return Err(CoconutError::Verification(
|
||||
"Verification of commitment hash from signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let alpha = partial_verification_key.alpha;
|
||||
|
||||
let signed_attributes = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(partial_verification_key.beta_g2.iter())
|
||||
.map(|(&attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// Verify the signature share
|
||||
if !check_bilinear_pairing(
|
||||
&self.0.to_affine(),
|
||||
&G2Prepared::from((alpha + signed_attributes).to_affine()),
|
||||
&self.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of signature share failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Signature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Signature::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Signature {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct BlindedSignature(G1Projective, G1Projective);
|
||||
|
||||
impl Bytable for BlindedSignature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Self::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindedSignature {}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindedSignature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"BlindedSignature must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::expect_used)]
|
||||
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
#[allow(clippy::expect_used)]
|
||||
let sig_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let h = try_deserialize_g1_projective(
|
||||
h_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed h".to_string()),
|
||||
)?;
|
||||
let sig = try_deserialize_g1_projective(
|
||||
sig_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(BlindedSignature(h, sig))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlindedSignature {
|
||||
pub fn unblind(
|
||||
&self,
|
||||
partial_verification_key: &VerificationKey,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Signature {
|
||||
// parse the signature
|
||||
let h = &self.0;
|
||||
let c = &self.1;
|
||||
let blinding_removers = partial_verification_key
|
||||
.beta_g1
|
||||
.iter()
|
||||
.zip(pedersen_commitments_openings.iter())
|
||||
.map(|(beta, opening)| beta * opening)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let unblinded_c = c - blinding_removers;
|
||||
|
||||
Signature(*h, unblinded_c)
|
||||
}
|
||||
|
||||
pub fn unblind_and_verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[&Attribute],
|
||||
public_attributes: &[&Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
pedersen_commitments_openings: &[Scalar],
|
||||
) -> Result<Signature> {
|
||||
let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings);
|
||||
unblinded.verify(
|
||||
params,
|
||||
partial_verification_key,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
commitment_hash,
|
||||
)?;
|
||||
Ok(unblinded)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
BlindedSignature::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// perhaps this should take signature by reference? we'll see how it goes
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignatureShare {
|
||||
signature: Signature,
|
||||
index: SignerIndex,
|
||||
}
|
||||
|
||||
impl From<(Signature, SignerIndex)> for SignatureShare {
|
||||
fn from(value: (Signature, SignerIndex)) -> Self {
|
||||
SignatureShare {
|
||||
signature: value.0,
|
||||
index: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureShare {
|
||||
pub fn new(signature: Signature, index: SignerIndex) -> Self {
|
||||
SignatureShare { signature, index }
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn index(&self) -> SignerIndex {
|
||||
self.index
|
||||
}
|
||||
|
||||
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
|
||||
// aggregate_signature_shares(shares)
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hash_to_scalar;
|
||||
use crate::scheme::aggregation::{
|
||||
aggregate_signatures_and_verify, aggregate_verification_keys,
|
||||
};
|
||||
use crate::scheme::issuance::{blind_sign, compute_hash, prepare_blind_sign, sign};
|
||||
use crate::scheme::keygen::{keygen, ttp_keygen};
|
||||
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
|
||||
use crate::tests::helpers::random_scalars_refs;
|
||||
|
||||
#[test]
|
||||
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(private_attributes, params, 2);
|
||||
|
||||
let (_commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
|
||||
let wrong_commitment_opening = params.random_scalar();
|
||||
let wrong_commitment = params.gen1() * wrong_commitment_opening;
|
||||
let fake_commitment_hash = compute_hash(wrong_commitment, &[]);
|
||||
let wrong_commitments_openings = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
assert!(sig1
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&fake_commitment_hash,
|
||||
&wrong_commitments_openings,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unblind_returns_error_if_signature_verification_fails() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let p = [hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
|
||||
let private_attributes = vec![&p[0], &p[1]];
|
||||
|
||||
let p2 = [hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
|
||||
let private_attributes2 = vec![&p2[0], &p2[1]];
|
||||
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap();
|
||||
|
||||
assert!(sig1
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&private_attributes2,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_private_attributes() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[])
|
||||
.unwrap()
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &[])
|
||||
.unwrap()
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&sig1,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&sig2,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&theta1,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_attributes() {
|
||||
let params = Parameters::new(2).unwrap();
|
||||
random_scalars_refs!(attributes, params, 2);
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
let sig1 = sign(keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(keypair2.secret_key(), &attributes).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig2,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypair1 = keygen(¶ms);
|
||||
let keypair2 = keygen(¶ms);
|
||||
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&sig1,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&sig2,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&theta1,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
keypair2.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
keypair1.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
let keypairs = ttp_keygen(¶ms, 2, 3).unwrap();
|
||||
|
||||
let (commitments_openings, lambda) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let sigs = keypairs
|
||||
.iter()
|
||||
.map(|keypair| {
|
||||
blind_sign(¶ms, keypair.secret_key(), &lambda, &public_attributes)
|
||||
.unwrap()
|
||||
.unblind_and_verify(
|
||||
¶ms,
|
||||
keypair.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_sig = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&attributes,
|
||||
&sigs[..2],
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
// taking different subset of keys and credentials
|
||||
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
|
||||
let aggr_sig = aggregate_signatures_and_verify(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&attributes,
|
||||
&sigs[1..],
|
||||
Some(&[2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = signature.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
signature.0.to_affine().to_compressed(),
|
||||
signature.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blinded_signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let blinded_sig = BlindedSignature(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = blinded_sig.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
blinded_sig.0.to_affine().to_compressed(),
|
||||
blinded_sig.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar};
|
||||
use ff::Field;
|
||||
use group::Curve;
|
||||
use rand::thread_rng;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::utils::hash_g1;
|
||||
|
||||
/// System-wide parameters used for the protocol
|
||||
#[derive(Clone)]
|
||||
pub struct Parameters {
|
||||
/// Generator of the G1 group
|
||||
g1: G1Affine,
|
||||
|
||||
/// Additional generators of the G1 group
|
||||
hs: Vec<G1Affine>,
|
||||
|
||||
/// Generator of the G2 group
|
||||
g2: G2Affine,
|
||||
|
||||
/// Precomputed G2 generator used for the miller loop
|
||||
_g2_prepared_miller: G2Prepared,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
pub fn new(num_attributes: u32) -> Result<Parameters> {
|
||||
if num_attributes == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to setup the scheme for 0 attributes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = (1..=num_attributes)
|
||||
.map(|i| hash_g1(format!("h{i}")).to_affine())
|
||||
.collect();
|
||||
|
||||
Ok(Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs,
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gen1(&self) -> &G1Affine {
|
||||
&self.g1
|
||||
}
|
||||
|
||||
pub fn gen2(&self) -> &G2Affine {
|
||||
&self.g2
|
||||
}
|
||||
|
||||
pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared {
|
||||
&self._g2_prepared_miller
|
||||
}
|
||||
|
||||
pub fn gen_hs(&self) -> &[G1Affine] {
|
||||
&self.hs
|
||||
}
|
||||
|
||||
pub fn random_scalar(&self) -> Scalar {
|
||||
// lazily-initialized thread-local random number generator, seeded by the system
|
||||
let mut rng = thread_rng();
|
||||
Scalar::random(&mut rng)
|
||||
}
|
||||
|
||||
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
|
||||
(0..n).map(|_| self.random_scalar()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(num_attributes: u32) -> Result<Parameters> {
|
||||
Parameters::new(num_attributes)
|
||||
}
|
||||
|
||||
// for ease of use in tests requiring params
|
||||
// TODO: not sure if this will have to go away when tests require some specific number of generators
|
||||
#[cfg(test)]
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs: Vec::new(),
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofKappaZeta;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::Signature;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g2_projective;
|
||||
use crate::Attribute;
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
|
||||
use core::ops::Neg;
|
||||
use group::{Curve, Group};
|
||||
|
||||
pub use crate::scheme::double_use::BlindedSerialNumber;
|
||||
|
||||
// TODO NAMING: this whole thing
|
||||
// Theta
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct VerifyCredentialRequest {
|
||||
// blinded_message (kappa)
|
||||
pub blinded_message: G2Projective,
|
||||
// blinded serial number (zeta)
|
||||
pub blinded_serial_number: BlindedSerialNumber,
|
||||
// sigma
|
||||
pub credential: Signature,
|
||||
// pi_v
|
||||
pub pi_v: ProofKappaZeta,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for VerifyCredentialRequest {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
|
||||
if bytes.len() < 288 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// safety: we just checked for the length so the unwraps are fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let blinded_message_bytes = bytes[..96].try_into().unwrap();
|
||||
let blinded_message = try_deserialize_g2_projective(
|
||||
&blinded_message_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"failed to deserialize the blinded message (kappa)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let blinded_serial_number_bytes = &bytes[96..192];
|
||||
let blinded_serial_number =
|
||||
BlindedSerialNumber::try_from_byte_slice(blinded_serial_number_bytes)?;
|
||||
|
||||
let credential = Signature::try_from(&bytes[192..288])?;
|
||||
|
||||
let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?;
|
||||
|
||||
Ok(VerifyCredentialRequest {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
credential,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifyCredentialRequest {
|
||||
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
|
||||
self.pi_v.verify(
|
||||
params,
|
||||
verification_key,
|
||||
&self.blinded_message,
|
||||
&self.blinded_serial_number,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn has_blinded_serial_number(&self, blinded_serial_number_bs58: &str) -> Result<bool> {
|
||||
let blinded_serial_number = BlindedSerialNumber::try_from_bs58(blinded_serial_number_bs58)?;
|
||||
let ret = self.blinded_serial_number.eq(&blinded_serial_number);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
// blinded message (kappa) || blinded serial number (zeta) || credential || pi_v
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
|
||||
let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed();
|
||||
let credential_bytes = self.credential.to_bytes();
|
||||
let proof_bytes = self.pi_v.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(288 + proof_bytes.len());
|
||||
bytes.extend_from_slice(&blinded_message_bytes);
|
||||
bytes.extend_from_slice(&blinded_serial_number_bytes);
|
||||
bytes.extend_from_slice(&credential_bytes);
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<VerifyCredentialRequest> {
|
||||
VerifyCredentialRequest::try_from(bytes)
|
||||
}
|
||||
|
||||
pub fn blinded_serial_number(&self) -> BlindedSerialNumber {
|
||||
self.blinded_serial_number
|
||||
}
|
||||
|
||||
pub fn blinded_serial_number_bs58(&self) -> String {
|
||||
self.blinded_serial_number.to_bs58()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for VerifyCredentialRequest {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
VerifyCredentialRequest::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for VerifyCredentialRequest {}
|
||||
|
||||
pub fn compute_kappa(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
private_attributes: &[&Attribute],
|
||||
blinding_factor: Scalar,
|
||||
) -> G2Projective {
|
||||
params.gen2() * blinding_factor
|
||||
+ verification_key.alpha
|
||||
+ private_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(&priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>()
|
||||
}
|
||||
|
||||
pub fn compute_zeta(params: &Parameters, serial_number: &Attribute) -> G2Projective {
|
||||
params.gen2() * serial_number
|
||||
}
|
||||
|
||||
pub fn prove_bandwidth_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
signature: &Signature,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
) -> Result<VerifyCredentialRequest> {
|
||||
if verification_key.beta_g2.len() < 2 {
|
||||
return Err(
|
||||
CoconutError::Verification(
|
||||
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
|
||||
verification_key.beta_g2.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Randomize the signature
|
||||
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
|
||||
|
||||
// blinded_message : kappa in the paper.
|
||||
// Value kappa is needed since we want to show a signature sigma'.
|
||||
// In order to verify sigma' we need both the verification key vk and the message m.
|
||||
// However, we do not want to reveal m to whomever we are showing the signature.
|
||||
// Thus, we need kappa which allows us to verify sigma'. In particular,
|
||||
// kappa is computed on m as input, but thanks to the use or random value r,
|
||||
// it does not reveal any information about m.
|
||||
let private_attributes = [serial_number, binding_number];
|
||||
let blinded_message = compute_kappa(
|
||||
params,
|
||||
verification_key,
|
||||
&private_attributes,
|
||||
sign_blinding_factor,
|
||||
);
|
||||
|
||||
// zeta is a commitment to the serial number (i.e., a public value associated with the serial number)
|
||||
let blinded_serial_number = compute_zeta(params, serial_number);
|
||||
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
params,
|
||||
verification_key,
|
||||
serial_number,
|
||||
binding_number,
|
||||
&sign_blinding_factor,
|
||||
&blinded_message,
|
||||
&blinded_serial_number,
|
||||
);
|
||||
|
||||
Ok(VerifyCredentialRequest {
|
||||
blinded_message,
|
||||
blinded_serial_number: blinded_serial_number.into(),
|
||||
credential: signature_prime,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether e(P, Q) * e(-R, S) == id
|
||||
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
|
||||
// checking e(P, Q) * e(-R, S) == id
|
||||
// is equivalent to checking e(P, Q) == e(R, S)
|
||||
// but requires only a single final exponentiation rather than two of them
|
||||
// and therefore, as seen via benchmarks.rs, is almost 50% faster
|
||||
// (1.47ms vs 2.45ms, tested on R9 5900X)
|
||||
|
||||
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
|
||||
multi_miller.final_exponentiation().is_identity().into()
|
||||
}
|
||||
|
||||
pub fn check_vk_pairing(
|
||||
params: &Parameters,
|
||||
dkg_values: &[G2Projective],
|
||||
vk: &VerificationKey,
|
||||
) -> bool {
|
||||
let values_len = dkg_values.len();
|
||||
if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if &vk.alpha != *dkg_values.first().as_ref().unwrap() {
|
||||
return false;
|
||||
}
|
||||
let dkg_betas = &dkg_values[1..];
|
||||
if dkg_betas
|
||||
.iter()
|
||||
.zip(vk.beta_g2.iter())
|
||||
.any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| {
|
||||
!check_bilinear_pairing(
|
||||
params.gen1(),
|
||||
&G2Prepared::from(g2.to_affine()),
|
||||
&g1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
)
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn verify_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
theta: &VerifyCredentialRequest,
|
||||
public_attributes: &[&Attribute],
|
||||
) -> bool {
|
||||
if public_attributes.len() + theta.pi_v.private_attributes_len()
|
||||
> verification_key.beta_g2.len()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if !theta.verify_proof(params, verification_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let kappa = if public_attributes.is_empty() {
|
||||
theta.blinded_message
|
||||
} else {
|
||||
let signed_public_attributes = public_attributes
|
||||
.iter()
|
||||
.zip(
|
||||
verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
.skip(theta.pi_v.private_attributes_len()),
|
||||
)
|
||||
.map(|(&pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
theta.blinded_message + signed_public_attributes
|
||||
};
|
||||
|
||||
check_bilinear_pairing(
|
||||
&theta.credential.0.to_affine(),
|
||||
&G2Prepared::from(kappa.to_affine()),
|
||||
&(theta.credential.1).to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(theta.credential.0.is_identity())
|
||||
}
|
||||
|
||||
// Used in tests only
|
||||
pub fn verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
public_attributes: &[&Attribute],
|
||||
sig: &Signature,
|
||||
) -> bool {
|
||||
let kappa = (verification_key.alpha
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta_g2.iter())
|
||||
.map(|(&m_i, b_i)| b_i * m_i)
|
||||
.sum::<G2Projective>())
|
||||
.to_affine();
|
||||
|
||||
check_bilinear_pairing(
|
||||
&sig.0.to_affine(),
|
||||
&G2Prepared::from(kappa),
|
||||
&sig.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(sig.0.is_identity())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::issuance::sign;
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn vk_pairing() {
|
||||
let params = setup(2).unwrap();
|
||||
let keypair = keygen(¶ms);
|
||||
let vk = keypair.verification_key();
|
||||
|
||||
let mut dkg_values = vec![vk.alpha];
|
||||
dkg_values.append(&mut vk.beta_g2.clone());
|
||||
assert!(check_vk_pairing(¶ms, &dkg_values, vk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theta_bytes_roundtrip() {
|
||||
let params = setup(2).unwrap();
|
||||
|
||||
let keypair = keygen(¶ms);
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
keypair.verification_key(),
|
||||
&signature,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = theta.to_bytes();
|
||||
assert_eq!(
|
||||
VerifyCredentialRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
theta
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_forged_signature_via_linear_combination() {
|
||||
// This test checks if the protocol correctly rejects forged signatures created
|
||||
// by linear combinations of valid signatures. The verification for forged
|
||||
// signatures should fail.
|
||||
let params = Parameters::new(4).unwrap();
|
||||
|
||||
let scalar_2 = Scalar::one() + Scalar::one();
|
||||
let scalar_2_inv = Scalar::invert(&scalar_2).unwrap();
|
||||
|
||||
//#1
|
||||
let a = params.random_scalar();
|
||||
let zero = Scalar::zero();
|
||||
let a_zero = vec![&a, &zero];
|
||||
let zero_a = vec![&zero, &a];
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
|
||||
//#2
|
||||
let sig_a_zero = sign(validator_keypair.secret_key(), &a_zero).unwrap();
|
||||
let sig_zero_a = sign(validator_keypair.secret_key(), &zero_a).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
validator_keypair.verification_key(),
|
||||
&a_zero,
|
||||
&sig_a_zero
|
||||
));
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
validator_keypair.verification_key(),
|
||||
&zero_a,
|
||||
&sig_zero_a
|
||||
));
|
||||
|
||||
//#3
|
||||
let h0 = sig_a_zero.0;
|
||||
// Removed unnecessary references
|
||||
let h1 = scalar_2_inv * sig_a_zero.1 + scalar_2_inv * sig_zero_a.1;
|
||||
let forged_signature = Signature(h0, h1);
|
||||
let a_half = a * scalar_2_inv;
|
||||
let new_plaintext = vec![&a_half, &a_half];
|
||||
|
||||
// The forged signature should not pass verification
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
validator_keypair.verification_key(),
|
||||
&new_plaintext,
|
||||
&forged_signature
|
||||
));
|
||||
|
||||
//#4
|
||||
let scalar_3 = Scalar::one() + Scalar::one() + Scalar::one();
|
||||
let scalar_4 = Scalar::one() + Scalar::one() + Scalar::one() + Scalar::one();
|
||||
let scalar_4_inv = Scalar::invert(&scalar_4).unwrap();
|
||||
let scalar_3_over_4 = scalar_3 * scalar_4_inv;
|
||||
|
||||
// Removed unnecessary references
|
||||
let h1_2 = scalar_4_inv * sig_a_zero.1 + scalar_3_over_4 * sig_zero_a.1;
|
||||
let forged_signature_2 = Signature(h0, h1_2);
|
||||
let a_quarter = a * scalar_4_inv;
|
||||
let a_3_over_4 = a * scalar_3_over_4;
|
||||
let new_plaintext_2 = vec![&a_quarter, &a_3_over_4];
|
||||
|
||||
// The second forged signature should also not pass verification
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
validator_keypair.verification_key(),
|
||||
&new_plaintext_2,
|
||||
&forged_signature_2
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::random_scalars_refs;
|
||||
use crate::tests::helpers::tests::generate_dkg_keys;
|
||||
use crate::{
|
||||
aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential,
|
||||
CoconutError, VerificationKey,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn keygen() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
let node_indices = vec![15u64, 248, 33521];
|
||||
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?;
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
let theta = theta_from_keys_and_attributes(
|
||||
¶ms,
|
||||
&coconut_keypairs,
|
||||
&node_indices,
|
||||
&public_attributes,
|
||||
)?;
|
||||
|
||||
// Verify credentials
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn dkg() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
let node_indices = vec![15u64, 248, 33521];
|
||||
|
||||
random_scalars_refs!(public_attributes, params, 2);
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = generate_dkg_keys(5, &node_indices);
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
let theta = theta_from_keys_and_attributes(
|
||||
¶ms,
|
||||
&coconut_keypairs,
|
||||
&node_indices,
|
||||
&public_attributes,
|
||||
)?;
|
||||
|
||||
// Verify credentials
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::*;
|
||||
use itertools::izip;
|
||||
use std::fmt::Debug;
|
||||
|
||||
// unwraps are fine in the test code
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn theta_from_keys_and_attributes(
|
||||
params: &Parameters,
|
||||
coconut_keypairs: &Vec<KeyPair>,
|
||||
indices: &[scheme::SignerIndex],
|
||||
public_attributes: &[&PublicAttribute],
|
||||
) -> Result<VerifyCredentialRequest, CoconutError> {
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![&serial_number, &binding_number];
|
||||
|
||||
// generate commitment
|
||||
let (commitments_openings, blind_sign_request) =
|
||||
prepare_blind_sign(params, &private_attributes, public_attributes)?;
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key().clone())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(indices))?;
|
||||
|
||||
// generate blinded signatures
|
||||
let mut blinded_signatures = Vec::new();
|
||||
|
||||
for keypair in coconut_keypairs {
|
||||
let blinded_signature = blind_sign(
|
||||
params,
|
||||
keypair.secret_key(),
|
||||
&blind_sign_request,
|
||||
public_attributes,
|
||||
)?;
|
||||
blinded_signatures.push(blinded_signature)
|
||||
}
|
||||
|
||||
// Unblind
|
||||
let unblinded_signatures: Vec<(scheme::SignerIndex, Signature)> = izip!(
|
||||
indices.iter(),
|
||||
blinded_signatures.iter(),
|
||||
verification_keys.iter()
|
||||
)
|
||||
.map(|(idx, s, vk)| {
|
||||
(
|
||||
*idx,
|
||||
s.unblind_and_verify(
|
||||
params,
|
||||
vk,
|
||||
&private_attributes,
|
||||
public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
&commitments_openings,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregate signatures
|
||||
let signature_shares: Vec<SignatureShare> = unblinded_signatures
|
||||
.iter()
|
||||
.map(|(idx, signature)| SignatureShare::new(*signature, *idx))
|
||||
.collect();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
|
||||
// Randomize credentials and generate any cryptographic material to verify them
|
||||
let signature = aggregate_signature_shares_and_verify(
|
||||
params,
|
||||
&verification_key,
|
||||
&attributes,
|
||||
&signature_shares,
|
||||
)?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
let theta = prove_bandwidth_credential(
|
||||
params,
|
||||
&verification_key,
|
||||
&signature,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
)?;
|
||||
|
||||
Ok(theta)
|
||||
}
|
||||
|
||||
// unwraps are fine in the test code
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn transpose_matrix<T: Debug>(matrix: Vec<Vec<T>>) -> Vec<Vec<T>> {
|
||||
if matrix.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let len = matrix[0].len();
|
||||
let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect();
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
iters
|
||||
.iter_mut()
|
||||
.map(|it| it.next().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! random_scalars_refs {
|
||||
( $x: ident, $params: expr, $n: expr ) => {
|
||||
let _vec = $params.n_random_scalars($n);
|
||||
#[allow(clippy::map_identity)]
|
||||
let $x = _vec.iter().collect::<Vec<_>>();
|
||||
};
|
||||
}
|
||||
|
||||
pub use random_scalars_refs;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use bls12_381::Scalar;
|
||||
use nym_dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
|
||||
pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec<Scalar> {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = nym_dkg::bte::setup();
|
||||
|
||||
// the simplest possible case
|
||||
let threshold = 2;
|
||||
|
||||
let mut receivers = std::collections::BTreeMap::new();
|
||||
let mut full_keys = Vec::new();
|
||||
for index in node_indices {
|
||||
let (dk, pk) = nym_dkg::bte::keygen(¶ms, &mut rng);
|
||||
receivers.insert(*index, *pk.public_key());
|
||||
full_keys.push((dk, pk))
|
||||
}
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
let shares = dealings
|
||||
.iter()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
|
||||
|
||||
derived_secrets.push(recovered_secret)
|
||||
}
|
||||
derived_secrets
|
||||
}
|
||||
pub fn generate_dkg_keys(num_attributes: u32, node_indices: &[NodeIndex]) -> Vec<KeyPair> {
|
||||
let params = Parameters::new(num_attributes).unwrap();
|
||||
let mut all_secrets = vec![];
|
||||
for _ in 0..num_attributes {
|
||||
let secrets = generate_dkg_secrets(node_indices);
|
||||
all_secrets.push(secrets);
|
||||
}
|
||||
let signers = transpose_matrix(all_secrets);
|
||||
signers
|
||||
.into_iter()
|
||||
.map(|mut secrets| {
|
||||
let x = secrets.pop().unwrap();
|
||||
let sk = SecretKey::create_from_raw(x, secrets);
|
||||
let vk = sk.verification_key(¶ms);
|
||||
KeyPair::from_keys(sk, vk)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod e2e;
|
||||
pub mod helpers;
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::CoconutError;
|
||||
use bls12_381::{G1Affine, G1Projective, Scalar};
|
||||
use group::GroupEncoding;
|
||||
|
||||
pub trait Bytable
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn to_byte_vec(&self) -> Vec<u8>;
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError>;
|
||||
}
|
||||
|
||||
pub trait Base58
|
||||
where
|
||||
Self: Bytable,
|
||||
{
|
||||
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
|
||||
let bs58_decoded = &bs58::decode(x.as_ref()).into_vec()?;
|
||||
Self::try_from_byte_slice(bs58_decoded)
|
||||
}
|
||||
fn to_bs58(&self) -> String {
|
||||
bs58::encode(self.to_byte_vec()).into_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Scalar {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
|
||||
let received = slice.len();
|
||||
let Ok(arr) = slice.try_into() else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "Scalar".to_string(),
|
||||
received,
|
||||
expected: 32,
|
||||
});
|
||||
};
|
||||
|
||||
let maybe_scalar = Scalar::from_bytes(arr);
|
||||
if maybe_scalar.is_none().into() {
|
||||
Err(CoconutError::ScalarDeserializationFailure)
|
||||
} else {
|
||||
// safety: this unwrap is fine as we've just checked the element is not none
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Ok(maybe_scalar.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Scalar {}
|
||||
|
||||
impl Bytable for G1Projective {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
|
||||
let received = slice.len();
|
||||
let arr: Result<[u8; 48], _> = slice.try_into();
|
||||
let Ok(bytes) = arr else {
|
||||
return Err(CoconutError::UnexpectedArrayLength {
|
||||
typ: "G1Projective".to_string(),
|
||||
received,
|
||||
expected: 48,
|
||||
});
|
||||
};
|
||||
|
||||
let maybe_g1 = G1Affine::from_compressed(&bytes);
|
||||
if maybe_g1.is_none().into() {
|
||||
Err(CoconutError::G1ProjectiveDeserializationFailure)
|
||||
} else {
|
||||
// safety: this unwrap is fine as we've just checked the element is not none
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Ok(maybe_g1.unwrap().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for G1Projective {}
|
||||
@@ -1,382 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
|
||||
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
|
||||
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
|
||||
pub struct Polynomial {
|
||||
coefficients: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl Polynomial {
|
||||
// for polynomial of degree n, we generate n+1 values
|
||||
// (for example for degree 1, like y = x + 2, we need [2,1])
|
||||
pub fn new_random(params: &Parameters, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: params.n_random_scalars((degree + 1) as usize),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the polynomial at point x.
|
||||
pub fn evaluate(&self, x: &Scalar) -> Scalar {
|
||||
if self.coefficients.is_empty() {
|
||||
Scalar::zero()
|
||||
// if x is zero then we can ignore most of the expensive computation and
|
||||
// just return the last term of the polynomial
|
||||
} else if x.is_zero().into() {
|
||||
// we checked that coefficients are not empty so unwrap here is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
*self.coefficients.first().unwrap()
|
||||
} else {
|
||||
self.coefficients
|
||||
.iter()
|
||||
.enumerate()
|
||||
// coefficient[n] * x ^ n
|
||||
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
|
||||
let x = Scalar::zero();
|
||||
|
||||
points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, point_i)| {
|
||||
let mut numerator = Scalar::one();
|
||||
let mut denominator = Scalar::one();
|
||||
let xi = Scalar::from(*point_i);
|
||||
|
||||
for (j, point_j) in points.iter().enumerate() {
|
||||
if j != i {
|
||||
let xj = Scalar::from(*point_j);
|
||||
|
||||
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
|
||||
numerator *= x - xj;
|
||||
|
||||
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
|
||||
denominator *= xi - xj;
|
||||
}
|
||||
}
|
||||
// numerator / denominator
|
||||
numerator * denominator.invert().unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
|
||||
/// It can be used for Scalars, G1 and G2 points.
|
||||
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
|
||||
points: &[SignerIndex],
|
||||
values: &[T],
|
||||
) -> Result<T>
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
if points.is_empty() || values.is_empty() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if points.len() != values.len() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let coefficients = generate_lagrangian_coefficients_at_origin(points);
|
||||
|
||||
Ok(coefficients
|
||||
.into_iter()
|
||||
.zip(values.iter())
|
||||
.map(|(coeff, val)| val * coeff)
|
||||
.sum())
|
||||
}
|
||||
|
||||
// A temporary way of hashing particular message into G1.
|
||||
// Implementation idea was taken from `threshold_crypto`:
|
||||
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
|
||||
// Eventually it should get replaced by, most likely, the osswu map
|
||||
// method once ideally it's implemented inside the pairing crate.
|
||||
|
||||
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
|
||||
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
|
||||
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
|
||||
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
|
||||
|
||||
pub fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
|
||||
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
|
||||
}
|
||||
|
||||
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
|
||||
let mut output = vec![Scalar::zero()];
|
||||
|
||||
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
|
||||
msg.as_ref(),
|
||||
SCALAR_HASH_DOMAIN,
|
||||
&mut output,
|
||||
);
|
||||
output[0]
|
||||
}
|
||||
|
||||
pub fn try_deserialize_scalar_vec(
|
||||
expected_len: u64,
|
||||
bytes: &[u8],
|
||||
err: CoconutError,
|
||||
) -> Result<Vec<Scalar>> {
|
||||
if bytes.len() != expected_len as usize * 32 {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(expected_len as usize);
|
||||
for i in 0..expected_len as usize {
|
||||
// we just checked we have exactly the amount of bytes we need and thus the unwrap is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
|
||||
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
|
||||
None => return Err(err),
|
||||
Some(scalar) => scalar,
|
||||
};
|
||||
out.push(s)
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
|
||||
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
|
||||
}
|
||||
|
||||
pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result<G1Projective> {
|
||||
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G1Projective::from)
|
||||
}
|
||||
|
||||
pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result<G2Projective> {
|
||||
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G2Projective::from)
|
||||
}
|
||||
|
||||
// use core::fmt;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::de::Visitor;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
//
|
||||
// // #[cfg(feature = "serde")]
|
||||
// #[serde(remote = "Scalar")]
|
||||
// pub(crate) struct ScalarDef(pub Scalar);
|
||||
//
|
||||
// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
//
|
||||
// impl Serialize for ScalarDef {
|
||||
// fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer,
|
||||
// {
|
||||
// use serde::ser::SerializeTuple;
|
||||
// let mut tup = serializer.serialize_tuple(32)?;
|
||||
// for byte in self.0.to_bytes().iter() {
|
||||
// tup.serialize_element(byte)?;
|
||||
// }
|
||||
// tup.end()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl<'de> Deserialize<'de> for ScalarDef {
|
||||
// fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
||||
// where
|
||||
// D: Deserializer<'de>,
|
||||
// {
|
||||
// struct ScalarVisitor;
|
||||
//
|
||||
// impl<'de> Visitor<'de> for ScalarVisitor {
|
||||
// type Value = ScalarDef;
|
||||
//
|
||||
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
// formatter.write_str("a 32-byte canonical bls12_381 scalar")
|
||||
// }
|
||||
//
|
||||
// fn visit_seq<A>(self, mut seq: A) -> core::result::Result<ScalarDef, A::Error>
|
||||
// where
|
||||
// A: serde::de::SeqAccess<'de>,
|
||||
// {
|
||||
// let mut bytes = [0u8; 32];
|
||||
// for i in 0..32 {
|
||||
// bytes[i] = seq
|
||||
// .next_element()?
|
||||
// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?;
|
||||
// }
|
||||
//
|
||||
// let res = Scalar::from_bytes(&bytes);
|
||||
// if res.is_some().into() {
|
||||
// Ok(ScalarDef(res.unwrap()))
|
||||
// } else {
|
||||
// Err(serde::de::Error::custom(
|
||||
// &"scalar was not canonically encoded",
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// deserializer.deserialize_tuple(32, ScalarVisitor)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G1ProjectiveSerdeHelper(Scalar);
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G2ProjectiveSerdeHelper(Scalar);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::RngCore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn polynomial_evaluation() {
|
||||
// y = 42 (it should be 42 regardless of x)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(42)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
|
||||
|
||||
// y = x + 10, at x = 2 (exp: 12)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(10), Scalar::from(1)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
|
||||
|
||||
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![
|
||||
(-Scalar::from(3)),
|
||||
Scalar::from(2),
|
||||
(-Scalar::from(5)),
|
||||
Scalar::zero(),
|
||||
Scalar::from(1),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
|
||||
|
||||
// empty polynomial
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![],
|
||||
};
|
||||
|
||||
// should always be 0
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn performing_lagrangian_scalar_interpolation_at_origin() {
|
||||
// x^2 + 3
|
||||
// x, f(x):
|
||||
// 1, 4,
|
||||
// 2, 7,
|
||||
// 3, 12,
|
||||
let points = vec![1, 2, 3];
|
||||
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(3),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// x^3 + 3x^2 - 5x + 11
|
||||
// x, f(x):
|
||||
// 1, 10
|
||||
// 2, 21
|
||||
// 3, 50
|
||||
// 4, 103
|
||||
let points = vec![1, 2, 3, 4];
|
||||
let values = vec![
|
||||
Scalar::from(10),
|
||||
Scalar::from(21),
|
||||
Scalar::from(50),
|
||||
Scalar::from(103),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(11),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// more points than it is required
|
||||
// x^2 + x + 10
|
||||
// x, f(x)
|
||||
// 1, 12
|
||||
// 2, 16
|
||||
// 3, 22
|
||||
// 4, 30
|
||||
// 5, 40
|
||||
let points = vec![1, 2, 3, 4, 5];
|
||||
let values = vec![
|
||||
Scalar::from(12),
|
||||
Scalar::from(16),
|
||||
Scalar::from(22),
|
||||
Scalar::from(30),
|
||||
Scalar::from(40),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(10),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_g1_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_g1(msg1), hash_g1(msg1));
|
||||
assert_eq!(hash_g1(msg2), hash_g1(msg2));
|
||||
assert_ne!(hash_g1(msg1), hash_g1(msg2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_scalar_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
|
||||
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
|
||||
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
|
||||
}
|
||||
}
|
||||
@@ -48,12 +48,10 @@ features = ["sync"]
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
sphinx = [
|
||||
"nym-crypto/sphinx",
|
||||
"nym-sphinx-params/sphinx",
|
||||
"nym-sphinx-types/sphinx",
|
||||
]
|
||||
outfox = [
|
||||
"nym-crypto/outfox",
|
||||
"nym-sphinx-params/outfox",
|
||||
"nym-sphinx-types/outfox",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::AckKey;
|
||||
use nym_crypto::symmetric::stream_cipher::{self, encrypt, iv_from_slice, random_iv, IvSizeUser};
|
||||
use nym_crypto::symmetric::stream_cipher::{
|
||||
self, encrypt, random_iv, try_iv_from_slice, IvSizeUser,
|
||||
};
|
||||
use nym_sphinx_params::{AckEncryptionAlgorithm, SerializedFragmentIdentifier, FRAG_ID_LEN};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
@@ -25,7 +27,11 @@ pub fn recover_identifier(
|
||||
iv_id_ciphertext: &[u8],
|
||||
) -> Option<SerializedFragmentIdentifier> {
|
||||
let iv_size = AckEncryptionAlgorithm::iv_size();
|
||||
let iv = iv_from_slice::<AckEncryptionAlgorithm>(&iv_id_ciphertext[..iv_size]);
|
||||
if iv_id_ciphertext.len() < FRAG_ID_LEN + iv_size {
|
||||
return None;
|
||||
}
|
||||
|
||||
let iv = try_iv_from_slice::<AckEncryptionAlgorithm>(&iv_id_ciphertext[..iv_size])?;
|
||||
|
||||
let id = stream_cipher::decrypt::<AckEncryptionAlgorithm>(
|
||||
key.inner(),
|
||||
|
||||
@@ -8,7 +8,7 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric", "sphinx"] } # all addresses are expressed in terms on their crypto keys
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx"] } # we need to be able to refer to some types defined inside sphinx crate
|
||||
serde = { workspace = true } # implementing serialization/deserialization for some types, like `Recipient`
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -19,6 +19,9 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ReplySurbError {
|
||||
#[error("did not receive enough data to recover a reply SURB")]
|
||||
TooShort,
|
||||
|
||||
#[error("tried to use reply SURB with an unpadded message")]
|
||||
UnpaddedMessageError,
|
||||
|
||||
@@ -131,7 +134,9 @@ impl ReplySurb {
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ReplySurbError> {
|
||||
// TODO: introduce bound checks to guard us against out of bound reads
|
||||
if bytes.len() <= SurbEncryptionKeySize::USIZE {
|
||||
return Err(ReplySurbError::TooShort);
|
||||
}
|
||||
|
||||
let encryption_key =
|
||||
SurbEncryptionKey::try_from_bytes(&bytes[..SurbEncryptionKeySize::USIZE])?;
|
||||
|
||||
@@ -559,7 +559,7 @@ mod tests {
|
||||
let mut address_bytes = [0; NODE_ADDRESS_LENGTH];
|
||||
rng.fill_bytes(&mut address_bytes);
|
||||
|
||||
let dummy_private = PrivateKey::new_with_rng(rng);
|
||||
let dummy_private = PrivateKey::random_from_rng(rng);
|
||||
let pub_key = (&dummy_private).into();
|
||||
Node {
|
||||
address: NodeAddressBytes::from_bytes(address_bytes),
|
||||
|
||||
@@ -130,28 +130,33 @@ impl Decoder for NymCodec {
|
||||
mod packet_encoding {
|
||||
use super::*;
|
||||
use nym_sphinx_types::{
|
||||
crypto, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
|
||||
DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
|
||||
PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
};
|
||||
|
||||
fn random_pubkey() -> nym_sphinx_types::PublicKey {
|
||||
let private_key = PrivateKey::random();
|
||||
(&private_key).into()
|
||||
}
|
||||
|
||||
fn make_valid_outfox_packet(size: PacketSize) -> NymPacket {
|
||||
let (_, node1_pk) = crypto::keygen();
|
||||
let node1_pk = random_pubkey();
|
||||
let node1 = Node::new(
|
||||
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
|
||||
node1_pk,
|
||||
);
|
||||
let (_, node2_pk) = crypto::keygen();
|
||||
let node2_pk = random_pubkey();
|
||||
let node2 = Node::new(
|
||||
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
|
||||
node2_pk,
|
||||
);
|
||||
let (_, node3_pk) = crypto::keygen();
|
||||
let node3_pk = random_pubkey();
|
||||
let node3 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node3_pk,
|
||||
);
|
||||
|
||||
let (_, node4_pk) = crypto::keygen();
|
||||
let node4_pk = random_pubkey();
|
||||
let node4 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node4_pk,
|
||||
@@ -170,17 +175,17 @@ mod packet_encoding {
|
||||
}
|
||||
|
||||
fn make_valid_sphinx_packet(size: PacketSize) -> NymPacket {
|
||||
let (_, node1_pk) = crypto::keygen();
|
||||
let node1_pk = random_pubkey();
|
||||
let node1 = Node::new(
|
||||
NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]),
|
||||
node1_pk,
|
||||
);
|
||||
let (_, node2_pk) = crypto::keygen();
|
||||
let node2_pk = random_pubkey();
|
||||
let node2 = Node::new(
|
||||
NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]),
|
||||
node2_pk,
|
||||
);
|
||||
let (_, node3_pk) = crypto::keygen();
|
||||
let node3_pk = random_pubkey();
|
||||
let node3 = Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
node3_pk,
|
||||
|
||||
@@ -4,8 +4,10 @@ use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressE
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError,
|
||||
Version as SphinxPacketVersion,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
@@ -13,12 +15,38 @@ use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResult {
|
||||
pub enum MixProcessingResultData {
|
||||
/// Contains unwrapped data that should first get delayed before being sent to next hop.
|
||||
ForwardHop(MixPacket, Option<SphinxDelay>),
|
||||
ForwardHop {
|
||||
packet: MixPacket,
|
||||
delay: Option<SphinxDelay>,
|
||||
},
|
||||
|
||||
/// Contains all data extracted out of the final hop packet that could be forwarded to the destination.
|
||||
FinalHop(ProcessedFinalHop),
|
||||
FinalHop { final_hop_data: ProcessedFinalHop },
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum MixPacketVersion {
|
||||
Outfox,
|
||||
Sphinx(SphinxPacketVersion),
|
||||
}
|
||||
|
||||
impl Display for MixPacketVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
MixPacketVersion::Outfox => "outfox".fmt(f),
|
||||
MixPacketVersion::Sphinx(sphinx_version) => {
|
||||
write!(f, "sphinx-{}", sphinx_version.value())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MixProcessingResult {
|
||||
pub packet_version: MixPacketVersion,
|
||||
pub processing_data: MixProcessingResultData,
|
||||
}
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
@@ -107,37 +135,63 @@ fn perform_final_processing(
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
match packet {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay) => {
|
||||
process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type)
|
||||
}
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop(
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
process_final_hop(
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let mix_packet = MixPacket::new(
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, None))
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,14 +202,16 @@ fn process_final_hop(
|
||||
payload: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?;
|
||||
|
||||
Ok(MixProcessingResult::FinalHop(ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
}))
|
||||
Ok(MixProcessingResultData::FinalHop {
|
||||
final_hop_data: ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn split_into_ack_and_message(
|
||||
@@ -211,11 +267,14 @@ fn process_forward_hop(
|
||||
forward_address: NodeAddressBytes,
|
||||
delay: SphinxDelay,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
|
||||
|
||||
let mix_packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay)))
|
||||
let packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: Some(delay),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: what more could we realistically test here?
|
||||
|
||||
@@ -16,5 +16,5 @@ nym-sphinx-types = { path = "../types" }
|
||||
|
||||
[features]
|
||||
default = ["sphinx"]
|
||||
sphinx = ["nym-crypto/sphinx", "nym-sphinx-types/outfox"]
|
||||
outfox = ["nym-crypto/outfox", "nym-sphinx-types/outfox"]
|
||||
sphinx = ["nym-sphinx-types/outfox"]
|
||||
outfox = ["nym-sphinx-types/outfox"]
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
pub use nym_outfox::{
|
||||
constants::MIN_PACKET_SIZE, constants::MIX_PARAMS_LEN, constants::OUTFOX_PACKET_OVERHEAD,
|
||||
error::OutfoxError,
|
||||
};
|
||||
// re-exporting types and constants available in sphinx
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub use sphinx_packet::{
|
||||
constants::{
|
||||
@@ -21,12 +29,10 @@ pub use sphinx_packet::{
|
||||
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURBMaterial, SURB},
|
||||
Error as SphinxError, ProcessedPacket,
|
||||
version::Version,
|
||||
version::UPDATED_LEGACY_VERSION,
|
||||
Error as SphinxError, ProcessedPacket, ProcessedPacketData,
|
||||
};
|
||||
#[cfg(feature = "sphinx")]
|
||||
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NymPacketError {
|
||||
@@ -85,8 +91,12 @@ impl NymPacket {
|
||||
destination: &Destination,
|
||||
delays: &[Delay],
|
||||
) -> Result<NymPacket, NymPacketError> {
|
||||
// FIXME:
|
||||
// for now explicitly use the legacy version until sufficient number of nodes
|
||||
// understand both variants
|
||||
Ok(NymPacket::Sphinx(
|
||||
SphinxPacketBuilder::new()
|
||||
.with_version(UPDATED_LEGACY_VERSION)
|
||||
.with_payload_size(size)
|
||||
.build_packet(message, route, destination, delays)?,
|
||||
))
|
||||
|
||||
@@ -12,3 +12,7 @@ readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
bincode.workspace = true
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProtocolError {
|
||||
#[error("invalid service provider type: {0}")]
|
||||
InvalidServiceProviderType(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum ServiceProviderType {
|
||||
@@ -11,8 +19,103 @@ pub enum ServiceProviderType {
|
||||
Authenticator = 2,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServiceProviderType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NetworkRequester => write!(f, "NetworkRequester"),
|
||||
Self::IpPacketRouter => write!(f, "IpPacketRouter"),
|
||||
Self::Authenticator => write!(f, "Authenticator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ServiceProviderType {
|
||||
type Error = ProtocolError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::NetworkRequester),
|
||||
1 => Ok(Self::IpPacketRouter),
|
||||
2 => Ok(Self::Authenticator),
|
||||
_ => Err(ProtocolError::InvalidServiceProviderType(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Protocol {
|
||||
pub version: u8,
|
||||
pub service_provider_type: ServiceProviderType,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8; 2]> for Protocol {
|
||||
type Error = ProtocolError;
|
||||
|
||||
fn try_from(bytes: &[u8; 2]) -> Result<Self, Self::Error> {
|
||||
let version = bytes[0];
|
||||
let service_provider_type = ServiceProviderType::try_from(bytes[1])
|
||||
.map_err(|_| ProtocolError::InvalidServiceProviderType(bytes[1]))?;
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
service_provider_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bincode::Options;
|
||||
|
||||
fn make_bincode_serializer() -> impl bincode::Options {
|
||||
bincode::DefaultOptions::new()
|
||||
.with_big_endian()
|
||||
.with_varint_encoding()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_serialization() {
|
||||
let protocol = Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::NetworkRequester,
|
||||
};
|
||||
|
||||
let serialized = make_bincode_serializer().serialize(&protocol).unwrap();
|
||||
let deserialized: Protocol = make_bincode_serializer().deserialize(&serialized).unwrap();
|
||||
|
||||
assert_eq!(protocol, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_serialization() {
|
||||
let protocol = Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::NetworkRequester,
|
||||
};
|
||||
|
||||
let serialized = make_bincode_serializer().serialize(&protocol).unwrap();
|
||||
assert_eq!(serialized.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_deserialization() {
|
||||
let bytes = [4, ServiceProviderType::NetworkRequester as u8];
|
||||
let deserialized = Protocol::try_from(&bytes).unwrap();
|
||||
|
||||
let expected = Protocol {
|
||||
version: 4,
|
||||
service_provider_type: ServiceProviderType::NetworkRequester,
|
||||
};
|
||||
|
||||
assert_eq!(expected, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_protocol_deserialization() {
|
||||
let bytes = [4, 3];
|
||||
let deserialized = Protocol::try_from(&bytes);
|
||||
|
||||
assert!(deserialized.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user