Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c45e8da43d | |||
| 12cc49a734 | |||
| 7e56a9e88c | |||
| 9790009eac | |||
| 379d593daf | |||
| ce75b99b6f | |||
| bcb7c41fd7 | |||
| bb091ce47f | |||
| b28ff17c30 | |||
| 9b14e00653 | |||
| ec8b5e6e9d | |||
| effed4d7d6 | |||
| d4584c305a | |||
| afc53d4379 | |||
| 4278e88d3c | |||
| e12a34ce6b | |||
| 1de64f7b52 | |||
| 66dbe09e66 | |||
| dcce269921 | |||
| c043f0096a | |||
| a7cd7a58f2 | |||
| fe6da046dc | |||
| 8bbdb94b13 | |||
| e32601ab86 | |||
| 161138bdff | |||
| 0529e84a31 | |||
| 95f98016de | |||
| 4967bbb5bd | |||
| 2952144d32 | |||
| 80c21b3ed9 | |||
| 1f0d5f8ad0 | |||
| 49ce56c367 | |||
| 4ab6f4c3a9 | |||
| 3727370b9e | |||
| b3272097f9 | |||
| ebc13c4327 | |||
| ec3a6b3e27 | |||
| 19f3c76f72 | |||
| 90cc239999 | |||
| c1bd5db902 | |||
| fb1649bab5 | |||
| b21ca41e16 | |||
| 8656abcbde | |||
| 99b30c2570 | |||
| 2c5d31e685 | |||
| 3ae9ea5de6 | |||
| cf65bc1295 | |||
| 8bcec241a2 | |||
| 306e9b9dc2 | |||
| 2d5f851252 | |||
| d36e349cc6 | |||
| 4990a4745f | |||
| 5ce087dafe | |||
| caf03a09c8 | |||
| 0d399f7d70 | |||
| 56cf181770 | |||
| f0aa2feb76 | |||
| 4df927cc3d | |||
| 5db47b8931 | |||
| 27c1b29615 | |||
| c80c8ef899 | |||
| 3f4373eb98 | |||
| cf10bb12ef | |||
| cb1e93e58d | |||
| d0cd22c4da | |||
| a721e97c06 | |||
| f4f98027a0 | |||
| dee27e805d | |||
| 6f7dc36e5c | |||
| ef50f361ba | |||
| 3c55b28e69 | |||
| f1624e658e | |||
| fc44f2fe1c | |||
| cc26e4043c | |||
| bb242080cf | |||
| 3ebaf48aa3 | |||
| 2d7a55daba | |||
| 5f36742ce6 | |||
| 8547e770da | |||
| 862178c9c5 | |||
| e0dd9b533e | |||
| 5ab3f95b8f | |||
| 46097c80fe | |||
| ab0eb35906 | |||
| 8bb3b066ba | |||
| 2a04234c26 | |||
| ef8f6ed07b | |||
| d480ddb133 | |||
| b119820591 | |||
| e128949dc2 | |||
| 9499b987e5 | |||
| d6ac786295 | |||
| 4d09d9c3db | |||
| 8c9044adf3 | |||
| 472085ca52 | |||
| 2f089e80ff |
@@ -3,3 +3,21 @@
|
||||
|
||||
RUST_LOG=info
|
||||
RUST_BACKTRACE=1
|
||||
|
||||
#########################################
|
||||
# geoipupdate (needed for explorer-api) #
|
||||
#########################################
|
||||
# MaxMind account ID (change it to a valid account ID)
|
||||
GEOIPUPDATE_ACCOUNT_ID=xxx
|
||||
# MaxMind license key (change it to a valid license key)
|
||||
GEOIPUPDATE_LICENSE_KEY=xxx
|
||||
# List of space-separated database edition IDs. Edition IDs may
|
||||
# consist of letters, digits, and dashes. For example, GeoIP2-City
|
||||
# would download the GeoIP2 City database (GeoIP2-City).
|
||||
GEOIPUPDATE_EDITION_IDS=GeoLite2-Country
|
||||
# The number of hours between geoipupdate runs. If this is not set
|
||||
# or is set to 0, geoipupdate will run once and exit.
|
||||
GEOIPUPDATE_FREQUENCY=72
|
||||
# The path to the directory where geoipupdate will download the
|
||||
# database.
|
||||
GEOIP_DB_DIRECTORY=./explorer-api/geo_ip
|
||||
|
||||
+27
-14
@@ -1,36 +1,49 @@
|
||||
name: Daily security audit
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
schedule:
|
||||
- cron: '5 9 * * *'
|
||||
jobs:
|
||||
security_audit:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/audit-check@v1
|
||||
- name: Checkout repository code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
toolchain: stable
|
||||
- name: Install cargo deny
|
||||
run: cargo install --locked cargo-deny
|
||||
- name: Run cargo deny
|
||||
run: cargo deny check advisories --hide-inclusion-graph &> .github/workflows/support-files/notifications/deny.message
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
notification:
|
||||
if: ${{ failure() }}
|
||||
needs: security_audit
|
||||
needs: cargo-deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
- name: Download report from previous job
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications
|
||||
- name: Keybase - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Keybase - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "Nym daily audit"
|
||||
NYM_NOTIFICATION_KIND: security
|
||||
NYM_PROJECT_NAME: "Daily security report"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
|
||||
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
|
||||
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBTECH_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "test"
|
||||
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "security"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -3,7 +3,7 @@ require('dotenv').config();
|
||||
const Bot = require('keybase-bot');
|
||||
|
||||
let context = {
|
||||
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect'],
|
||||
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security'],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const Handlebars = require('handlebars');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { Octokit, App } = require('octokit');
|
||||
|
||||
async function addToContextAndValidate(context) {
|
||||
return
|
||||
}
|
||||
|
||||
async function getMessageBody(context) {
|
||||
try {
|
||||
const source = fs
|
||||
.readFileSync("deny.message").toString();
|
||||
return source;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addToContextAndValidate,
|
||||
getMessageBody,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Tests for validator API
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "validator-api/tests/**"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: validator-api/tests
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: validator api tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Node v18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.1.0
|
||||
|
||||
- name: Install yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Run yarn
|
||||
run: yarn
|
||||
|
||||
- name: Launch tests
|
||||
run: yarn test
|
||||
working-directory: validator-api/tests
|
||||
@@ -15,10 +15,15 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- native-client/socks5-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671])
|
||||
- wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673])
|
||||
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint
|
||||
- native-client/socks5-client/network-requester: improve handling error cases ([#1713])
|
||||
- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM
|
||||
- clients: add testing-only support for two more extended packet sizes (8kb and 16kb).
|
||||
|
||||
### Fixed
|
||||
|
||||
- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
|
||||
- validator-api should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721])
|
||||
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724])
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -26,6 +31,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
|
||||
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
|
||||
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
|
||||
- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681])
|
||||
- moved `Percent` struct to to `contracts-common`, change affects explorer-api
|
||||
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725])
|
||||
|
||||
[#1541]: https://github.com/nymtech/nym/pull/1541
|
||||
[#1558]: https://github.com/nymtech/nym/pull/1558
|
||||
@@ -39,6 +47,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
[#1669]: https://github.com/nymtech/nym/pull/1669
|
||||
[#1671]: https://github.com/nymtech/nym/pull/1671
|
||||
[#1673]: https://github.com/nymtech/nym/pull/1673
|
||||
[#1681]: https://github.com/nymtech/nym/pull/1681
|
||||
[#1687]: https://github.com/nymtech/nym/pull/1687
|
||||
[#1702]: https://github.com/nymtech/nym/pull/1702
|
||||
[#1703]: https://github.com/nymtech/nym/pull/1703
|
||||
[#1713]: https://github.com/nymtech/nym/pull/1713
|
||||
[#1721]: https://github.com/nymtech/nym/pull/1721
|
||||
[#1724]: https://github.com/nymtech/nym/pull/1724
|
||||
[#1725]: https://github.com/nymtech/nym/pull/1725
|
||||
|
||||
|
||||
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
|
||||
|
||||
Generated
+30
@@ -737,7 +737,10 @@ name = "contracts-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1583,10 +1586,13 @@ version = "1.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 3.2.8",
|
||||
"contracts-common",
|
||||
"dotenv",
|
||||
"humantime-serde",
|
||||
"isocountry",
|
||||
"itertools",
|
||||
"log",
|
||||
"logging",
|
||||
"maxminddb",
|
||||
"mixnet-contract-common",
|
||||
"network-defaults",
|
||||
@@ -2725,6 +2731,14 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logging"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.4"
|
||||
@@ -3067,6 +3081,7 @@ dependencies = [
|
||||
"clap_complete_fig",
|
||||
"dotenv",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nym-cli-commands",
|
||||
"pretty_env_logger",
|
||||
@@ -3122,6 +3137,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"pemstore",
|
||||
@@ -3131,6 +3147,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sled",
|
||||
"task",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
"topology",
|
||||
@@ -3163,6 +3180,7 @@ dependencies = [
|
||||
"gateway-requests",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-client",
|
||||
"mixnode-common",
|
||||
"network-defaults",
|
||||
@@ -3205,6 +3223,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-client",
|
||||
"mixnode-common",
|
||||
"nonexhaustive-delayqueue",
|
||||
@@ -3239,6 +3258,7 @@ dependencies = [
|
||||
"futures",
|
||||
"ipnetwork 0.20.0",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
@@ -3264,6 +3284,7 @@ version = "1.0.2"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"log",
|
||||
"logging",
|
||||
"pretty_env_logger",
|
||||
"rocket",
|
||||
"serde",
|
||||
@@ -3290,6 +3311,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
@@ -3302,6 +3324,7 @@ dependencies = [
|
||||
"snafu 0.6.10",
|
||||
"socks5-requests",
|
||||
"task",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"topology",
|
||||
"url",
|
||||
@@ -3348,6 +3371,7 @@ dependencies = [
|
||||
"coconut-interface",
|
||||
"config",
|
||||
"console-subscriber",
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"credential-storage",
|
||||
"credentials",
|
||||
@@ -3362,6 +3386,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"inclusion-probability",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-contract-common",
|
||||
"multisig-contract-common",
|
||||
"nymcoconut",
|
||||
@@ -6385,6 +6410,7 @@ dependencies = [
|
||||
"coconut-interface",
|
||||
"colored",
|
||||
"config",
|
||||
"contracts-common",
|
||||
"cosmrs",
|
||||
"cosmwasm-std",
|
||||
"cw3",
|
||||
@@ -6473,12 +6499,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
name = "vesting-contract"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vesting-contract-common",
|
||||
]
|
||||
|
||||
@@ -6486,7 +6514,9 @@ dependencies = [
|
||||
name = "vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -41,6 +41,7 @@ members = [
|
||||
"common/execute",
|
||||
"common/inclusion-probability",
|
||||
"common/ledger",
|
||||
"common/logging",
|
||||
"common/mixnode-common",
|
||||
"common/network-defaults",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
|
||||
@@ -2,12 +2,12 @@ test: clippy-all cargo-test wasm fmt
|
||||
test-all: test cargo-test-expensive
|
||||
no-clippy: build cargo-test wasm fmt
|
||||
happy: fmt clippy-happy test
|
||||
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect
|
||||
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-wasm-client
|
||||
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect
|
||||
cargo-test: test-main test-contracts test-wallet test-connect test-coconut
|
||||
cargo-test: test-main test-contracts test-wallet test-connect test-coconut test-wasm-client
|
||||
cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive test-coconut-expensive
|
||||
build: build-contracts build-wallet build-main build-connect
|
||||
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect
|
||||
build: build-contracts build-wallet build-main build-connect build-wasm-client
|
||||
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client
|
||||
|
||||
clippy-happy-main:
|
||||
cargo clippy
|
||||
@@ -40,6 +40,9 @@ clippy-all-wallet:
|
||||
clippy-all-connect:
|
||||
cargo clippy --workspace --manifest-path nym-connect/Cargo.toml --all-features -- -D warnings
|
||||
|
||||
clippy-all-wasm-client:
|
||||
cargo clippy --workspace --manifest-path clients/webassembly/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
test-main:
|
||||
cargo test --workspace
|
||||
|
||||
@@ -68,6 +71,9 @@ test-wallet:
|
||||
test-wallet-expensive:
|
||||
cargo test --manifest-path nym-wallet/Cargo.toml --all-features -- --ignored
|
||||
|
||||
test-wasm-client:
|
||||
cargo test --workspace --manifest-path clients/webassembly/Cargo.toml --all-features
|
||||
|
||||
test-connect:
|
||||
cargo test --manifest-path nym-connect/Cargo.toml --all-features
|
||||
|
||||
@@ -86,6 +92,9 @@ build-wallet:
|
||||
build-connect:
|
||||
cargo build --manifest-path nym-connect/Cargo.toml --workspace
|
||||
|
||||
build-wasm-client:
|
||||
cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown
|
||||
|
||||
build-nym-cli:
|
||||
cargo build --release --manifest-path tools/nym-cli/Cargo.toml
|
||||
|
||||
@@ -101,6 +110,9 @@ fmt-wallet:
|
||||
fmt-connect:
|
||||
cargo fmt --manifest-path nym-connect/Cargo.toml --all
|
||||
|
||||
fmt-wasm-client:
|
||||
cargo fmt --manifest-path clients/webassembly/Cargo.toml --all
|
||||
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
|
||||
|
||||
@@ -110,3 +122,6 @@ mixnet-opt: wasm
|
||||
generate-typescript:
|
||||
cd tools/ts-rs-cli && cargo run && cd ../..
|
||||
yarn types:lint:fix
|
||||
|
||||
run-validator-tests:
|
||||
cd validator-api/tests/functional_test && yarn test
|
||||
@@ -16,6 +16,7 @@ use rand::{rngs::OsRng, CryptoRng, Rng};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time;
|
||||
@@ -171,11 +172,18 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
)
|
||||
.expect("Somehow failed to generate a loop cover message with a valid topology");
|
||||
|
||||
// if this one fails, there's no retrying because it means that either:
|
||||
// - we run out of memory
|
||||
// - the receiver channel is closed
|
||||
// in either case there's no recovery and we can only panic
|
||||
self.mix_tx.unbounded_send(vec![cover_message]).unwrap();
|
||||
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
|
||||
match err {
|
||||
TrySendError::Full(_) => {
|
||||
// This isn't a problem, if the channel is full means we're already sending the
|
||||
// max amount of messages downstream can handle.
|
||||
log::debug!("Failed to send cover message - channel full");
|
||||
}
|
||||
TrySendError::Closed(_) => {
|
||||
log::warn!("Failed to send cover message - channel closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: I'm not entirely sure whether this is really required, because I'm not 100%
|
||||
// sure how `yield_now()` works - whether it just notifies the scheduler or whether it
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::spawn_future;
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use gateway_client::GatewayClient;
|
||||
use log::*;
|
||||
use nymsphinx::forwarding::packet::MixPacket;
|
||||
|
||||
pub type BatchMixMessageSender = mpsc::UnboundedSender<Vec<MixPacket>>;
|
||||
pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver<Vec<MixPacket>>;
|
||||
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
|
||||
pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>;
|
||||
|
||||
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
|
||||
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
|
||||
const MAX_FAILURE_COUNT: usize = 100;
|
||||
|
||||
pub struct MixTrafficController {
|
||||
@@ -25,15 +25,17 @@ pub struct MixTrafficController {
|
||||
}
|
||||
|
||||
impl MixTrafficController {
|
||||
pub fn new(
|
||||
mix_rx: BatchMixMessageReceiver,
|
||||
gateway_client: GatewayClient,
|
||||
) -> MixTrafficController {
|
||||
MixTrafficController {
|
||||
gateway_client,
|
||||
mix_rx,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
}
|
||||
pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) {
|
||||
let (sphinx_message_sender, sphinx_message_receiver) =
|
||||
tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE);
|
||||
(
|
||||
MixTrafficController {
|
||||
gateway_client,
|
||||
mix_rx: sphinx_message_receiver,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
},
|
||||
sphinx_message_sender,
|
||||
)
|
||||
}
|
||||
|
||||
async fn on_messages(&mut self, mut mix_packets: Vec<MixPacket>) {
|
||||
@@ -72,7 +74,7 @@ impl MixTrafficController {
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
mix_packets = self.mix_rx.next() => match mix_packets {
|
||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
Some(mix_packets) => {
|
||||
self.on_messages(mix_packets).await;
|
||||
},
|
||||
@@ -96,7 +98,7 @@ impl MixTrafficController {
|
||||
spawn_future(async move {
|
||||
debug!("Started MixTrafficController without graceful shutdown support");
|
||||
|
||||
while let Some(mix_packets) = self.mix_rx.next().await {
|
||||
while let Some(mix_packets) = self.mix_rx.recv().await {
|
||||
self.on_messages(mix_packets).await;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,6 +27,23 @@ use tokio::time;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_timer;
|
||||
|
||||
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
|
||||
// the available buffer space we want to take somewhat swift action, but we still need to give a
|
||||
// short time to give the channel a chance reduce pressure.
|
||||
const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1;
|
||||
// The minimum time between decreasing the average delay between packets. We don't want to change
|
||||
// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to
|
||||
// wait a little to see the effect before we decrease further.
|
||||
const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30;
|
||||
// If we enough time passes without any sign of backpressure in the channel, we can consider
|
||||
// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out
|
||||
// bandwidth at all times.
|
||||
const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30;
|
||||
// The maximum multiplier we apply to the base average Poisson delay.
|
||||
const MAX_DELAY_MULTIPLIER: u32 = 6;
|
||||
// The minium multiplier we apply to the base average Poisson delay.
|
||||
const MIN_DELAY_MULTIPLIER: u32 = 1;
|
||||
|
||||
/// Configurable parameters of the `OutQueueControl`
|
||||
pub(crate) struct Config {
|
||||
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
|
||||
@@ -68,6 +85,101 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
struct SendingDelayController {
|
||||
/// Multiply the average sending delay.
|
||||
/// This is normally set to unity, but if we detect backpressure we increase this
|
||||
/// multiplier. We use discrete steps.
|
||||
current_multiplier: u32,
|
||||
|
||||
/// Maximum delay multiplier
|
||||
upper_bound: u32,
|
||||
|
||||
/// Minimum delay multiplier
|
||||
lower_bound: u32,
|
||||
|
||||
/// To make sure we don't change the multiplier to fast, we limit a change to some duration
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
time_when_changed: time::Instant,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
time_when_changed: wasm_timer::Instant,
|
||||
|
||||
/// If we have a long enough time without any backpressure detected we try reducing the sending
|
||||
/// delay multiplier
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
time_when_backpressure_detected: time::Instant,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
time_when_backpressure_detected: wasm_timer::Instant,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_time_now() -> time::Instant {
|
||||
time::Instant::now()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn get_time_now() -> wasm_timer::Instant {
|
||||
wasm_timer::Instant::now()
|
||||
}
|
||||
|
||||
impl SendingDelayController {
|
||||
fn new(lower_bound: u32, upper_bound: u32) -> Self {
|
||||
assert!(lower_bound <= upper_bound);
|
||||
let now = get_time_now();
|
||||
SendingDelayController {
|
||||
current_multiplier: MIN_DELAY_MULTIPLIER,
|
||||
upper_bound,
|
||||
lower_bound,
|
||||
time_when_changed: now,
|
||||
time_when_backpressure_detected: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_multiplier(&self) -> u32 {
|
||||
self.current_multiplier
|
||||
}
|
||||
|
||||
fn increase_delay_multiplier(&mut self) {
|
||||
self.current_multiplier =
|
||||
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
|
||||
self.time_when_changed = get_time_now();
|
||||
log::debug!(
|
||||
"Increasing sending delay multiplier to: {}",
|
||||
self.current_multiplier
|
||||
);
|
||||
}
|
||||
|
||||
fn decrease_delay_multiplier(&mut self) {
|
||||
self.current_multiplier =
|
||||
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
|
||||
self.time_when_changed = get_time_now();
|
||||
log::debug!(
|
||||
"Decreasing sending delay multiplier to: {}",
|
||||
self.current_multiplier
|
||||
);
|
||||
}
|
||||
|
||||
fn record_backpressure_detected(&mut self) {
|
||||
self.time_when_backpressure_detected = get_time_now();
|
||||
}
|
||||
|
||||
fn not_increased_delay_recently(&self) -> bool {
|
||||
get_time_now()
|
||||
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
|
||||
}
|
||||
|
||||
fn is_sending_reliable(&self) -> bool {
|
||||
let now = get_time_now();
|
||||
let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS);
|
||||
let acceptable_time_without_backpressure =
|
||||
Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS);
|
||||
|
||||
now > self.time_when_backpressure_detected + acceptable_time_without_backpressure
|
||||
&& now > self.time_when_changed + delay_change_interval
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct OutQueueControl<R>
|
||||
where
|
||||
R: CryptoRng + Rng,
|
||||
@@ -89,6 +201,10 @@ where
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
next_delay: Option<Pin<Box<wasm_timer::Delay>>>,
|
||||
|
||||
// To make sure we don't overload the mix_tx channel, we limit the rate we are pushing
|
||||
// messages.
|
||||
sending_rate_controller: SendingDelayController,
|
||||
|
||||
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
|
||||
/// out to the network without any further delays.
|
||||
mix_tx: BatchMixMessageSender,
|
||||
@@ -156,6 +272,10 @@ where
|
||||
ack_key,
|
||||
sent_notifier,
|
||||
next_delay: None,
|
||||
sending_rate_controller: SendingDelayController::new(
|
||||
MIN_DELAY_MULTIPLIER,
|
||||
MAX_DELAY_MULTIPLIER,
|
||||
),
|
||||
mix_tx,
|
||||
real_receiver,
|
||||
our_full_destination,
|
||||
@@ -176,7 +296,7 @@ where
|
||||
async fn on_message(&mut self, next_message: StreamMessage) {
|
||||
trace!("created new message");
|
||||
|
||||
let next_message = match next_message {
|
||||
let (next_message, fragment_id) = match next_message {
|
||||
StreamMessage::Cover => {
|
||||
// TODO for way down the line: in very rare cases (during topology update) we might have
|
||||
// to wait a really tiny bit before actually obtaining the permit hence messing with our
|
||||
@@ -195,32 +315,35 @@ where
|
||||
}
|
||||
let topology_ref = topology_ref_option.unwrap();
|
||||
|
||||
generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
topology_ref,
|
||||
&self.ack_key,
|
||||
&self.our_full_destination,
|
||||
self.config.average_ack_delay,
|
||||
self.config.average_packet_delay,
|
||||
self.config.cover_packet_size,
|
||||
(
|
||||
generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
topology_ref,
|
||||
&self.ack_key,
|
||||
&self.our_full_destination,
|
||||
self.config.average_ack_delay,
|
||||
self.config.average_packet_delay,
|
||||
self.config.cover_packet_size,
|
||||
)
|
||||
.expect(
|
||||
"Somehow failed to generate a loop cover message with a valid topology",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.expect("Somehow failed to generate a loop cover message with a valid topology")
|
||||
}
|
||||
StreamMessage::Real(real_message) => {
|
||||
self.sent_notify(real_message.fragment_id);
|
||||
real_message.mix_packet
|
||||
(real_message.mix_packet, Some(real_message.fragment_id))
|
||||
}
|
||||
};
|
||||
|
||||
// if this one fails, there's no retrying because it means that either:
|
||||
// - we run out of memory
|
||||
// - the receiver channel is closed
|
||||
// in either case there's no recovery and we can only panic
|
||||
if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) {
|
||||
log::warn!(
|
||||
"Failed to send {} packets (possible process shutdown?)",
|
||||
err.into_inner().len()
|
||||
);
|
||||
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
|
||||
log::error!("Failed to send - channel closed: {}", err);
|
||||
}
|
||||
|
||||
// notify ack controller about sending our message only after we actually managed to push it
|
||||
// through the channel
|
||||
if let Some(fragment_id) = fragment_id {
|
||||
self.sent_notify(fragment_id);
|
||||
}
|
||||
|
||||
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
|
||||
@@ -234,7 +357,44 @@ where
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
fn current_average_message_sending_delay(&self) -> Duration {
|
||||
self.config.average_message_sending_delay
|
||||
* self.sending_rate_controller.current_multiplier()
|
||||
}
|
||||
|
||||
fn adjust_current_average_message_sending_delay(&mut self) {
|
||||
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
|
||||
log::trace!(
|
||||
"used_slots: {used_slots}, current_multiplier: {}",
|
||||
self.sending_rate_controller.current_multiplier()
|
||||
);
|
||||
|
||||
// Even just a single used slot is enough to signal backpressure
|
||||
if used_slots > 0 {
|
||||
log::trace!("Backpressure detected");
|
||||
self.sending_rate_controller.record_backpressure_detected();
|
||||
}
|
||||
|
||||
// If the buffer is running out, slow down the sending rate
|
||||
if self.mix_tx.capacity() == 0
|
||||
&& self.sending_rate_controller.not_increased_delay_recently()
|
||||
{
|
||||
self.sending_rate_controller.increase_delay_multiplier();
|
||||
}
|
||||
|
||||
// Very carefully step up the sending rate in case it seems like we can solidly handle the
|
||||
// current rate.
|
||||
if self.sending_rate_controller.is_sending_reliable() {
|
||||
self.sending_rate_controller.decrease_delay_multiplier();
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
|
||||
// The average delay could change depending on if backpressure in the downstream channel
|
||||
// (mix_tx) was detected.
|
||||
self.adjust_current_average_message_sending_delay();
|
||||
let avg_delay = self.current_average_message_sending_delay();
|
||||
|
||||
if let Some(ref mut next_delay) = &mut self.next_delay {
|
||||
// it is not yet time to return a message
|
||||
if next_delay.as_mut().poll(cx).is_pending() {
|
||||
@@ -243,7 +403,6 @@ where
|
||||
|
||||
// we know it's time to send a message, so let's prepare delay for the next one
|
||||
// Get the `now` by looking at the current `delay` deadline
|
||||
let avg_delay = self.config.average_message_sending_delay;
|
||||
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
|
||||
|
||||
// The next interval value is `next_poisson_delay` after the one that just
|
||||
|
||||
@@ -57,24 +57,15 @@ impl<'a> TopologyReadPermit<'a> {
|
||||
) -> Option<&'a NymTopology> {
|
||||
// Note: implicit deref with Deref for TopologyReadPermit is happening here
|
||||
let topology_ref_option = self.permit.as_ref();
|
||||
match topology_ref_option {
|
||||
None => None,
|
||||
Some(topology_ref) => {
|
||||
// see if it's possible to route the packet to both gateways
|
||||
if !topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS)
|
||||
|| !topology_ref.gateway_exists(ack_recipient.gateway())
|
||||
|| if let Some(packet_recipient) = packet_recipient {
|
||||
!topology_ref.gateway_exists(packet_recipient.gateway())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
{
|
||||
None
|
||||
topology_ref_option.as_ref().filter(|topology_ref| {
|
||||
!(!topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS)
|
||||
|| !topology_ref.gateway_exists(ack_recipient.gateway())
|
||||
|| if let Some(packet_recipient) = packet_recipient {
|
||||
!topology_ref.gateway_exists(packet_recipient.gateway())
|
||||
} else {
|
||||
Some(topology_ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use config::NymConfig;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
@@ -247,8 +248,8 @@ impl<T: NymConfig> Config<T> {
|
||||
self.debug.disable_main_poisson_packet_distribution
|
||||
}
|
||||
|
||||
pub fn get_use_extended_packet_size(&self) -> bool {
|
||||
self.debug.use_extended_packet_size
|
||||
pub fn get_use_extended_packet_size(&self) -> Option<ExtendedPacketSize> {
|
||||
self.debug.use_extended_packet_size.clone()
|
||||
}
|
||||
|
||||
pub fn get_version(&self) -> &str {
|
||||
@@ -470,8 +471,16 @@ pub struct Debug {
|
||||
/// poisson distribution.
|
||||
pub disable_main_poisson_packet_distribution: bool,
|
||||
|
||||
/// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
|
||||
pub use_extended_packet_size: bool,
|
||||
/// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size.
|
||||
pub use_extended_packet_size: Option<ExtendedPacketSize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExtendedPacketSize {
|
||||
Extended8,
|
||||
Extended16,
|
||||
Extended32,
|
||||
}
|
||||
|
||||
impl Default for Debug {
|
||||
@@ -488,7 +497,17 @@ impl Default for Debug {
|
||||
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
|
||||
disable_loop_cover_traffic_stream: false,
|
||||
disable_main_poisson_packet_distribution: false,
|
||||
use_extended_packet_size: false,
|
||||
use_extended_packet_size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExtendedPacketSize> for PacketSize {
|
||||
fn from(size: ExtendedPacketSize) -> PacketSize {
|
||||
match size {
|
||||
ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8,
|
||||
ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16,
|
||||
ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,6 @@ pub enum ClientCoreError {
|
||||
ListOfValidatorApisIsEmpty,
|
||||
#[error("Could not load existing gateway configuration: {0}")]
|
||||
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
|
||||
#[error("The current network topology seem to be insufficient to route any packets through")]
|
||||
InsufficientNetworkTopology,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ pretty_env_logger = "0.4" # for formatting log messages
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
|
||||
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
|
||||
sled = "0.34" # for storage of replySURB decryption keys
|
||||
thiserror = "1.0.34"
|
||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime
|
||||
tokio-tungstenite = "0.14" # websocket
|
||||
|
||||
@@ -38,6 +39,7 @@ completions = { path = "../../common/completions" }
|
||||
credential-storage = { path = "../../common/credential-storage" }
|
||||
credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
logging = { path = "../../common/logging"}
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
gateway-requests = { path = "../../gateway/gateway-requests" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
@@ -6,9 +6,7 @@ use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
};
|
||||
use client_core::client::key_manager::KeyManager;
|
||||
use client_core::client::mix_traffic::{
|
||||
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
|
||||
};
|
||||
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
|
||||
use client_core::client::real_messages_control;
|
||||
use client_core::client::real_messages_control::RealMessagesController;
|
||||
use client_core::client::received_buffer::{
|
||||
@@ -20,6 +18,7 @@ use client_core::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
use client_core::error::ClientCoreError;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
@@ -31,11 +30,11 @@ use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::error::ClientError;
|
||||
use crate::websocket;
|
||||
|
||||
pub(crate) mod config;
|
||||
@@ -103,8 +102,9 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
stream.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
stream.start_with_shutdown(shutdown);
|
||||
@@ -132,8 +132,9 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
controller_config.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
info!("Starting real traffic stream...");
|
||||
@@ -233,7 +234,7 @@ impl NymClient {
|
||||
&mut self,
|
||||
topology_accessor: TopologyAccessor,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
) -> Result<(), ClientError> {
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(
|
||||
self.config.get_base().get_validator_api_endpoints(),
|
||||
self.config.get_base().get_topology_refresh_rate(),
|
||||
@@ -248,14 +249,16 @@ impl NymClient {
|
||||
|
||||
// TODO: a slightly more graceful termination here
|
||||
if !topology_refresher.is_topology_routable().await {
|
||||
panic!(
|
||||
"The current network topology seem to be insufficient to route any packets through\
|
||||
log::error!(
|
||||
"The current network topology seem to be insufficient to route any packets through \
|
||||
- check if enough nodes and a gateway are online"
|
||||
);
|
||||
return Err(ClientCoreError::InsufficientNetworkTopology.into());
|
||||
}
|
||||
|
||||
info!("Starting topology refresher...");
|
||||
topology_refresher.start_with_shutdown(shutdown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
|
||||
@@ -263,13 +266,13 @@ impl NymClient {
|
||||
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
|
||||
// requests?
|
||||
fn start_mix_traffic_controller(
|
||||
&mut self,
|
||||
mix_rx: BatchMixMessageReceiver,
|
||||
gateway_client: GatewayClient,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
) -> BatchMixMessageSender {
|
||||
info!("Starting mix traffic controller...");
|
||||
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
|
||||
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
|
||||
mix_traffic_controller.start_with_shutdown(shutdown);
|
||||
mix_tx
|
||||
}
|
||||
|
||||
fn start_websocket_listener(
|
||||
@@ -329,8 +332,8 @@ impl NymClient {
|
||||
}
|
||||
|
||||
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
|
||||
pub async fn run_forever(&mut self) {
|
||||
let shutdown = self.start().await;
|
||||
pub async fn run_forever(&mut self) -> Result<(), ClientError> {
|
||||
let shutdown = self.start().await?;
|
||||
wait_for_signal().await;
|
||||
|
||||
println!(
|
||||
@@ -347,20 +350,16 @@ impl NymClient {
|
||||
//shutdown.wait_for_shutdown().await;
|
||||
|
||||
log::info!("Stopping nym-client");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> ShutdownNotifier {
|
||||
pub async fn start(&mut self) -> Result<ShutdownNotifier, ClientError> {
|
||||
info!("Starting nym client");
|
||||
// channels for inter-component communication
|
||||
// TODO: make the channels be internally created by the relevant components
|
||||
// rather than creating them here, so say for example the buffer controller would create the request channels
|
||||
// and would allow anyone to clone the sender channel
|
||||
|
||||
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
|
||||
// they are used by cover traffic stream and real traffic stream
|
||||
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
|
||||
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
|
||||
|
||||
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
|
||||
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
|
||||
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
|
||||
@@ -385,7 +384,7 @@ impl NymClient {
|
||||
// the components are started in very specific order. Unless you know what you are doing,
|
||||
// do not change that.
|
||||
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
|
||||
.await;
|
||||
.await?;
|
||||
self.start_received_messages_buffer_controller(
|
||||
received_buffer_request_receiver,
|
||||
mixnet_messages_receiver,
|
||||
@@ -397,11 +396,13 @@ impl NymClient {
|
||||
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
|
||||
.await;
|
||||
|
||||
self.start_mix_traffic_controller(
|
||||
sphinx_message_receiver,
|
||||
gateway_client,
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
|
||||
// that are to be sent to the mixnet. They are used by cover traffic stream and real
|
||||
// traffic stream.
|
||||
// The MixTrafficController then sends the actual traffic
|
||||
let sphinx_message_sender =
|
||||
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
reply_key_storage,
|
||||
@@ -447,6 +448,6 @@ impl NymClient {
|
||||
info!("Client startup finished!");
|
||||
info!("The address of this client is: {}", self.as_mix_recipient());
|
||||
|
||||
shutdown
|
||||
Ok(shutdown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::error::ClientError;
|
||||
use clap::CommandFactory;
|
||||
use clap::{Parser, Subcommand};
|
||||
use completions::{fig_generate, ArgShell};
|
||||
@@ -83,16 +84,17 @@ pub(crate) struct OverrideConfig {
|
||||
enabled_credentials_mode: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: &Cli) {
|
||||
pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> {
|
||||
let bin_name = "nym-native-client";
|
||||
|
||||
match &args.command {
|
||||
Commands::Init(m) => init::execute(m).await,
|
||||
Commands::Run(m) => run::execute(m).await,
|
||||
Commands::Run(m) => run::execute(m).await?,
|
||||
Commands::Upgrade(m) => upgrade::execute(m),
|
||||
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
|
||||
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::{
|
||||
client::{config::Config, NymClient},
|
||||
commands::{override_config, OverrideConfig},
|
||||
error::ClientError,
|
||||
};
|
||||
|
||||
use clap::Args;
|
||||
@@ -73,14 +74,14 @@ fn version_check(cfg: &Config) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: &Run) {
|
||||
pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> {
|
||||
let id = &args.id;
|
||||
|
||||
let mut config = match Config::load_from_file(Some(id)) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
|
||||
return;
|
||||
return Err(ClientError::FailedToLoadConfig(id.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,8 +90,8 @@ pub(crate) async fn execute(args: &Run) {
|
||||
|
||||
if !version_check(&config) {
|
||||
error!("failed the local version check");
|
||||
return;
|
||||
return Err(ClientError::FailedLocalVersionCheck);
|
||||
}
|
||||
|
||||
NymClient::new(config).run_forever().await;
|
||||
NymClient::new(config).run_forever().await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use client_core::error::ClientCoreError;
|
||||
use crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||
use gateway_client::error::GatewayClientError;
|
||||
use validator_client::ValidatorClientError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Gateway client error: {0}")]
|
||||
GatewayClientError(#[from] GatewayClientError),
|
||||
#[error("Ed25519 error: {0}")]
|
||||
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
|
||||
#[error("Validator client error: {0}")]
|
||||
ValidatorClientError(#[from] ValidatorClientError),
|
||||
#[error("client-core error: {0}")]
|
||||
ClientCoreError(#[from] ClientCoreError),
|
||||
|
||||
#[error("Failed to load config for: {0}")]
|
||||
FailedToLoadConfig(String),
|
||||
#[error("Failed local version check, client and config mismatch")]
|
||||
FailedLocalVersionCheck,
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub mod websocket;
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{crate_version, Parser};
|
||||
use error::ClientError;
|
||||
use logging::setup_logging;
|
||||
use network_defaults::setup_env;
|
||||
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod websocket;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> Result<(), ClientError> {
|
||||
setup_logging();
|
||||
println!("{}", banner());
|
||||
|
||||
let args = commands::Cli::parse();
|
||||
setup_env(args.config_env_file.clone());
|
||||
commands::execute(&args).await;
|
||||
commands::execute(&args).await
|
||||
}
|
||||
|
||||
fn banner() -> String {
|
||||
@@ -34,25 +37,3 @@ fn banner() -> String {
|
||||
crate_version!()
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("handlebars", log::LevelFilter::Warn)
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pretty_env_logger = "0.4"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
serde = { version = "1.0", features = ["derive"] } # for config serialization/deserialization
|
||||
snafu = "0.6"
|
||||
thiserror = "1.0.34"
|
||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
|
||||
url = "2.2"
|
||||
|
||||
@@ -31,6 +32,7 @@ completions = { path = "../../common/completions" }
|
||||
credential-storage = { path = "../../common/credential-storage" }
|
||||
credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
logging = { path = "../../common/logging"}
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
gateway-requests = { path = "../../gateway/gateway-requests" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::error::Socks5ClientError;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
@@ -13,9 +14,7 @@ use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
};
|
||||
use client_core::client::key_manager::KeyManager;
|
||||
use client_core::client::mix_traffic::{
|
||||
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
|
||||
};
|
||||
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
|
||||
use client_core::client::real_messages_control::RealMessagesController;
|
||||
use client_core::client::received_buffer::{
|
||||
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
|
||||
@@ -25,6 +24,7 @@ use client_core::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
use client_core::error::ClientCoreError;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
@@ -36,7 +36,6 @@ use gateway_client::{
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
pub mod config;
|
||||
@@ -103,8 +102,9 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
stream.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
stream.start_with_shutdown(shutdown);
|
||||
@@ -132,8 +132,9 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
controller_config.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
info!("Starting real traffic stream...");
|
||||
@@ -233,7 +234,7 @@ impl NymClient {
|
||||
&mut self,
|
||||
topology_accessor: TopologyAccessor,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
) -> Result<(), Socks5ClientError> {
|
||||
let topology_refresher_config = TopologyRefresherConfig::new(
|
||||
self.config.get_base().get_validator_api_endpoints(),
|
||||
self.config.get_base().get_topology_refresh_rate(),
|
||||
@@ -248,14 +249,16 @@ impl NymClient {
|
||||
|
||||
// TODO: a slightly more graceful termination here
|
||||
if !topology_refresher.is_topology_routable().await {
|
||||
panic!(
|
||||
"The current network topology seem to be insufficient to route any packets through\
|
||||
log::error!(
|
||||
"The current network topology seem to be insufficient to route any packets through \
|
||||
- check if enough nodes and a gateway are online"
|
||||
);
|
||||
return Err(ClientCoreError::InsufficientNetworkTopology.into());
|
||||
}
|
||||
|
||||
info!("Starting topology refresher...");
|
||||
topology_refresher.start_with_shutdown(shutdown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
|
||||
@@ -263,13 +266,13 @@ impl NymClient {
|
||||
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
|
||||
// requests?
|
||||
fn start_mix_traffic_controller(
|
||||
&mut self,
|
||||
mix_rx: BatchMixMessageReceiver,
|
||||
gateway_client: GatewayClient,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
) -> BatchMixMessageSender {
|
||||
info!("Starting mix traffic controller...");
|
||||
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
|
||||
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
|
||||
mix_traffic_controller.start_with_shutdown(shutdown);
|
||||
mix_tx
|
||||
}
|
||||
|
||||
fn start_socks5_listener(
|
||||
@@ -294,8 +297,8 @@ impl NymClient {
|
||||
}
|
||||
|
||||
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
|
||||
pub async fn run_forever(&mut self) {
|
||||
let mut shutdown = self.start().await;
|
||||
pub async fn run_forever(&mut self) -> Result<(), Socks5ClientError> {
|
||||
let mut shutdown = self.start().await?;
|
||||
wait_for_signal().await;
|
||||
|
||||
log::info!("Sending shutdown");
|
||||
@@ -306,11 +309,15 @@ impl NymClient {
|
||||
shutdown.wait_for_shutdown().await;
|
||||
|
||||
log::info!("Stopping nym-socks5-client");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Variant of `run_forever` that listends for remote control messages
|
||||
pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) {
|
||||
let mut shutdown = self.start().await;
|
||||
pub async fn run_and_listen(
|
||||
&mut self,
|
||||
mut receiver: Socks5ControlMessageReceiver,
|
||||
) -> Result<(), Socks5ClientError> {
|
||||
let mut shutdown = self.start().await?;
|
||||
tokio::select! {
|
||||
message = receiver.next() => {
|
||||
log::debug!("Received message: {:?}", message);
|
||||
@@ -336,20 +343,16 @@ impl NymClient {
|
||||
shutdown.wait_for_shutdown().await;
|
||||
|
||||
log::info!("Stopping nym-socks5-client");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) -> ShutdownNotifier {
|
||||
pub async fn start(&mut self) -> Result<ShutdownNotifier, Socks5ClientError> {
|
||||
info!("Starting nym client");
|
||||
// channels for inter-component communication
|
||||
// TODO: make the channels be internally created by the relevant components
|
||||
// rather than creating them here, so say for example the buffer controller would create the request channels
|
||||
// and would allow anyone to clone the sender channel
|
||||
|
||||
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
|
||||
// they are used by cover traffic stream and real traffic stream
|
||||
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
|
||||
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
|
||||
|
||||
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
|
||||
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
|
||||
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
|
||||
@@ -374,7 +377,7 @@ impl NymClient {
|
||||
// the components are started in very specific order. Unless you know what you are doing,
|
||||
// do not change that.
|
||||
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
|
||||
.await;
|
||||
.await?;
|
||||
self.start_received_messages_buffer_controller(
|
||||
received_buffer_request_receiver,
|
||||
mixnet_messages_receiver,
|
||||
@@ -386,11 +389,13 @@ impl NymClient {
|
||||
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
|
||||
.await;
|
||||
|
||||
self.start_mix_traffic_controller(
|
||||
sphinx_message_receiver,
|
||||
gateway_client,
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
|
||||
// that are to be sent to the mixnet. They are used by cover traffic stream and real
|
||||
// traffic stream.
|
||||
// The MixTrafficController then sends the actual traffic
|
||||
let sphinx_message_sender =
|
||||
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
reply_key_storage,
|
||||
@@ -421,6 +426,6 @@ impl NymClient {
|
||||
info!("Client startup finished!");
|
||||
info!("The address of this client is: {}", self.as_mix_recipient());
|
||||
|
||||
shutdown
|
||||
Ok(shutdown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::error::Socks5ClientError;
|
||||
use clap::CommandFactory;
|
||||
use clap::{Parser, Subcommand};
|
||||
use completions::{fig_generate, ArgShell};
|
||||
@@ -83,16 +84,17 @@ pub(crate) struct OverrideConfig {
|
||||
enabled_credentials_mode: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: &Cli) {
|
||||
pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> {
|
||||
let bin_name = "nym-socks5-client";
|
||||
|
||||
match &args.command {
|
||||
Commands::Init(m) => init::execute(m).await,
|
||||
Commands::Run(m) => run::execute(m).await,
|
||||
Commands::Run(m) => run::execute(m).await?,
|
||||
Commands::Upgrade(m) => upgrade::execute(m),
|
||||
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
|
||||
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::{
|
||||
client::{config::Config, NymClient},
|
||||
commands::{override_config, OverrideConfig},
|
||||
error::Socks5ClientError,
|
||||
};
|
||||
|
||||
use clap::Args;
|
||||
@@ -80,14 +81,14 @@ fn version_check(cfg: &Config) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: &Run) {
|
||||
pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> {
|
||||
let id = &args.id;
|
||||
|
||||
let mut config = match Config::load_from_file(Some(id)) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
|
||||
return;
|
||||
return Err(Socks5ClientError::FailedToLoadConfig(id.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,8 +97,8 @@ pub(crate) async fn execute(args: &Run) {
|
||||
|
||||
if !version_check(&config) {
|
||||
error!("failed the local version check");
|
||||
return;
|
||||
return Err(Socks5ClientError::FailedLocalVersionCheck);
|
||||
}
|
||||
|
||||
NymClient::new(config).run_forever().await;
|
||||
NymClient::new(config).run_forever().await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use client_core::error::ClientCoreError;
|
||||
use crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||
use gateway_client::error::GatewayClientError;
|
||||
use validator_client::ValidatorClientError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Socks5ClientError {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Gateway client error: {0}")]
|
||||
GatewayClientError(#[from] GatewayClientError),
|
||||
#[error("Ed25519 error: {0}")]
|
||||
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
|
||||
#[error("Validator client error: {0}")]
|
||||
ValidatorClientError(#[from] ValidatorClientError),
|
||||
#[error("client-core error: {0}")]
|
||||
ClientCoreError(#[from] ClientCoreError),
|
||||
|
||||
#[error("Failed to load config for: {0}")]
|
||||
FailedToLoadConfig(String),
|
||||
#[error("Failed local version check, client and config mismatch")]
|
||||
FailedLocalVersionCheck,
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub mod socks;
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{crate_version, Parser};
|
||||
use error::Socks5ClientError;
|
||||
use logging::setup_logging;
|
||||
use network_defaults::setup_env;
|
||||
|
||||
pub mod client;
|
||||
mod commands;
|
||||
pub mod error;
|
||||
pub mod socks;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> Result<(), Socks5ClientError> {
|
||||
setup_logging();
|
||||
println!("{}", banner());
|
||||
|
||||
let args = commands::Cli::parse();
|
||||
setup_env(args.config_env_file.clone());
|
||||
commands::execute(&args).await;
|
||||
commands::execute(&args).await
|
||||
}
|
||||
|
||||
fn banner() -> String {
|
||||
@@ -34,23 +37,3 @@ fn banner() -> String {
|
||||
crate_version!()
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client-wasm"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
@@ -55,6 +55,8 @@ wee_alloc = { version = "0.4", optional = true }
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
|
||||
wasm-opt = true
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
@@ -2,6 +2,11 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
performance: {
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
},
|
||||
entry: {
|
||||
bootstrap: './bootstrap.js',
|
||||
worker: './worker.js',
|
||||
@@ -22,6 +27,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
],
|
||||
experiments: { syncWebAssembly: true },
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ async function main() {
|
||||
// const preferredGateway = 'CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM';
|
||||
|
||||
const gatewayEndpoint = await get_gateway(validator, preferredGateway);
|
||||
gatewayEndpoint.gateway_listener = "wss://gateway1.nymtech.net:443"; // this is needed if we want it to work on the web. However this gateway is a v1 gateway, we will need to change for v2 once we get there
|
||||
|
||||
// only really useful if you want to adjust some settings like traffic rate
|
||||
// (if not needed you can just pass a null)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
// due to expansion of #[wasm_bindgen] macro on `Debug` Config struct
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
use client_core::config::{Debug as ConfigDebug, GatewayEndpoint};
|
||||
use client_core::config::{Debug as ConfigDebug, ExtendedPacketSize, GatewayEndpoint};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -107,6 +107,11 @@ pub struct Debug {
|
||||
|
||||
impl From<Debug> for ConfigDebug {
|
||||
fn from(debug: Debug) -> Self {
|
||||
// For now we just always use the (older) 32kb extended size
|
||||
let use_extended_packet_size = debug
|
||||
.use_extended_packet_size
|
||||
.then(|| ExtendedPacketSize::Extended32);
|
||||
|
||||
ConfigDebug {
|
||||
average_packet_delay: Duration::from_millis(debug.average_packet_delay_ms),
|
||||
average_ack_delay: Duration::from_millis(debug.average_ack_delay_ms),
|
||||
@@ -126,7 +131,7 @@ impl From<Debug> for ConfigDebug {
|
||||
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
|
||||
disable_main_poisson_packet_distribution: debug
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_extended_packet_size: debug.use_extended_packet_size,
|
||||
use_extended_packet_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +153,7 @@ impl From<ConfigDebug> for Debug {
|
||||
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
|
||||
disable_main_poisson_packet_distribution: debug
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_extended_packet_size: debug.use_extended_packet_size,
|
||||
use_extended_packet_size: debug.use_extended_packet_size.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use client_core::client::{
|
||||
cover_traffic_stream::LoopCoverTrafficStream,
|
||||
inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender},
|
||||
key_manager::KeyManager,
|
||||
mix_traffic::{BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController},
|
||||
mix_traffic::{BatchMixMessageSender, MixTrafficController},
|
||||
real_messages_control::{self, RealMessagesController},
|
||||
received_buffer::{
|
||||
ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender,
|
||||
@@ -22,7 +22,6 @@ use gateway_client::{
|
||||
MixnetMessageSender,
|
||||
};
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use rand::rngs::OsRng;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
@@ -110,8 +109,8 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.debug.use_extended_packet_size {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = &self.config.debug.use_extended_packet_size {
|
||||
stream.set_custom_packet_size(size.clone().into());
|
||||
}
|
||||
|
||||
stream.start();
|
||||
@@ -135,8 +134,8 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.debug.use_extended_packet_size {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = &self.config.debug.use_extended_packet_size {
|
||||
controller_config.set_custom_packet_size(size.clone().into());
|
||||
}
|
||||
|
||||
console_log!("Starting real traffic stream...");
|
||||
@@ -253,13 +252,11 @@ impl NymClient {
|
||||
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
|
||||
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
|
||||
// requests?
|
||||
fn start_mix_traffic_controller(
|
||||
&mut self,
|
||||
mix_rx: BatchMixMessageReceiver,
|
||||
gateway_client: GatewayClient,
|
||||
) {
|
||||
fn start_mix_traffic_controller(gateway_client: GatewayClient) -> BatchMixMessageSender {
|
||||
console_log!("Starting mix traffic controller...");
|
||||
MixTrafficController::new(mix_rx, gateway_client).start();
|
||||
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
|
||||
mix_traffic_controller.start();
|
||||
mix_tx
|
||||
}
|
||||
|
||||
// TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour
|
||||
@@ -307,11 +304,6 @@ impl NymClient {
|
||||
// rather than creating them here, so say for example the buffer controller would create the request channels
|
||||
// and would allow anyone to clone the sender channel
|
||||
|
||||
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
|
||||
// they are used by cover traffic stream and real traffic stream
|
||||
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
|
||||
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
|
||||
|
||||
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
|
||||
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
|
||||
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
|
||||
@@ -339,7 +331,12 @@ impl NymClient {
|
||||
.start_gateway_client(mixnet_messages_sender, ack_sender)
|
||||
.await;
|
||||
|
||||
self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client);
|
||||
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
|
||||
// that are to be sent to the mixnet. They are used by cover traffic stream and real
|
||||
// traffic stream.
|
||||
// The MixTrafficController then sends the actual traffic
|
||||
let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client);
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
ack_receiver,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
@@ -62,9 +62,19 @@ impl PacketRouter {
|
||||
trace!("routing regular packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket.plaintext_size() - ack_overhead
|
||||
== PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended packet");
|
||||
trace!("routing extended8 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended16 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended32 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else {
|
||||
// this can happen if other clients are not padding their messages
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -31,12 +32,14 @@ impl Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +204,8 @@ impl SendWithoutResponse for Client {
|
||||
packet_mode: PacketMode,
|
||||
) -> io::Result<()> {
|
||||
trace!("Sending packet to {:?}", address);
|
||||
let framed_packet = FramedSphinxPacket::new(packet, packet_mode);
|
||||
let framed_packet =
|
||||
FramedSphinxPacket::new(packet, packet_mode, self.config.use_legacy_version);
|
||||
|
||||
if let Some(sender) = self.conn_new.get_mut(&address) {
|
||||
if let Err(err) = sender.channel.try_send(framed_packet) {
|
||||
@@ -259,6 +263,7 @@ mod tests {
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_version: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,14 @@ impl PacketForwarder {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
) -> (PacketForwarder, MixForwardingSender) {
|
||||
let client_config = Config::new(
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_version,
|
||||
);
|
||||
|
||||
let (packet_sender, packet_receiver) = mpsc::unbounded();
|
||||
|
||||
@@ -13,6 +13,7 @@ colored = "2.0"
|
||||
cw3 = "0.13.1"
|
||||
mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" }
|
||||
contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" }
|
||||
coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
vesting-contract = { path = "../../../contracts/vesting" }
|
||||
|
||||
@@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query;
|
||||
use cosmrs::rpc::Error as TendermintRpcError;
|
||||
use cosmrs::rpc::HttpClientUrl;
|
||||
use cosmrs::tx::Msg;
|
||||
use cosmwasm_std::Uint128;
|
||||
use execute::execute;
|
||||
use network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::time::SystemTime;
|
||||
use vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
|
||||
use vesting_contract_common::QueryMsg as VestingQueryMsg;
|
||||
|
||||
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
|
||||
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
|
||||
@@ -47,6 +45,7 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
use mixnet_contract_common::MixId;
|
||||
pub use signing_client::Client as SigningNymdClient;
|
||||
pub use traits::{VestingQueryClient, VestingSigningClient};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
pub mod coin;
|
||||
pub mod cosmwasm_client;
|
||||
@@ -482,16 +481,6 @@ impl<C> NymdClient<C> {
|
||||
self.client.get_total_supply().await
|
||||
}
|
||||
|
||||
pub async fn vesting_get_locked_pledge_cap(&self) -> Result<Uint128, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = VestingQueryMsg::GetLockedPledgeCap {};
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn simulate<I, M>(&self, messages: I) -> Result<SimulateResponse, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
@@ -737,12 +726,16 @@ impl<C> NymdClient<C> {
|
||||
#[execute("vesting")]
|
||||
fn _vesting_update_locked_pledge_cap(
|
||||
&self,
|
||||
amount: Uint128,
|
||||
address: String,
|
||||
cap: PledgeCap,
|
||||
fee: Option<Fee>,
|
||||
) -> (VestingExecuteMsg, Option<Fee>)
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
(VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee)
|
||||
(
|
||||
VestingExecuteMsg::UpdateLockedPledgeCap { address, cap },
|
||||
fee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::NymdClient;
|
||||
use async_trait::async_trait;
|
||||
use contracts_common::ContractBuildInformation;
|
||||
use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp};
|
||||
use mixnet_contract_common::MixId;
|
||||
use serde::Deserialize;
|
||||
use vesting_contract::vesting::Account;
|
||||
use vesting_contract_common::{
|
||||
messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse,
|
||||
@@ -16,6 +18,15 @@ use vesting_contract_common::{
|
||||
|
||||
#[async_trait]
|
||||
pub trait VestingQueryClient {
|
||||
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_vesting_contract_version(&self) -> Result<ContractBuildInformation, NymdError> {
|
||||
self.query_vesting_contract(VestingQueryMsg::GetContractVersion {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn locked_coins(
|
||||
&self,
|
||||
address: &str,
|
||||
@@ -107,6 +118,15 @@ pub trait VestingQueryClient {
|
||||
|
||||
#[async_trait]
|
||||
impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
|
||||
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &query)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn locked_coins(
|
||||
&self,
|
||||
vesting_account_address: &str,
|
||||
|
||||
@@ -9,6 +9,7 @@ use async_trait::async_trait;
|
||||
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
|
||||
use mixnet_contract_common::{Gateway, MixId, MixNode};
|
||||
use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
#[async_trait]
|
||||
pub trait VestingSigningClient {
|
||||
@@ -105,6 +106,7 @@ pub trait VestingSigningClient {
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
amount: Coin,
|
||||
cap: Option<PledgeCap>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError>;
|
||||
}
|
||||
@@ -382,6 +384,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
amount: Coin,
|
||||
cap: Option<PledgeCap>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError> {
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
|
||||
@@ -389,6 +392,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
|
||||
owner_address: owner_address.to_string(),
|
||||
staking_address,
|
||||
vesting_spec,
|
||||
cap,
|
||||
};
|
||||
self.client
|
||||
.execute(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use thiserror::Error;
|
||||
use validator_api_requests::models::RequestError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidatorAPIError {
|
||||
@@ -10,4 +11,7 @@ pub enum ValidatorAPIError {
|
||||
|
||||
#[error("Request failed with error message - {0}")]
|
||||
GenericRequestFailure(String),
|
||||
|
||||
#[error("The validator API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())]
|
||||
ApiRequestFailure { status: u16, error: RequestError },
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::validator_api::error::ValidatorAPIError;
|
||||
use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
|
||||
use reqwest::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use validator_api_requests::coconut::{
|
||||
@@ -12,9 +13,10 @@ use validator_api_requests::coconut::{
|
||||
VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use validator_api_requests::models::{
|
||||
GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
|
||||
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
|
||||
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError,
|
||||
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
@@ -47,6 +49,19 @@ impl Client {
|
||||
&self.url
|
||||
}
|
||||
|
||||
async fn send_get_request<K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<Response, ValidatorAPIError>
|
||||
where
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let url = create_api_url(&self.url, path, params);
|
||||
Ok(self.reqwest_client.get(url).send().await?)
|
||||
}
|
||||
|
||||
async fn query_validator_api<T, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
@@ -57,8 +72,36 @@ impl Client {
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let url = create_api_url(&self.url, path, params);
|
||||
Ok(self.reqwest_client.get(url).send().await?.json().await?)
|
||||
let res = self.send_get_request(path, params).await?;
|
||||
if res.status().is_success() {
|
||||
Ok(res.json().await?)
|
||||
} else {
|
||||
Err(ValidatorAPIError::GenericRequestFailure(res.text().await?))
|
||||
}
|
||||
}
|
||||
|
||||
// This works for endpoints returning Result<Json<T>, ErrorResponse>
|
||||
async fn query_validator_api_fallible<T, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<T, ValidatorAPIError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let res = self.send_get_request(path, params).await?;
|
||||
let status = res.status();
|
||||
if res.status().is_success() {
|
||||
Ok(res.json().await?)
|
||||
} else {
|
||||
let request_error: RequestError = res.json().await?;
|
||||
Err(ValidatorAPIError::ApiRequestFailure {
|
||||
status: status.as_u16(),
|
||||
error: request_error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_validator_api<B, T, K, V>(
|
||||
@@ -135,6 +178,74 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_report(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<MixnodeStatusReportResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
routes::REPORT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_gateway_report(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> Result<GatewayStatusReportResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
routes::REPORT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_history(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<MixnodeUptimeHistoryResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
routes::HISTORY,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_gateway_history(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> Result<GatewayUptimeHistoryResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
routes::HISTORY,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_rewarded_mixnodes_detailed(
|
||||
&self,
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorAPIError> {
|
||||
@@ -234,7 +345,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<RewardEstimationResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -251,7 +362,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<StakeSaturationResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -268,7 +379,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<InclusionProbabilityResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -285,7 +396,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<UptimeResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
|
||||
@@ -10,7 +10,6 @@ pub const GATEWAYS: &str = "gateways";
|
||||
pub const DETAILED: &str = "detailed";
|
||||
pub const ACTIVE: &str = "active";
|
||||
pub const REWARDED: &str = "rewarded";
|
||||
|
||||
pub const COCONUT_ROUTES: &str = "coconut";
|
||||
pub const BANDWIDTH: &str = "bandwidth";
|
||||
|
||||
@@ -28,6 +27,8 @@ pub const CORE_STATUS_COUNT: &str = "core-status-count";
|
||||
pub const SINCE_ARG: &str = "since";
|
||||
|
||||
pub const STATUS: &str = "status";
|
||||
pub const REPORT: &str = "report";
|
||||
pub const HISTORY: &str = "history";
|
||||
pub const REWARD_ESTIMATION: &str = "reward-estimation";
|
||||
pub const AVG_UPTIME: &str = "avg_uptime";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough};
|
||||
|
||||
use comfy_table::Table;
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventData};
|
||||
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventKind};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {}
|
||||
@@ -90,8 +90,8 @@ async fn print_delegation_events(
|
||||
]);
|
||||
|
||||
for event in events {
|
||||
match event.event {
|
||||
PendingEpochEventData::Delegate {
|
||||
match event.event.kind {
|
||||
PendingEpochEventKind::Delegate {
|
||||
owner,
|
||||
mix_id,
|
||||
amount,
|
||||
@@ -107,7 +107,7 @@ async fn print_delegation_events(
|
||||
]);
|
||||
}
|
||||
}
|
||||
PendingEpochEventData::Undelegate {
|
||||
PendingEpochEventKind::Undelegate {
|
||||
owner,
|
||||
mix_id,
|
||||
proxy,
|
||||
|
||||
@@ -12,6 +12,7 @@ use validator_client::nymd::AccountId;
|
||||
use validator_client::nymd::VestingSigningClient;
|
||||
use validator_client::nymd::{CosmosCoin, Denom};
|
||||
use vesting_contract_common::messages::VestingSpecification;
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
use crate::context::SigningClient;
|
||||
|
||||
@@ -34,6 +35,12 @@ pub struct Args {
|
||||
|
||||
#[clap(long)]
|
||||
pub staking_address: Option<String>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM"
|
||||
)]
|
||||
pub pledge_cap: Option<PledgeCap>,
|
||||
}
|
||||
|
||||
pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) {
|
||||
@@ -55,6 +62,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw
|
||||
args.staking_address,
|
||||
Some(vesting),
|
||||
coin.into(),
|
||||
args.pledge_cap,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -9,3 +9,8 @@ edition = "2021"
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.0"
|
||||
|
||||
@@ -11,6 +11,8 @@ use cosmwasm_std::Event;
|
||||
/// * `event`: event to search through.
|
||||
/// * `key`: key associated with the particular attribute
|
||||
pub fn must_find_attribute(event: &Event, key: &str) -> String {
|
||||
// due to how the function is supposed to work, the unwrap is fine in this instance
|
||||
#[allow(clippy::unwrap_used)]
|
||||
may_find_attribute(event, key).unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
|
||||
|
||||
@@ -1,7 +1,128 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::Uint128;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Mul;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
|
||||
amount * Uint128::new(1)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContractsCommonError {
|
||||
#[error("Provided percent value ({0}) is greater than 100%")]
|
||||
InvalidPercent(Decimal),
|
||||
|
||||
#[error("{source}")]
|
||||
StdErr {
|
||||
#[from]
|
||||
source: cosmwasm_std::StdError,
|
||||
},
|
||||
}
|
||||
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
pub fn new(value: Decimal) -> Result<Self, ContractsCommonError> {
|
||||
if value > Decimal::one() {
|
||||
Err(ContractsCommonError::InvalidPercent(value))
|
||||
} else {
|
||||
Ok(Percent(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0 == Decimal::zero()
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self(Decimal::zero())
|
||||
}
|
||||
|
||||
pub fn hundred() -> Self {
|
||||
Self(Decimal::one())
|
||||
}
|
||||
|
||||
pub fn from_percentage_value(value: u64) -> Result<Self, ContractsCommonError> {
|
||||
Percent::new(Decimal::percent(value))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Decimal {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn round_to_integer(&self) -> u8 {
|
||||
let hundred = Decimal::from_ratio(100u32, 1u32);
|
||||
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
|
||||
truncate_decimal(hundred * self.0).u128() as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
|
||||
write!(f, "{}%", adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Percent {
|
||||
type Err = ContractsCommonError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Percent::new(Decimal::from_str(s)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Decimal> for Percent {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Decimal) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Percent> for Decimal {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Percent) -> Self::Output {
|
||||
rhs * self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Uint128> for Percent {
|
||||
type Output = Uint128;
|
||||
|
||||
fn mul(self, rhs: Uint128) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
// implement custom Deserialize because we want to validate Percent has the correct range
|
||||
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Decimal::deserialize(deserializer)?;
|
||||
if v > Decimal::one() {
|
||||
Err(D::Error::custom(
|
||||
"provided decimal percent is larger than 100%",
|
||||
))
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: there's no reason this couldn't be used for proper binaries, but in that case
|
||||
// perhaps the struct should get renamed and moved to a "more" common crate
|
||||
@@ -31,3 +152,47 @@ pub struct ContractBuildInformation {
|
||||
/// Provides the rustc version that was used for the build, for example `1.52.0-nightly`.
|
||||
pub rustc_version: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn percent_serde() {
|
||||
let valid_value = Percent::from_percentage_value(80).unwrap();
|
||||
let serialized = serde_json::to_string(&valid_value).unwrap();
|
||||
|
||||
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(valid_value, deserialized);
|
||||
|
||||
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
|
||||
for invalid_value in invalid_values {
|
||||
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
|
||||
Percent::from_percentage_value(95).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_to_absolute_integer() {
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 1);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 45);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 99);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
|
||||
pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000);
|
||||
|
||||
// I'm still not 100% sure how to feel about existence of this file
|
||||
// This is equivalent of representing our display coin with 6 decimal places.
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
// due to code generated by JsonSchema
|
||||
#![allow(clippy::field_reassign_with_default)]
|
||||
|
||||
use crate::constants::TOKEN_SUPPLY;
|
||||
use crate::helpers::IntoBaseDecimal;
|
||||
use crate::{Addr, MixId};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use cosmwasm_std::{Coin, Decimal, StdResult};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -34,13 +36,11 @@ pub struct Delegation {
|
||||
pub owner: Addr,
|
||||
|
||||
/// Id of the MixNode that this delegation was performed against.
|
||||
#[serde(alias = "node_id")]
|
||||
pub mix_id: MixId,
|
||||
|
||||
// Note to UI/UX devs: there's absolutely no point in displaying this value to the users,
|
||||
// it would serve them no purpose. It's only used for calculating rewards
|
||||
/// Value of the "unit delegation" associated with the mixnode at the time of delegation.
|
||||
#[serde(alias = "crr")]
|
||||
pub cumulative_reward_ratio: Decimal,
|
||||
|
||||
/// Original delegation amount. Note that it is never mutated as delegation accumulates rewards.
|
||||
@@ -62,6 +62,11 @@ impl Delegation {
|
||||
height: u64,
|
||||
proxy: Option<Addr>,
|
||||
) -> Self {
|
||||
assert!(
|
||||
amount.amount <= TOKEN_SUPPLY,
|
||||
"delegation cannot be larger than the token supply"
|
||||
);
|
||||
|
||||
Delegation {
|
||||
owner,
|
||||
mix_id,
|
||||
@@ -89,10 +94,8 @@ impl Delegation {
|
||||
(mix_id, owner_proxy_subkey)
|
||||
}
|
||||
|
||||
pub fn dec_amount(&self) -> Decimal {
|
||||
// the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal
|
||||
// with 0 decimal places
|
||||
Decimal::from_atomics(self.amount.amount, 0).unwrap()
|
||||
pub fn dec_amount(&self) -> StdResult<Decimal> {
|
||||
self.amount.amount.into_base_decimal()
|
||||
}
|
||||
|
||||
pub fn proxy_storage_key(&self) -> OwnerProxySubKey {
|
||||
|
||||
@@ -13,9 +13,6 @@ pub enum MixnetContractError {
|
||||
source: cosmwasm_std::StdError,
|
||||
},
|
||||
|
||||
#[error("Provided percent value is greater than 100%")]
|
||||
InvalidPercent,
|
||||
|
||||
#[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")]
|
||||
OverflowDecimalSubtraction {
|
||||
minuend: Decimal,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
|
||||
use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate};
|
||||
use crate::rewarding::RewardDistribution;
|
||||
use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
|
||||
use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
|
||||
pub use contracts_common::events::*;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, Event};
|
||||
|
||||
@@ -96,7 +96,7 @@ pub const PROXY_KEY: &str = "proxy";
|
||||
// delegation/undelegation
|
||||
pub const DELEGATOR_KEY: &str = "delegator";
|
||||
pub const DELEGATION_TARGET_KEY: &str = "delegation_target";
|
||||
pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height";
|
||||
pub const UNIT_REWARD_KEY: &str = "unit_reward";
|
||||
|
||||
// bonding/unbonding
|
||||
pub const MIX_ID_KEY: &str = "mix_id";
|
||||
@@ -120,52 +120,45 @@ pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params";
|
||||
|
||||
// rewarding
|
||||
pub const INTERVAL_KEY: &str = "interval_details";
|
||||
pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward";
|
||||
pub const TOTAL_PLEDGE_KEY: &str = "pledge";
|
||||
pub const TOTAL_DELEGATIONS_KEY: &str = "delegated";
|
||||
pub const OPERATOR_REWARD_KEY: &str = "operator_reward";
|
||||
pub const DELEGATES_REWARD_KEY: &str = "delegates_reward";
|
||||
pub const APPROXIMATE_TIME_LEFT_SECS_KEY: &str = "approximate_time_left_secs";
|
||||
pub const INTERVAL_REWARDING_PARAMS_UPDATE_KEY: &str = "interval_rewarding_params_update";
|
||||
pub const UPDATED_INTERVAL_REWARDING_PARAMS_KEY: &str = "updated_interval_rewarding_params";
|
||||
pub const PRIOR_DELEGATES_KEY: &str = "prior_delegates";
|
||||
pub const PRIOR_UNIT_REWARD: &str = "prior_unit_reward";
|
||||
pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward";
|
||||
|
||||
pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards";
|
||||
pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations";
|
||||
pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason";
|
||||
pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found";
|
||||
pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh";
|
||||
pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance";
|
||||
|
||||
// rewarded set update
|
||||
pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size";
|
||||
pub const REWARDED_SET_SIZE_KEY: &str = "rewarded_set_size";
|
||||
pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set";
|
||||
pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval";
|
||||
|
||||
pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval";
|
||||
pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
|
||||
pub const BLOCK_HEIGHT_KEY: &str = "block_height";
|
||||
pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error";
|
||||
|
||||
// interval
|
||||
pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed";
|
||||
pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at";
|
||||
pub const REWARDED_SET_NODES_KEY: &str = "rewarded_set_nodes";
|
||||
pub const NEW_EPOCHS_DURATION_SECS_KEY: &str = "new_epoch_durations_secs";
|
||||
pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval";
|
||||
|
||||
pub fn new_delegation_event(
|
||||
created_at: BlockHeight,
|
||||
delegator: &Addr,
|
||||
proxy: &Option<Addr>,
|
||||
amount: &Coin,
|
||||
mix_id: MixId,
|
||||
unit_reward: Decimal,
|
||||
) -> Event {
|
||||
Event::new(MixnetEventType::Delegation)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(DELEGATOR_KEY, delegator)
|
||||
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
|
||||
.add_attribute(AMOUNT_KEY, amount.to_string())
|
||||
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
|
||||
.add_attribute(UNIT_REWARD_KEY, unit_reward.to_string())
|
||||
}
|
||||
|
||||
pub fn new_delegation_on_unbonded_node_event(
|
||||
@@ -218,8 +211,9 @@ pub fn new_withdraw_delegator_reward_event(
|
||||
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
|
||||
}
|
||||
|
||||
pub fn new_active_set_update_event(new_size: u32) -> Event {
|
||||
pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event {
|
||||
Event::new(MixnetEventType::ActiveSetUpdate)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string())
|
||||
}
|
||||
|
||||
@@ -236,10 +230,13 @@ pub fn new_pending_active_set_update_event(
|
||||
}
|
||||
|
||||
pub fn new_rewarding_params_update_event(
|
||||
created_at: BlockHeight,
|
||||
|
||||
update: IntervalRewardingParamsUpdate,
|
||||
updated: IntervalRewardParams,
|
||||
) -> Event {
|
||||
Event::new(MixnetEventType::IntervalRewardingParamsUpdate)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(
|
||||
INTERVAL_REWARDING_PARAMS_UPDATE_KEY,
|
||||
update.to_inline_json(),
|
||||
@@ -265,8 +262,14 @@ pub fn new_pending_rewarding_params_update_event(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_undelegation_event(delegator: &Addr, proxy: &Option<Addr>, mix_id: MixId) -> Event {
|
||||
pub fn new_undelegation_event(
|
||||
created_at: BlockHeight,
|
||||
delegator: &Addr,
|
||||
proxy: &Option<Addr>,
|
||||
mix_id: MixId,
|
||||
) -> Event {
|
||||
Event::new(MixnetEventType::Undelegation)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(DELEGATOR_KEY, delegator)
|
||||
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
|
||||
.add_attribute(MIX_ID_KEY, mix_id.to_string())
|
||||
@@ -327,8 +330,10 @@ pub fn new_mixnode_bonding_event(
|
||||
.add_attribute(AMOUNT_KEY, amount.to_string())
|
||||
}
|
||||
|
||||
pub fn new_mixnode_unbonding_event(mix_id: MixId) -> Event {
|
||||
Event::new(MixnetEventType::MixnodeUnbonding).add_attribute(MIX_ID_KEY, mix_id.to_string())
|
||||
pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event {
|
||||
Event::new(MixnetEventType::MixnodeUnbonding)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(MIX_ID_KEY, mix_id.to_string())
|
||||
}
|
||||
|
||||
pub fn new_pending_mixnode_unbonding_event(
|
||||
@@ -370,8 +375,13 @@ pub fn new_mixnode_pending_cost_params_update_event(
|
||||
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
|
||||
}
|
||||
|
||||
pub fn new_mixnode_cost_params_update_event(mix_id: MixId, new_costs: &MixNodeCostParams) -> Event {
|
||||
pub fn new_mixnode_cost_params_update_event(
|
||||
created_at: BlockHeight,
|
||||
mix_id: MixId,
|
||||
new_costs: &MixNodeCostParams,
|
||||
) -> Event {
|
||||
Event::new(MixnetEventType::MixnodeCostParamsUpdate)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(MIX_ID_KEY, mix_id.to_string())
|
||||
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
|
||||
}
|
||||
@@ -461,7 +471,7 @@ pub fn new_mix_rewarding_event(
|
||||
interval.current_epoch_absolute_id().to_string(),
|
||||
)
|
||||
.add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string())
|
||||
.add_attribute(PRIOR_UNIT_REWARD, prior_unit_reward.to_string())
|
||||
.add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string())
|
||||
.add_attribute(MIX_ID_KEY, mix_id.to_string())
|
||||
.add_attribute(
|
||||
OPERATOR_REWARD_KEY,
|
||||
@@ -497,11 +507,13 @@ pub fn new_reconcile_pending_events() -> Event {
|
||||
}
|
||||
|
||||
pub fn new_interval_config_update_event(
|
||||
created_at: BlockHeight,
|
||||
epochs_in_interval: u32,
|
||||
epoch_duration_secs: u64,
|
||||
updated_rewarding_params: IntervalRewardParams,
|
||||
) -> Event {
|
||||
Event::new(MixnetEventType::IntervalConfigUpdate)
|
||||
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
|
||||
.add_attribute(
|
||||
NEW_EPOCHS_DURATION_SECS_KEY,
|
||||
epoch_duration_secs.to_string(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
|
||||
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
|
||||
@@ -11,3 +11,23 @@ pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
assert!(b - a < epsilon, "{} != {}", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_base_decimal(val: impl Into<Uint128>) -> StdResult<Decimal> {
|
||||
val.into_base_decimal()
|
||||
}
|
||||
|
||||
pub trait IntoBaseDecimal {
|
||||
fn into_base_decimal(self) -> StdResult<Decimal>;
|
||||
}
|
||||
|
||||
impl<T> IntoBaseDecimal for T
|
||||
where
|
||||
T: Into<Uint128>,
|
||||
{
|
||||
fn into_base_decimal(self) -> StdResult<Decimal> {
|
||||
let atomics = self.into();
|
||||
Decimal::from_atomics(atomics, 0).map_err(|_| StdError::GenericErr {
|
||||
msg: format!("Decimal range exceeded for {atomics} with 0 decimal places."),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ pub struct Interval {
|
||||
// TODO add a better TS type generation
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
#[serde(with = "string_rfc3339_offset_date_time")]
|
||||
// note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during
|
||||
// compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay
|
||||
// since we explicitly specified this field should correspond to typescript's string
|
||||
current_epoch_start: OffsetDateTime,
|
||||
current_epoch_id: EpochId,
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))]
|
||||
@@ -142,14 +145,17 @@ impl JsonSchema for Interval {
|
||||
impl Interval {
|
||||
/// Initialize epoch in the contract with default values.
|
||||
pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
|
||||
// if this fails it means the value provided from the chain itself (via cosmwasm) is invalid,
|
||||
// so we really have to panic here as anything beyond that point would be invalid anyway
|
||||
#[allow(clippy::expect_used)]
|
||||
let current_epoch_start =
|
||||
OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64)
|
||||
.expect("The timestamp provided via env.block.time is invalid");
|
||||
|
||||
Interval {
|
||||
id: 0,
|
||||
epochs_in_interval,
|
||||
// I really don't see a way for this to fail, unless the blockchain is lying to us
|
||||
current_epoch_start: OffsetDateTime::from_unix_timestamp(
|
||||
env.block.time.seconds() as i64
|
||||
)
|
||||
.expect("Invalid timestamp from env.block.time"),
|
||||
current_epoch_start,
|
||||
current_epoch_id: 0,
|
||||
epoch_length,
|
||||
total_elapsed_epochs: 0,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
mod constants;
|
||||
pub mod delegation;
|
||||
pub mod error;
|
||||
@@ -34,7 +37,8 @@ pub use mixnode::{
|
||||
};
|
||||
pub use msg::*;
|
||||
pub use pending_events::{
|
||||
PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData,
|
||||
PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingIntervalEvent,
|
||||
PendingIntervalEventData, PendingIntervalEventKind,
|
||||
};
|
||||
pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams};
|
||||
pub use types::*;
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
// due to code generated by JsonSchema
|
||||
#![allow(clippy::field_reassign_with_default)]
|
||||
|
||||
use crate::constants::UNIT_DELEGATION_BASE;
|
||||
use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE};
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::helpers::IntoBaseDecimal;
|
||||
use crate::reward_params::{NodeRewardParams, RewardingParams};
|
||||
use crate::rewarding::helpers::truncate_reward;
|
||||
use crate::rewarding::RewardDistribution;
|
||||
use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey};
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
@@ -64,7 +65,7 @@ impl MixNodeDetails {
|
||||
self.rewarding_details.pending_operator_reward(pledge)
|
||||
}
|
||||
|
||||
pub fn pending_detailed_operator_reward(&self) -> Decimal {
|
||||
pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
|
||||
let pledge = self.original_pledge();
|
||||
self.rewarding_details
|
||||
.pending_detailed_operator_reward(pledge)
|
||||
@@ -78,34 +79,27 @@ impl MixNodeDetails {
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeRewarding {
|
||||
/// Information provided by the operator that influence the cost function.
|
||||
#[serde(alias = "cp")]
|
||||
pub cost_params: MixNodeCostParams,
|
||||
|
||||
/// Total pledge and compounded reward earned by the node operator.
|
||||
#[serde(alias = "op")]
|
||||
pub operator: Decimal,
|
||||
|
||||
/// Total delegation and compounded reward earned by all node delegators.
|
||||
#[serde(alias = "dg")]
|
||||
pub delegates: Decimal,
|
||||
|
||||
/// Cumulative reward earned by the "unit delegation" since the block 0.
|
||||
#[serde(alias = "tur")]
|
||||
pub total_unit_reward: Decimal,
|
||||
|
||||
/// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0.
|
||||
#[serde(alias = "ud")]
|
||||
pub unit_delegation: Decimal,
|
||||
|
||||
/// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt
|
||||
/// to reward it multiple times in the same epoch.
|
||||
#[serde(alias = "le")]
|
||||
pub last_rewarded_epoch: EpochId,
|
||||
|
||||
// technically we don't need that field to determine reward magnitude or anything
|
||||
// but it saves on extra queries to determine if we're removing the final delegation
|
||||
// (so that we could zero the field correctly)
|
||||
#[serde(alias = "uqd")]
|
||||
pub unique_delegations: u32,
|
||||
}
|
||||
|
||||
@@ -114,16 +108,21 @@ impl MixNodeRewarding {
|
||||
cost_params: MixNodeCostParams,
|
||||
initial_pledge: &Coin,
|
||||
current_epoch: EpochId,
|
||||
) -> Self {
|
||||
MixNodeRewarding {
|
||||
) -> Result<Self, MixnetContractError> {
|
||||
assert!(
|
||||
initial_pledge.amount <= TOKEN_SUPPLY,
|
||||
"pledge cannot be larger than the token supply"
|
||||
);
|
||||
|
||||
Ok(MixNodeRewarding {
|
||||
cost_params,
|
||||
operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(),
|
||||
operator: initial_pledge.amount.into_base_decimal()?,
|
||||
delegates: Decimal::zero(),
|
||||
total_unit_reward: Decimal::zero(),
|
||||
unit_delegation: UNIT_DELEGATION_BASE,
|
||||
last_rewarded_epoch: current_epoch,
|
||||
unique_delegations: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Determines whether this node is still bonded. This is performed via a simple check,
|
||||
@@ -142,27 +141,30 @@ impl MixNodeRewarding {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal {
|
||||
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
|
||||
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
|
||||
let initial_dec = original_pledge.amount.into_base_decimal()?;
|
||||
if initial_dec > self.operator {
|
||||
panic!(
|
||||
"seems slashing has occurred while it has not been implemented nor accounted for!"
|
||||
)
|
||||
}
|
||||
self.operator - initial_dec
|
||||
Ok(self.operator - initial_dec)
|
||||
}
|
||||
|
||||
pub fn operator_pledge_with_reward(&self, denom: impl Into<String>) -> Coin {
|
||||
truncate_reward(self.operator, denom)
|
||||
}
|
||||
|
||||
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin {
|
||||
let delegator_reward = self.determine_delegation_reward(delegation);
|
||||
truncate_reward(delegator_reward, &delegation.amount.denom)
|
||||
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
|
||||
let delegator_reward = self.determine_delegation_reward(delegation)?;
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
}
|
||||
|
||||
pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin {
|
||||
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
|
||||
pub fn withdraw_operator_reward(
|
||||
&mut self,
|
||||
original_pledge: &Coin,
|
||||
) -> Result<Coin, MixnetContractError> {
|
||||
let initial_dec = original_pledge.amount.into_base_decimal()?;
|
||||
if initial_dec > self.operator {
|
||||
panic!(
|
||||
"seems slashing has occurred while it has not been implemented nor accounted for!"
|
||||
@@ -171,14 +173,14 @@ impl MixNodeRewarding {
|
||||
let diff = self.operator - initial_dec;
|
||||
self.operator = initial_dec;
|
||||
|
||||
truncate_reward(diff, &original_pledge.denom)
|
||||
Ok(truncate_reward(diff, &original_pledge.denom))
|
||||
}
|
||||
|
||||
pub fn withdraw_delegator_reward(
|
||||
&mut self,
|
||||
delegation: &mut Delegation,
|
||||
) -> Result<Coin, MixnetContractError> {
|
||||
let reward = self.determine_delegation_reward(delegation);
|
||||
let reward = self.determine_delegation_reward(delegation)?;
|
||||
self.decrease_delegates_decimal(reward)?;
|
||||
|
||||
delegation.cumulative_reward_ratio = self.full_reward_ratio();
|
||||
@@ -308,23 +310,27 @@ impl MixNodeRewarding {
|
||||
self.distribute_rewards(reward_distribution, absolute_epoch_id)
|
||||
}
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
|
||||
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> StdResult<Decimal> {
|
||||
let starting_ratio = delegation.cumulative_reward_ratio;
|
||||
let ending_ratio = self.full_reward_ratio();
|
||||
let adjust = starting_ratio + self.unit_delegation;
|
||||
|
||||
(ending_ratio - starting_ratio) * delegation.dec_amount() / adjust
|
||||
Ok((ending_ratio - starting_ratio) * delegation.dec_amount()? / adjust)
|
||||
}
|
||||
|
||||
// this updates `unique_delegations` field
|
||||
pub fn add_base_delegation(&mut self, amount: Uint128) {
|
||||
self.increase_delegates_uint128(amount);
|
||||
pub fn add_base_delegation(&mut self, amount: Uint128) -> Result<(), MixnetContractError> {
|
||||
self.increase_delegates_uint128(amount)?;
|
||||
self.unique_delegations += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn increase_delegates_uint128(&mut self, amount: Uint128) {
|
||||
// the unwrap here is fine as the value is guaranteed to fit under provided constraints
|
||||
self.delegates += Decimal::from_atomics(amount, 0).unwrap()
|
||||
pub fn increase_delegates_uint128(
|
||||
&mut self,
|
||||
amount: Uint128,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
self.delegates += amount.into_base_decimal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this updates `unique_delegations` field
|
||||
@@ -342,7 +348,7 @@ impl MixNodeRewarding {
|
||||
&mut self,
|
||||
amount: Uint128,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let amount_dec = Decimal::from_atomics(amount, 0).unwrap();
|
||||
let amount_dec = amount.into_base_decimal()?;
|
||||
self.decrease_delegates_decimal(amount_dec)
|
||||
}
|
||||
|
||||
@@ -375,8 +381,8 @@ impl MixNodeRewarding {
|
||||
}
|
||||
|
||||
pub fn undelegate(&mut self, delegation: &Delegation) -> Result<Coin, MixnetContractError> {
|
||||
let reward = self.determine_delegation_reward(delegation);
|
||||
let full_amount = reward + delegation.dec_amount();
|
||||
let reward = self.determine_delegation_reward(delegation)?;
|
||||
let full_amount = reward + delegation.dec_amount()?;
|
||||
self.remove_delegation_decimal(full_amount)?;
|
||||
Ok(truncate_reward(full_amount, &delegation.amount.denom))
|
||||
}
|
||||
@@ -428,7 +434,6 @@ impl MixNodeRewarding {
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeBond {
|
||||
/// Unique id assigned to the bonded mixnode.
|
||||
#[serde(alias = "id")]
|
||||
pub mix_id: MixId,
|
||||
|
||||
/// Address of the owner of this mixnode.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::delegation::OwnerProxySubKey;
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::helpers::IntoBaseDecimal;
|
||||
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
|
||||
use crate::reward_params::{
|
||||
IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams,
|
||||
@@ -31,6 +33,7 @@ pub struct InitialRewardingParams {
|
||||
pub initial_reward_pool: Decimal,
|
||||
pub initial_staking_supply: Decimal,
|
||||
|
||||
pub staking_supply_scale_factor: Percent,
|
||||
pub sybil_resistance: Percent,
|
||||
pub active_set_work_factor: Decimal,
|
||||
pub interval_pool_emission: Percent,
|
||||
@@ -40,17 +43,21 @@ pub struct InitialRewardingParams {
|
||||
}
|
||||
|
||||
impl InitialRewardingParams {
|
||||
pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams {
|
||||
pub fn into_rewarding_params(
|
||||
self,
|
||||
epochs_in_interval: u32,
|
||||
) -> Result<RewardingParams, MixnetContractError> {
|
||||
let epoch_reward_budget = self.initial_reward_pool
|
||||
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
|
||||
/ epochs_in_interval.into_base_decimal()?
|
||||
* self.interval_pool_emission;
|
||||
let stake_saturation_point =
|
||||
self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
|
||||
self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?;
|
||||
|
||||
RewardingParams {
|
||||
Ok(RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: self.initial_reward_pool,
|
||||
staking_supply: self.initial_staking_supply,
|
||||
staking_supply_scale_factor: self.staking_supply_scale_factor,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: self.sybil_resistance,
|
||||
@@ -59,7 +66,7 @@ impl InitialRewardingParams {
|
||||
},
|
||||
rewarded_set_size: self.rewarded_set_size,
|
||||
active_set_size: self.active_set_size,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::mixnode::MixNodeCostParams;
|
||||
use crate::reward_params::IntervalRewardingParamsUpdate;
|
||||
use crate::{EpochEventId, IntervalEventId, MixId};
|
||||
use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId};
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -14,7 +14,13 @@ pub struct PendingEpochEvent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PendingEpochEventData {
|
||||
pub struct PendingEpochEventData {
|
||||
pub created_at: BlockHeight,
|
||||
pub kind: PendingEpochEventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PendingEpochEventKind {
|
||||
// can't just pass the `Delegation` struct here as it's impossible to determine
|
||||
// `cumulative_reward_ratio` ahead of time
|
||||
Delegate {
|
||||
@@ -36,6 +42,15 @@ pub enum PendingEpochEventData {
|
||||
},
|
||||
}
|
||||
|
||||
impl PendingEpochEventKind {
|
||||
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingEpochEventData {
|
||||
PendingEpochEventData {
|
||||
created_at,
|
||||
kind: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent {
|
||||
fn from(data: (EpochEventId, PendingEpochEventData)) -> Self {
|
||||
PendingEpochEvent {
|
||||
@@ -52,7 +67,13 @@ pub struct PendingIntervalEvent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PendingIntervalEventData {
|
||||
pub struct PendingIntervalEventData {
|
||||
pub created_at: BlockHeight,
|
||||
pub kind: PendingIntervalEventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PendingIntervalEventKind {
|
||||
ChangeMixCostParams {
|
||||
mix_id: MixId,
|
||||
new_costs: MixNodeCostParams,
|
||||
@@ -67,6 +88,15 @@ pub enum PendingIntervalEventData {
|
||||
},
|
||||
}
|
||||
|
||||
impl PendingIntervalEventKind {
|
||||
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingIntervalEventData {
|
||||
PendingIntervalEventData {
|
||||
created_at,
|
||||
kind: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(IntervalEventId, PendingIntervalEventData)> for PendingIntervalEvent {
|
||||
fn from(data: (IntervalEventId, PendingIntervalEventData)) -> Self {
|
||||
PendingIntervalEvent {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::helpers::IntoBaseDecimal;
|
||||
use crate::{error::MixnetContractError, Percent};
|
||||
use cosmwasm_std::Decimal;
|
||||
use schemars::JsonSchema;
|
||||
@@ -26,6 +27,11 @@ pub struct IntervalRewardParams {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
pub staking_supply: Decimal,
|
||||
|
||||
/// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set.
|
||||
/// Also known as `beta`.
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
pub staking_supply_scale_factor: Percent,
|
||||
|
||||
// computed values
|
||||
/// Current value of the computed reward budget per epoch, per node.
|
||||
/// It is expected to be constant throughout the interval.
|
||||
@@ -105,24 +111,33 @@ impl RewardingParams {
|
||||
pub fn dec_rewarded_set_size(&self) -> Decimal {
|
||||
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
|
||||
// with 0 decimal places
|
||||
Decimal::from_atomics(self.rewarded_set_size, 0).unwrap()
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.rewarded_set_size.into_base_decimal().unwrap()
|
||||
}
|
||||
|
||||
pub fn dec_active_set_size(&self) -> Decimal {
|
||||
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
|
||||
// with 0 decimal places
|
||||
Decimal::from_atomics(self.active_set_size, 0).unwrap()
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.active_set_size.into_base_decimal().unwrap()
|
||||
}
|
||||
|
||||
fn dec_standby_set_size(&self) -> Decimal {
|
||||
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
|
||||
// with 0 decimal places
|
||||
Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap()
|
||||
#[allow(clippy::unwrap_used)]
|
||||
(self.rewarded_set_size - self.active_set_size)
|
||||
.into_base_decimal()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) {
|
||||
self.interval.epoch_reward_budget = self.interval.reward_pool
|
||||
/ Decimal::from_atomics(new_epochs_in_interval, 0).unwrap()
|
||||
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
|
||||
// with 0 decimal places
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let new_epochs_in_interval = new_epochs_in_interval.into_base_decimal().unwrap();
|
||||
|
||||
self.interval.epoch_reward_budget = self.interval.reward_pool / new_epochs_in_interval
|
||||
* self.interval.interval_pool_emission;
|
||||
}
|
||||
|
||||
@@ -164,6 +179,10 @@ impl RewardingParams {
|
||||
self.interval.staking_supply = staking_supply;
|
||||
}
|
||||
|
||||
if let Some(staking_supply_scale_factor) = updates.staking_supply_scale_factor {
|
||||
self.interval.staking_supply_scale_factor = staking_supply_scale_factor
|
||||
}
|
||||
|
||||
if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent {
|
||||
self.interval.sybil_resistance = sybil_resistance_percent;
|
||||
}
|
||||
@@ -191,13 +210,13 @@ impl RewardingParams {
|
||||
|
||||
if recompute_epoch_budget {
|
||||
self.interval.epoch_reward_budget = self.interval.reward_pool
|
||||
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
|
||||
/ epochs_in_interval.into_base_decimal()?
|
||||
* self.interval.interval_pool_emission;
|
||||
}
|
||||
|
||||
if recompute_saturation_point {
|
||||
self.interval.stake_saturation_point = self.interval.staking_supply
|
||||
/ Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
|
||||
self.interval.stake_saturation_point =
|
||||
self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -235,6 +254,9 @@ pub struct IntervalRewardingParamsUpdate {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub staking_supply: Option<Decimal>,
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub staking_supply_scale_factor: Option<Percent>,
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub sybil_resistance_percent: Option<Percent>,
|
||||
|
||||
@@ -252,6 +274,7 @@ impl IntervalRewardingParamsUpdate {
|
||||
// essentially at least a single field has to be a `Some`
|
||||
self.reward_pool.is_some()
|
||||
|| self.staking_supply.is_some()
|
||||
|| self.staking_supply_scale_factor.is_some()
|
||||
|| self.sybil_resistance_percent.is_some()
|
||||
|| self.active_set_work_factor.is_some()
|
||||
|| self.interval_pool_emission.is_some()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use contracts_common::truncate_decimal;
|
||||
use cosmwasm_std::{Coin, Decimal, Uint128};
|
||||
|
||||
/// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would
|
||||
@@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into<String>) -> Coin {
|
||||
pub fn truncate_reward_amount(reward: Decimal) -> Uint128 {
|
||||
truncate_decimal(reward)
|
||||
}
|
||||
|
||||
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
|
||||
amount * Uint128::new(1)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub struct RewardEstimate {
|
||||
pub operating_cost: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct RewardDistribution {
|
||||
pub operator: Decimal,
|
||||
pub delegates: Decimal,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::helpers::IntoBaseDecimal;
|
||||
use crate::reward_params::NodeRewardParams;
|
||||
use crate::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use crate::rewarding::RewardDistribution;
|
||||
@@ -33,24 +34,33 @@ impl Simulator {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_epoch(&mut self) {
|
||||
fn advance_epoch(&mut self) -> Result<(), MixnetContractError> {
|
||||
let updated = self.interval.advance_epoch();
|
||||
|
||||
// we rolled over an interval
|
||||
if self.interval.current_interval_id() + 1 == updated.current_interval_id() {
|
||||
let old = self.system_rewarding_params.interval;
|
||||
let reward_pool = old.reward_pool - self.pending_reward_pool_emission;
|
||||
let staking_supply = old.staking_supply + self.pending_reward_pool_emission;
|
||||
let staking_supply = old.staking_supply
|
||||
+ self
|
||||
.system_rewarding_params
|
||||
.interval
|
||||
.staking_supply_scale_factor
|
||||
* self.pending_reward_pool_emission;
|
||||
let epoch_reward_budget = reward_pool
|
||||
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
|
||||
/ self.interval.epochs_in_interval().into_base_decimal()?
|
||||
* old.interval_pool_emission.value();
|
||||
let stake_saturation_point = staking_supply
|
||||
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
|
||||
/ self
|
||||
.system_rewarding_params
|
||||
.rewarded_set_size
|
||||
.into_base_decimal()?;
|
||||
|
||||
let updated_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool,
|
||||
staking_supply,
|
||||
staking_supply_scale_factor: old.staking_supply_scale_factor,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: old.sybil_resistance,
|
||||
@@ -65,9 +75,15 @@ impl Simulator {
|
||||
self.pending_reward_pool_emission = Decimal::zero();
|
||||
}
|
||||
self.interval = updated;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bond(&mut self, pledge: Coin, cost_params: MixNodeCostParams) -> MixId {
|
||||
pub fn bond(
|
||||
&mut self,
|
||||
pledge: Coin,
|
||||
cost_params: MixNodeCostParams,
|
||||
) -> Result<MixId, MixnetContractError> {
|
||||
let mix_id = self.next_mix_id;
|
||||
|
||||
self.nodes.insert(
|
||||
@@ -77,16 +93,24 @@ impl Simulator {
|
||||
cost_params,
|
||||
&pledge,
|
||||
self.interval.current_epoch_absolute_id(),
|
||||
),
|
||||
)?,
|
||||
);
|
||||
|
||||
self.next_mix_id += 1;
|
||||
|
||||
mix_id
|
||||
Ok(mix_id)
|
||||
}
|
||||
|
||||
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin, mix_id: MixId) {
|
||||
let node = self.nodes.get_mut(&mix_id).expect("node doesn't exist");
|
||||
pub fn delegate<S: Into<String>>(
|
||||
&mut self,
|
||||
delegator: S,
|
||||
delegation: Coin,
|
||||
mix_id: MixId,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let node = self
|
||||
.nodes
|
||||
.get_mut(&mix_id)
|
||||
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
|
||||
node.delegate(delegator, delegation)
|
||||
}
|
||||
|
||||
@@ -97,23 +121,35 @@ impl Simulator {
|
||||
delegator: S,
|
||||
mix_id: MixId,
|
||||
) -> Result<(Coin, Coin), MixnetContractError> {
|
||||
let node = self.nodes.get_mut(&mix_id).expect("node not found");
|
||||
let node = self
|
||||
.nodes
|
||||
.get_mut(&mix_id)
|
||||
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
|
||||
node.undelegate(delegator)
|
||||
}
|
||||
|
||||
pub fn simulate_epoch_single_node(&mut self, params: NodeRewardParams) -> RewardDistribution {
|
||||
pub fn simulate_epoch_single_node(
|
||||
&mut self,
|
||||
params: NodeRewardParams,
|
||||
) -> Result<RewardDistribution, MixnetContractError> {
|
||||
assert_eq!(self.nodes.len(), 1);
|
||||
|
||||
let id = *self.nodes.keys().next().unwrap();
|
||||
let mut params_map = BTreeMap::new();
|
||||
params_map.insert(id, params);
|
||||
self.simulate_epoch(¶ms_map).remove(&id).unwrap()
|
||||
if let Some(&id) = self.nodes.keys().next() {
|
||||
let mut params_map = BTreeMap::new();
|
||||
params_map.insert(id, params);
|
||||
Ok(self
|
||||
.simulate_epoch(¶ms_map)?
|
||||
.remove(&id)
|
||||
.unwrap_or_default())
|
||||
} else {
|
||||
Ok(RewardDistribution::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_epoch(
|
||||
&mut self,
|
||||
node_params: &BTreeMap<MixId, NodeRewardParams>,
|
||||
) -> BTreeMap<MixId, RewardDistribution> {
|
||||
) -> Result<BTreeMap<MixId, RewardDistribution>, MixnetContractError> {
|
||||
let mut params_keys = node_params.keys().copied().collect::<Vec<_>>();
|
||||
params_keys.sort_unstable();
|
||||
let mut node_keys = self.nodes.keys().copied().collect::<Vec<_>>();
|
||||
@@ -141,34 +177,41 @@ impl Simulator {
|
||||
dist.insert(*mix_id, reward_distribution);
|
||||
}
|
||||
|
||||
self.advance_epoch();
|
||||
dist
|
||||
self.advance_epoch()?;
|
||||
Ok(dist)
|
||||
}
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
|
||||
self.nodes[&delegation.mix_id]
|
||||
pub fn determine_delegation_reward(
|
||||
&self,
|
||||
delegation: &Delegation,
|
||||
) -> Result<Decimal, MixnetContractError> {
|
||||
Ok(self.nodes[&delegation.mix_id]
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(delegation)
|
||||
.determine_delegation_reward(delegation)?)
|
||||
}
|
||||
|
||||
pub fn determine_total_delegation_reward(&self) -> Decimal {
|
||||
pub fn determine_total_delegation_reward(&self) -> Result<Decimal, MixnetContractError> {
|
||||
let mut total = Decimal::zero();
|
||||
|
||||
for node in self.nodes.values() {
|
||||
for delegation in node.delegations.values() {
|
||||
total += node
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(delegation)
|
||||
.determine_delegation_reward(delegation)?
|
||||
}
|
||||
}
|
||||
total
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
// assume node state doesn't change in the interval (kinda unrealistic)
|
||||
pub fn simulate_full_interval(&mut self, node_params: &BTreeMap<MixId, NodeRewardParams>) {
|
||||
pub fn simulate_full_interval(
|
||||
&mut self,
|
||||
node_params: &BTreeMap<MixId, NodeRewardParams>,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
for _ in 0..self.interval.epochs_in_interval() {
|
||||
self.simulate_epoch(node_params);
|
||||
self.simulate_epoch(node_params)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +234,10 @@ mod tests {
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use std::time::Duration;
|
||||
|
||||
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
|
||||
// https://github.com/rust-lang/rust-clippy/pull/9686
|
||||
// is merged into a release
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn base_simulator(initial_pledge: u128) -> Simulator {
|
||||
let profit_margin = Percent::from_percentage_value(10).unwrap();
|
||||
let interval_operating_cost = Coin::new(40_000_000, "unym");
|
||||
@@ -209,6 +256,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
@@ -231,20 +279,32 @@ mod tests {
|
||||
profit_margin_percent: profit_margin,
|
||||
interval_operating_cost,
|
||||
};
|
||||
simulator.bond(initial_pledge, cost_params);
|
||||
simulator.bond(initial_pledge, cost_params).unwrap();
|
||||
simulator
|
||||
}
|
||||
|
||||
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
|
||||
//
|
||||
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
|
||||
// https://github.com/rust-lang/rust-clippy/pull/9686
|
||||
// is merged into a release
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn check_rewarding_invariant(simulator: &Simulator) {
|
||||
for node in simulator.nodes.values() {
|
||||
let delegation_sum: Decimal =
|
||||
node.delegations.values().map(|d| d.dec_amount()).sum();
|
||||
let delegation_sum: Decimal = node
|
||||
.delegations
|
||||
.values()
|
||||
.map(|d| d.dec_amount().unwrap())
|
||||
.sum();
|
||||
|
||||
let reward_sum: Decimal = node
|
||||
.delegations
|
||||
.values()
|
||||
.map(|d| node.rewarding_details.determine_delegation_reward(d))
|
||||
.map(|d| {
|
||||
node.rewarding_details
|
||||
.determine_delegation_reward(d)
|
||||
.unwrap()
|
||||
})
|
||||
.sum();
|
||||
|
||||
// let reward_sum = simulator.determine_total_delegation_reward();
|
||||
@@ -262,7 +322,7 @@ mod tests {
|
||||
|
||||
let epoch_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch_single_node(epoch_params);
|
||||
let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap();
|
||||
|
||||
assert_eq!(rewards.delegates, Decimal::zero());
|
||||
compare_decimals(
|
||||
@@ -275,11 +335,13 @@ mod tests {
|
||||
#[test]
|
||||
fn single_delegation_at_genesis() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator
|
||||
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
|
||||
compare_decimals(
|
||||
rewards.delegates,
|
||||
@@ -290,7 +352,7 @@ mod tests {
|
||||
|
||||
compare_decimals(
|
||||
rewards.delegates,
|
||||
simulator.determine_total_delegation_reward(),
|
||||
simulator.determine_total_delegation_reward().unwrap(),
|
||||
None,
|
||||
);
|
||||
let node = &simulator.nodes[&0];
|
||||
@@ -310,20 +372,22 @@ mod tests {
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator1 = "1128452.5416104363".parse().unwrap();
|
||||
assert_eq!(rewards1.delegates, Decimal::zero());
|
||||
compare_decimals(rewards1.operator, expected_operator1, None);
|
||||
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator
|
||||
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator2 = "1363843.413584609".parse().unwrap();
|
||||
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward1, None);
|
||||
compare_decimals(rewards2.operator, expected_operator2, None);
|
||||
|
||||
let rewards3 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards3 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator3 = "1364017.7824440491".parse().unwrap();
|
||||
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
|
||||
compare_decimals(rewards3.delegates, expected_delegator_reward2, None);
|
||||
@@ -357,11 +421,15 @@ mod tests {
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
|
||||
simulator
|
||||
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
|
||||
@@ -371,14 +439,15 @@ mod tests {
|
||||
let node = simulator.nodes.get_mut(&0).unwrap();
|
||||
let reward = node
|
||||
.rewarding_details
|
||||
.withdraw_operator_reward(&original_pledge);
|
||||
.withdraw_operator_reward(&original_pledge)
|
||||
.unwrap();
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
|
||||
assert_eq!(
|
||||
node.rewarding_details.operator,
|
||||
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
|
||||
);
|
||||
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator2 = "1411113.0004067947".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
|
||||
@@ -395,11 +464,15 @@ mod tests {
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
|
||||
simulator
|
||||
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
|
||||
.unwrap();
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
|
||||
@@ -417,7 +490,7 @@ mod tests {
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
|
||||
|
||||
// new reward after withdrawal
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
let expected_operator2 = "1411250.1907492676".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
|
||||
@@ -460,22 +533,30 @@ mod tests {
|
||||
let mut performance = Percent::from_percentage_value(100).unwrap();
|
||||
for epoch in 0..720 {
|
||||
if epoch == 0 {
|
||||
simulator.delegate("a", Coin::new(18000_000000, "unym"), 0)
|
||||
simulator
|
||||
.delegate("a", Coin::new(18000_000000, "unym"), 0)
|
||||
.unwrap()
|
||||
}
|
||||
if epoch == 42 {
|
||||
simulator.delegate("b", Coin::new(2000_000000, "unym"), 0)
|
||||
simulator
|
||||
.delegate("b", Coin::new(2000_000000, "unym"), 0)
|
||||
.unwrap()
|
||||
}
|
||||
if epoch == 89 {
|
||||
is_active = false;
|
||||
}
|
||||
if epoch == 123 {
|
||||
simulator.delegate("c", Coin::new(6666_000000, "unym"), 0)
|
||||
simulator
|
||||
.delegate("c", Coin::new(6666_000000, "unym"), 0)
|
||||
.unwrap()
|
||||
}
|
||||
if epoch == 167 {
|
||||
performance = Percent::from_percentage_value(90).unwrap();
|
||||
}
|
||||
if epoch == 245 {
|
||||
simulator.delegate("d", Coin::new(2050_000000, "unym"), 0)
|
||||
simulator
|
||||
.delegate("d", Coin::new(2050_000000, "unym"), 0)
|
||||
.unwrap()
|
||||
}
|
||||
if epoch == 264 {
|
||||
let (delegation, _reward) = simulator.undelegate("b", 0).unwrap();
|
||||
@@ -496,13 +577,15 @@ mod tests {
|
||||
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
|
||||
}
|
||||
if epoch == 545 {
|
||||
simulator.delegate("e", Coin::new(5000_000000, "unym"), 0)
|
||||
simulator
|
||||
.delegate("e", Coin::new(5000_000000, "unym"), 0)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// this has to always hold
|
||||
check_rewarding_invariant(&simulator);
|
||||
let node_params = NodeRewardParams::new(performance, is_active);
|
||||
simulator.simulate_epoch_single_node(node_params);
|
||||
simulator.simulate_epoch_single_node(node_params).unwrap();
|
||||
}
|
||||
|
||||
// after everyone undelegates, there should be nothing left in the delegates pool
|
||||
@@ -537,6 +620,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
@@ -555,95 +639,135 @@ mod tests {
|
||||
|
||||
let mut simulator = Simulator::new(rewarding_params, interval);
|
||||
|
||||
let n0 = simulator.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0);
|
||||
let n0 = simulator
|
||||
.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0)
|
||||
.unwrap();
|
||||
|
||||
let n1 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1);
|
||||
let n1 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1)
|
||||
.unwrap();
|
||||
|
||||
let n2 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2);
|
||||
let n2 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2)
|
||||
.unwrap();
|
||||
|
||||
let n3 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3);
|
||||
let n3 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3)
|
||||
.unwrap();
|
||||
|
||||
let n4 = simulator.bond(
|
||||
Coin::new(1000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4);
|
||||
let n4 = simulator
|
||||
.bond(
|
||||
Coin::new(1000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4)
|
||||
.unwrap();
|
||||
|
||||
let n5 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5);
|
||||
let n5 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5)
|
||||
.unwrap();
|
||||
|
||||
let n6 = simulator.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6);
|
||||
let n6 = simulator
|
||||
.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6)
|
||||
.unwrap();
|
||||
|
||||
let n7 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7);
|
||||
let n7 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7)
|
||||
.unwrap();
|
||||
|
||||
let n8 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8);
|
||||
let n8 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8)
|
||||
.unwrap();
|
||||
|
||||
let n9 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9);
|
||||
let n9 = simulator
|
||||
.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
simulator
|
||||
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9)
|
||||
.unwrap();
|
||||
|
||||
let uptime_1 = Percent::from_percentage_value(100).unwrap();
|
||||
let uptime_09 = Percent::from_percentage_value(90).unwrap();
|
||||
@@ -665,7 +789,7 @@ mod tests {
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for _ in 0..23 {
|
||||
simulator.simulate_full_interval(&node_params);
|
||||
simulator.simulate_full_interval(&node_params).unwrap();
|
||||
}
|
||||
|
||||
// we allow the delta to be within 0.1unym,
|
||||
|
||||
+20
-12
@@ -20,21 +20,25 @@ impl SimulatedNode {
|
||||
cost_params: MixNodeCostParams,
|
||||
initial_pledge: &Coin,
|
||||
current_epoch: EpochId,
|
||||
) -> Self {
|
||||
SimulatedNode {
|
||||
) -> Result<Self, MixnetContractError> {
|
||||
Ok(SimulatedNode {
|
||||
mix_id,
|
||||
rewarding_details: MixNodeRewarding::initialise_new(
|
||||
cost_params,
|
||||
initial_pledge,
|
||||
current_epoch,
|
||||
),
|
||||
)?,
|
||||
delegations: HashMap::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin) {
|
||||
pub fn delegate<S: Into<String>>(
|
||||
&mut self,
|
||||
delegator: S,
|
||||
delegation: Coin,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
self.rewarding_details
|
||||
.add_base_delegation(delegation.amount);
|
||||
.add_base_delegation(delegation.amount)?;
|
||||
|
||||
let delegator = delegator.into();
|
||||
let delegation = Delegation::new(
|
||||
@@ -47,6 +51,7 @@ impl SimulatedNode {
|
||||
);
|
||||
|
||||
self.delegations.insert(delegator, delegation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn undelegate<S: Into<String>>(
|
||||
@@ -54,16 +59,19 @@ impl SimulatedNode {
|
||||
delegator: S,
|
||||
) -> Result<(Coin, Coin), MixnetContractError> {
|
||||
let delegator = delegator.into();
|
||||
let delegation = self
|
||||
.delegations
|
||||
.remove(&delegator)
|
||||
.expect("delegation not found");
|
||||
let delegation = self.delegations.remove(&delegator).ok_or(
|
||||
MixnetContractError::NoMixnodeDelegationFound {
|
||||
mix_id: MixId::MAX,
|
||||
address: delegator,
|
||||
proxy: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
let reward = self
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(&delegation);
|
||||
.determine_delegation_reward(&delegation)?;
|
||||
self.rewarding_details
|
||||
.remove_delegation_decimal(delegation.dec_amount() + reward)?;
|
||||
.remove_delegation_decimal(delegation.dec_amount()? + reward)?;
|
||||
|
||||
let reward_denom = &delegation.amount.denom;
|
||||
let truncated_reward = truncate_reward(reward, reward_denom);
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::rewarding::helpers::truncate_decimal;
|
||||
use crate::{Layer, RewardedSetNodeStatus};
|
||||
use cosmwasm_std::{Addr, Uint128};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Coin;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Index, Mul};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Index;
|
||||
|
||||
// type aliases for better reasoning about available data
|
||||
pub type IdentityKey = String;
|
||||
@@ -20,90 +17,10 @@ pub type SphinxKeyRef<'a> = &'a str;
|
||||
pub type EpochId = u32;
|
||||
pub type IntervalId = u32;
|
||||
pub type MixId = u32;
|
||||
pub type BlockHeight = u64;
|
||||
pub type EpochEventId = u32;
|
||||
pub type IntervalEventId = u32;
|
||||
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
pub fn new(value: Decimal) -> Result<Self, MixnetContractError> {
|
||||
if value > Decimal::one() {
|
||||
Err(MixnetContractError::InvalidPercent)
|
||||
} else {
|
||||
Ok(Percent(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0 == Decimal::zero()
|
||||
}
|
||||
|
||||
pub fn from_percentage_value(value: u64) -> Result<Self, MixnetContractError> {
|
||||
Percent::new(Decimal::percent(value))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Decimal {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn round_to_integer(&self) -> u8 {
|
||||
let hundred = Decimal::from_ratio(100u32, 1u32);
|
||||
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
|
||||
truncate_decimal(hundred * self.0).u128() as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
|
||||
write!(f, "{}%", adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Decimal> for Percent {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Decimal) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Percent> for Decimal {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Percent) -> Self::Output {
|
||||
rhs * self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Uint128> for Percent {
|
||||
type Output = Uint128;
|
||||
|
||||
fn mul(self, rhs: Uint128) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
// implement custom Deserialize because we want to validate Percent has the correct range
|
||||
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Decimal::deserialize(deserializer)?;
|
||||
if v > Decimal::one() {
|
||||
Err(D::Error::custom(
|
||||
"provided decimal percent is larger than 100%",
|
||||
))
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct LayerDistribution {
|
||||
pub layer1: u64,
|
||||
@@ -118,6 +35,10 @@ impl LayerDistribution {
|
||||
(Layer::Two, self.layer2),
|
||||
(Layer::Three, self.layer3),
|
||||
];
|
||||
|
||||
// we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY
|
||||
// not empty and thus the unwrap cannot fail
|
||||
#[allow(clippy::unwrap_used)]
|
||||
layers.iter().min_by_key(|x| x.1).unwrap().0
|
||||
}
|
||||
|
||||
@@ -205,48 +126,3 @@ pub struct PagedRewardedSetResponse {
|
||||
pub nodes: Vec<(MixId, RewardedSetNodeStatus)>,
|
||||
pub start_next_after: Option<MixId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn percent_serde() {
|
||||
let valid_value = Percent::from_percentage_value(80).unwrap();
|
||||
let serialized = serde_json::to_string(&valid_value).unwrap();
|
||||
|
||||
println!("{}", serialized);
|
||||
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(valid_value, deserialized);
|
||||
|
||||
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
|
||||
for invalid_value in invalid_values {
|
||||
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
|
||||
Percent::from_percentage_value(95).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_to_absolute_integer() {
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 1);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 45);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 99);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
mixnet-contract-common = { path = "../mixnet-contract" }
|
||||
contracts-common = { path = "../contracts-common" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
log = "0.4"
|
||||
ts-rs = {version = "6.1.2", optional = true}
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
|
||||
use log::warn;
|
||||
use mixnet_contract_common::MixId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
@@ -42,6 +49,46 @@ impl PledgeData {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub enum PledgeCap {
|
||||
Percent(Percent),
|
||||
Absolute(Uint128), // This has to be in unym
|
||||
}
|
||||
|
||||
impl FromStr for PledgeCap {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(cap: &str) -> Result<Self, Self::Err> {
|
||||
let cap = cap.replace('_', "").replace(',', ".");
|
||||
match Percent::from_str(&cap) {
|
||||
Ok(p) => {
|
||||
if p.is_zero() {
|
||||
warn!("Pledge cap set to 0%, are you sure this is right?")
|
||||
}
|
||||
Ok(PledgeCap::Percent(p))
|
||||
}
|
||||
Err(_) => {
|
||||
match cap.parse::<u128>() {
|
||||
Ok(i) => {
|
||||
if i < 100_000_000_000 {
|
||||
warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?");
|
||||
}
|
||||
Ok(PledgeCap::Absolute(Uint128::from(i)))
|
||||
}
|
||||
Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PledgeCap {
|
||||
fn default() -> Self {
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).expect("This can never fail!"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct OriginalVestingResponse {
|
||||
pub amount: Coin,
|
||||
@@ -98,3 +145,32 @@ pub struct AllDelegationsResponse {
|
||||
pub delegations: Vec<VestingDelegation>,
|
||||
pub start_next_after: Option<(u32, MixId, u64)>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_std::Uint128;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::PledgeCap;
|
||||
|
||||
#[test]
|
||||
fn test_pledge_cap_from_str() {
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("0.1").unwrap(),
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("0,1").unwrap(),
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("100_000_000_000").unwrap(),
|
||||
PledgeCap::Absolute(Uint128::new(100_000_000_000))
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("100000000000").unwrap(),
|
||||
PledgeCap::Absolute(Uint128::new(100_000_000_000))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use cosmwasm_std::{Coin, Timestamp, Uint128};
|
||||
use cosmwasm_std::{Coin, Timestamp};
|
||||
use mixnet_contract_common::{
|
||||
mixnode::{MixNodeConfigUpdate, MixNodeCostParams},
|
||||
Gateway, MixId, MixNode,
|
||||
@@ -6,6 +6,8 @@ use mixnet_contract_common::{
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::PledgeCap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct InitMsg {
|
||||
@@ -83,6 +85,7 @@ pub enum ExecuteMsg {
|
||||
owner_address: String,
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
cap: Option<PledgeCap>,
|
||||
},
|
||||
WithdrawVestedCoins {
|
||||
amount: Coin,
|
||||
@@ -120,7 +123,8 @@ pub enum ExecuteMsg {
|
||||
to_address: Option<String>,
|
||||
},
|
||||
UpdateLockedPledgeCap {
|
||||
amount: Uint128,
|
||||
address: String,
|
||||
cap: PledgeCap,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -156,6 +160,7 @@ impl ExecuteMsg {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GetContractVersion {},
|
||||
LockedCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
@@ -201,7 +206,6 @@ pub enum QueryMsg {
|
||||
GetCurrentVestingPeriod {
|
||||
address: String,
|
||||
},
|
||||
GetLockedPledgeCap {},
|
||||
GetDelegationTimes {
|
||||
address: String,
|
||||
mix_id: MixId,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "logging"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.0"
|
||||
pretty_env_logger = "0.4.0"
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// I'd argue we should start transitioning from `log` to `tracing`
|
||||
pub fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("handlebars", log::LevelFilter::Warn)
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
@@ -116,7 +116,10 @@ impl SphinxPacketProcessor {
|
||||
trace!("received an ack packet!");
|
||||
Ok((None, data))
|
||||
}
|
||||
PacketSize::RegularPacket | PacketSize::ExtendedPacket => {
|
||||
PacketSize::RegularPacket
|
||||
| PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32 => {
|
||||
trace!("received a normal packet!");
|
||||
let (ack_data, message) = self.split_hop_data_into_ack_and_message(data)?;
|
||||
let (ack_first_hop, ack_packet) = SurbAck::try_recover_first_hop_packet(&ack_data)?;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
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.
|
||||
@@ -85,7 +85,7 @@ impl SecretKey {
|
||||
// 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) as usize * 32);
|
||||
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());
|
||||
@@ -162,7 +162,7 @@ impl TryFrom<&[u8]> for VerificationKey {
|
||||
let mut beta_g1_end: u64 = 0;
|
||||
for i in 0..betas_len {
|
||||
let start = (104 + i * 48) as usize;
|
||||
let end = (start + 48) as usize;
|
||||
let end = start + 48;
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g1_projective(
|
||||
&beta_i_bytes,
|
||||
@@ -178,7 +178,7 @@ impl TryFrom<&[u8]> for VerificationKey {
|
||||
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) as usize;
|
||||
let end = start + 96;
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g2_projective(
|
||||
&beta_i_bytes,
|
||||
|
||||
@@ -94,10 +94,10 @@ fn prepare_unlinked_fragmented_set(
|
||||
|
||||
for i in 1..(pre_casted_frags + 1) {
|
||||
// we can't use u8 directly here as upper (NON-INCLUSIVE, so it would always fit) bound could be u8::max_value() + 1
|
||||
let lb = (i as usize - 1) * unlinked_fragment_payload_max_len(max_plaintext_size);
|
||||
let lb = (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size);
|
||||
let ub = usize::min(
|
||||
message.len(),
|
||||
i as usize * unlinked_fragment_payload_max_len(max_plaintext_size),
|
||||
i * unlinked_fragment_payload_max_len(max_plaintext_size),
|
||||
);
|
||||
fragments.push(
|
||||
Fragment::try_new(
|
||||
|
||||
@@ -25,8 +25,10 @@ impl Display for MixPacketFormattingError {
|
||||
InvalidPacketSize(actual) =>
|
||||
write!(
|
||||
f,
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size()
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
|
||||
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
|
||||
PacketSize::ExtendedPacket32.size()
|
||||
),
|
||||
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
|
||||
InvalidPacketMode => write!(f, "provided packet mode is invalid")
|
||||
|
||||
@@ -6,7 +6,6 @@ use bytes::{Buf, BufMut, BytesMut};
|
||||
use nymsphinx_params::packet_modes::InvalidPacketMode;
|
||||
use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
|
||||
use nymsphinx_types::SphinxPacket;
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
@@ -75,7 +74,7 @@ impl Decoder for SphinxCodec {
|
||||
if src.is_empty() {
|
||||
// can't do anything if we have no bytes, but let's reserve enough for the most
|
||||
// conservative case, i.e. receiving an ack packet
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ impl Decoder for SphinxCodec {
|
||||
};
|
||||
|
||||
let sphinx_packet_size = header.packet_size.size();
|
||||
let frame_len = Header::SIZE + sphinx_packet_size;
|
||||
let frame_len = header.size() + sphinx_packet_size;
|
||||
|
||||
if src.len() < frame_len {
|
||||
// we don't have enough bytes to read the rest of frame
|
||||
@@ -96,7 +95,7 @@ impl Decoder for SphinxCodec {
|
||||
}
|
||||
|
||||
// advance buffer past the header - at this point we have enough bytes
|
||||
src.advance(Header::SIZE);
|
||||
src.advance(header.size());
|
||||
let sphinx_packet_bytes = src.split_to(sphinx_packet_size);
|
||||
let sphinx_packet = match SphinxPacket::from_bytes(&sphinx_packet_bytes) {
|
||||
Ok(sphinx_packet) => sphinx_packet,
|
||||
@@ -115,21 +114,27 @@ impl Decoder for SphinxCodec {
|
||||
// has appropriate capacity in anticipation of future calls to decode.
|
||||
// Failing to do so leads to inefficiency.
|
||||
|
||||
// if we have at least one more byte available, we can reserve enough bytes for
|
||||
// if we have enough bytes to decode the header of the next packet, we can reserve enough bytes for
|
||||
// the entire next frame, if not, we assume the next frame is an ack packet and
|
||||
// reserve for that.
|
||||
// we also assume the next packet coming from the same client will use exactly the same versioning
|
||||
// as the current packet
|
||||
let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size();
|
||||
if !src.is_empty() {
|
||||
let next_packet_len = match PacketSize::try_from(src[0]) {
|
||||
Ok(next_packet_len) => next_packet_len,
|
||||
match Header::decode(src) {
|
||||
Ok(Some(next_header)) => {
|
||||
allocate_for_next_packet = next_header.size() + next_header.packet_size.size();
|
||||
}
|
||||
Ok(None) => {
|
||||
// we don't have enough information to know how much to reserve, fallback to the ack case
|
||||
}
|
||||
|
||||
// the next frame will be malformed but let's leave handling the error to the next
|
||||
// call to 'decode', as presumably, the current sphinx packet is still valid
|
||||
Err(_) => return Ok(Some(nymsphinx_packet)),
|
||||
};
|
||||
let next_frame_len = next_packet_len.size() + Header::SIZE;
|
||||
src.reserve(next_frame_len - 1);
|
||||
} else {
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
}
|
||||
src.reserve(allocate_for_next_packet);
|
||||
|
||||
Ok(Some(nymsphinx_packet))
|
||||
}
|
||||
@@ -199,6 +204,8 @@ mod packet_encoding {
|
||||
#[cfg(test)]
|
||||
mod decode_will_allocate_enough_bytes_for_next_call {
|
||||
use super::*;
|
||||
use nymsphinx_params::packet_version::PacketVersion;
|
||||
use nymsphinx_params::PacketMode;
|
||||
|
||||
#[test]
|
||||
fn for_empty_bytes() {
|
||||
@@ -207,20 +214,23 @@ mod packet_encoding {
|
||||
assert!(SphinxCodec.decode(&mut empty_bytes).unwrap().is_none());
|
||||
assert_eq!(
|
||||
empty_bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_bytes_with_header() {
|
||||
fn for_bytes_with_legacy_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
@@ -228,12 +238,60 @@ mod packet_encoding {
|
||||
header.encode(&mut bytes);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
|
||||
assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame() {
|
||||
fn for_bytes_with_versioned_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::VERSIONED_SIZE + packet_size.size()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_legacy_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size: Default::default(),
|
||||
packet_mode: Default::default(),
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(packet, &mut bytes).unwrap();
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_versioned_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedSphinxPacket {
|
||||
header: Header::default(),
|
||||
@@ -245,17 +303,50 @@ mod packet_encoding {
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::VERSIONED_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_byte() {
|
||||
// if there was at least 1 byte left, there should be enough space for entire next frame
|
||||
fn for_full_frame_with_extra_bytes_with_legacy_header() {
|
||||
// if there was at least 2 byte left, there should be enough space for entire next frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
let first_packet = FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size: Default::default(),
|
||||
packet_mode: Default::default(),
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketMode::default() as u8);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_bytes_with_versioned_header() {
|
||||
// if there was at least 3 byte left, there should be enough space for entire next frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
@@ -266,10 +357,12 @@ mod packet_encoding {
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap());
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketMode::default() as u8);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
assert!(bytes.capacity() >= Header::SIZE + packet_size.size())
|
||||
assert!(bytes.capacity() >= Header::VERSIONED_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::codec::SphinxCodecError;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nymsphinx_params::packet_sizes::PacketSize;
|
||||
use nymsphinx_params::packet_version::PacketVersion;
|
||||
use nymsphinx_params::PacketMode;
|
||||
use nymsphinx_types::SphinxPacket;
|
||||
use std::convert::TryFrom;
|
||||
@@ -17,12 +18,14 @@ pub struct FramedSphinxPacket {
|
||||
}
|
||||
|
||||
impl FramedSphinxPacket {
|
||||
pub fn new(packet: SphinxPacket, packet_mode: PacketMode) -> Self {
|
||||
pub fn new(packet: SphinxPacket, packet_mode: PacketMode, use_legacy_version: bool) -> Self {
|
||||
// If this fails somebody is using the library in a super incorrect way, because they
|
||||
// already managed to somehow create a sphinx packet
|
||||
let packet_size = PacketSize::get_type(packet.len()).unwrap();
|
||||
|
||||
FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::new(use_legacy_version),
|
||||
packet_size,
|
||||
packet_mode,
|
||||
},
|
||||
@@ -48,6 +51,9 @@ impl FramedSphinxPacket {
|
||||
// but would that really be worth it?
|
||||
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct Header {
|
||||
/// Represents the wire format version used to construct this packet.
|
||||
pub(crate) packet_version: PacketVersion,
|
||||
|
||||
/// Represents type and consequently size of the included SphinxPacket.
|
||||
pub(crate) packet_size: PacketSize,
|
||||
|
||||
@@ -64,11 +70,25 @@ pub struct Header {
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub(crate) const SIZE: usize = 2;
|
||||
pub(crate) const LEGACY_SIZE: usize = 2;
|
||||
pub(crate) const VERSIONED_SIZE: usize = 3;
|
||||
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
if self.packet_version.is_legacy() {
|
||||
Self::LEGACY_SIZE
|
||||
} else {
|
||||
Self::VERSIONED_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
// we reserve one byte for `packet_size` and the other for `mode`
|
||||
dst.reserve(Self::SIZE);
|
||||
dst.reserve(Self::LEGACY_SIZE);
|
||||
if let Some(version) = self.packet_version.as_u8() {
|
||||
dst.reserve(Self::VERSIONED_SIZE);
|
||||
dst.put_u8(version)
|
||||
}
|
||||
|
||||
dst.put_u8(self.packet_size as u8);
|
||||
dst.put_u8(self.packet_mode as u8);
|
||||
// reserve bytes for the actual packet
|
||||
@@ -76,16 +96,30 @@ impl Header {
|
||||
}
|
||||
|
||||
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, SphinxCodecError> {
|
||||
if src.len() < Self::SIZE {
|
||||
if src.len() < Self::LEGACY_SIZE {
|
||||
// can't do anything if we don't have enough bytes - but reserve enough for the next call
|
||||
src.reserve(Self::SIZE);
|
||||
src.reserve(Self::LEGACY_SIZE);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(Header {
|
||||
packet_size: PacketSize::try_from(src[0])?,
|
||||
packet_mode: PacketMode::try_from(src[1])?,
|
||||
}))
|
||||
let packet_version = PacketVersion::from(src[0]);
|
||||
if packet_version.is_legacy() {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[0])?,
|
||||
packet_mode: PacketMode::try_from(src[1])?,
|
||||
}))
|
||||
} else if src.len() < Self::VERSIONED_SIZE {
|
||||
// we're missing that 1 byte to read the full header...
|
||||
src.reserve(Self::VERSIONED_SIZE);
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[1])?,
|
||||
packet_mode: PacketMode::try_from(src[2])?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +142,16 @@ mod header_encoding {
|
||||
// make sure this is still 'unknown' for if we make changes in the future
|
||||
assert!(PacketSize::try_from(unknown_packet_size).is_err());
|
||||
|
||||
let mut bytes = BytesMut::from([unknown_packet_size, PacketMode::default() as u8].as_ref());
|
||||
// unfortunately this will only work for the 'versioned' variant
|
||||
// due to the hack used to get legacy mode compatibility
|
||||
let mut bytes = BytesMut::from(
|
||||
[
|
||||
PacketVersion::new_versioned(123).as_u8().unwrap(),
|
||||
unknown_packet_size,
|
||||
PacketMode::default() as u8,
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert!(Header::decode(&mut bytes).is_err())
|
||||
}
|
||||
|
||||
@@ -127,23 +170,47 @@ mod header_encoding {
|
||||
let mut empty_bytes = BytesMut::new();
|
||||
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_1.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
|
||||
let mut empty_bytes = BytesMut::with_capacity(1);
|
||||
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_2.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet() {
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert_eq!(bytes.capacity(), bytes.len() + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ pub use packet_sizes::PacketSize;
|
||||
|
||||
pub mod packet_modes;
|
||||
pub mod packet_sizes;
|
||||
pub mod packet_version;
|
||||
|
||||
// If somebody can provide an argument why it might be reasonable to have more than 255 mix hops,
|
||||
// I will change this to [`usize`]
|
||||
@@ -24,6 +25,17 @@ pub const DEFAULT_NUM_MIX_HOPS: u8 = 3;
|
||||
pub const FRAG_ID_LEN: usize = 5;
|
||||
pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN];
|
||||
|
||||
// wait, wait, but why are we starting with version 7?
|
||||
// when packet header gets serialized, the following bytes (in that order) are put onto the wire:
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_mode
|
||||
// it also just so happens that the only valid values for packet_size indicator include values 1-6
|
||||
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
|
||||
// otherwise we should treat it as legacy
|
||||
/// Increment it whenever we perform any breaking change in the wire format!
|
||||
const CURRENT_PACKET_VERSION_NUMBER: u8 = 7;
|
||||
|
||||
// TODO: ask @AP about the choice of below algorithms
|
||||
|
||||
/// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload.
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::FRAG_ID_LEN;
|
||||
use nymsphinx_types::header::HEADER_SIZE;
|
||||
use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE;
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
// it's up to the smart people to figure those values out : )
|
||||
const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024;
|
||||
@@ -15,11 +16,16 @@ const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 102
|
||||
const ACK_IV_SIZE: usize = 16;
|
||||
|
||||
const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + ACK_IV_SIZE + FRAG_ID_LEN;
|
||||
const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_8: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 8 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_16: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 16 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_32: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidPacketSize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidExtendedPacketSize;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PacketSize {
|
||||
@@ -30,7 +36,28 @@ pub enum PacketSize {
|
||||
AckPacket = 2,
|
||||
|
||||
// for example for streaming fast and furious in uncompressed 10bit 4K HDR quality
|
||||
ExtendedPacket = 3,
|
||||
ExtendedPacket32 = 3,
|
||||
|
||||
// for example for streaming fast and furious in heavily compressed lossy RealPlayer quality
|
||||
ExtendedPacket8 = 4,
|
||||
|
||||
// for example for streaming fast and furious in compressed XviD quality
|
||||
ExtendedPacket16 = 5,
|
||||
}
|
||||
|
||||
impl FromStr for PacketSize {
|
||||
type Err = InvalidPacketSize;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"regular" => Ok(Self::RegularPacket),
|
||||
"ack" => Ok(Self::AckPacket),
|
||||
"extended8" => Ok(Self::ExtendedPacket8),
|
||||
"extended16" => Ok(Self::ExtendedPacket16),
|
||||
"extended32" => Ok(Self::ExtendedPacket32),
|
||||
_ => Err(InvalidPacketSize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PacketSize {
|
||||
@@ -40,7 +67,9 @@ impl TryFrom<u8> for PacketSize {
|
||||
match value {
|
||||
_ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket),
|
||||
_ if value == (PacketSize::AckPacket as u8) => Ok(Self::AckPacket),
|
||||
_ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket),
|
||||
_ if value == (PacketSize::ExtendedPacket8 as u8) => Ok(Self::ExtendedPacket8),
|
||||
_ if value == (PacketSize::ExtendedPacket16 as u8) => Ok(Self::ExtendedPacket16),
|
||||
_ if value == (PacketSize::ExtendedPacket32 as u8) => Ok(Self::ExtendedPacket32),
|
||||
_ => Err(InvalidPacketSize),
|
||||
}
|
||||
}
|
||||
@@ -51,7 +80,9 @@ impl PacketSize {
|
||||
match self {
|
||||
PacketSize::RegularPacket => REGULAR_PACKET_SIZE,
|
||||
PacketSize::AckPacket => ACK_PACKET_SIZE,
|
||||
PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE,
|
||||
PacketSize::ExtendedPacket8 => EXTENDED_PACKET_SIZE_8,
|
||||
PacketSize::ExtendedPacket16 => EXTENDED_PACKET_SIZE_16,
|
||||
PacketSize::ExtendedPacket32 => EXTENDED_PACKET_SIZE_32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +99,33 @@ impl PacketSize {
|
||||
Ok(PacketSize::RegularPacket)
|
||||
} else if PacketSize::AckPacket.size() == size {
|
||||
Ok(PacketSize::AckPacket)
|
||||
} else if PacketSize::ExtendedPacket.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket)
|
||||
} else if PacketSize::ExtendedPacket8.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket8)
|
||||
} else if PacketSize::ExtendedPacket16.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket16)
|
||||
} else if PacketSize::ExtendedPacket32.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket32)
|
||||
} else {
|
||||
Err(InvalidPacketSize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_extended_size(&self) -> bool {
|
||||
match self {
|
||||
PacketSize::RegularPacket | PacketSize::AckPacket => false,
|
||||
PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32 => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_extended_size(self) -> Option<Self> {
|
||||
if self.is_extended_size() {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PacketSize {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PacketVersion {
|
||||
// this will allow updated mixnodes to still understand packets from before the update
|
||||
Legacy,
|
||||
Versioned(u8),
|
||||
}
|
||||
|
||||
impl PacketVersion {
|
||||
pub fn new(use_legacy: bool) -> Self {
|
||||
if use_legacy {
|
||||
Self::new_legacy()
|
||||
} else {
|
||||
Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_legacy() -> Self {
|
||||
PacketVersion::Legacy
|
||||
}
|
||||
|
||||
pub fn new_versioned(version: u8) -> Self {
|
||||
PacketVersion::Versioned(version)
|
||||
}
|
||||
|
||||
pub fn is_legacy(&self) -> bool {
|
||||
matches!(self, PacketVersion::Legacy)
|
||||
}
|
||||
|
||||
pub fn as_u8(&self) -> Option<u8> {
|
||||
match self {
|
||||
PacketVersion::Legacy => None,
|
||||
PacketVersion::Versioned(version) => Some(*version),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PacketVersion {
|
||||
fn from(v: u8) -> Self {
|
||||
match v {
|
||||
n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy,
|
||||
n => PacketVersion::Versioned(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PacketVersion {
|
||||
fn default() -> Self {
|
||||
PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,13 @@ pub struct OrderedMessageBuffer {
|
||||
messages: HashMap<u64, OrderedMessage>,
|
||||
}
|
||||
|
||||
/// Data returned from `OrderedMessageBuffer` on a successful read of gapless ordered data.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ReadContiguousData {
|
||||
pub data: Vec<u8>,
|
||||
pub last_index: u64,
|
||||
}
|
||||
|
||||
impl OrderedMessageBuffer {
|
||||
pub fn new() -> OrderedMessageBuffer {
|
||||
OrderedMessageBuffer {
|
||||
@@ -42,7 +49,7 @@ impl OrderedMessageBuffer {
|
||||
/// a read will return the bytes of messages 0, 1, 2. Subsequent reads will
|
||||
/// return `None` until message 3 comes in, at which point 3, 4, and any
|
||||
/// further contiguous messages which have arrived will be returned.
|
||||
pub fn read(&mut self) -> Option<Vec<u8>> {
|
||||
pub fn read(&mut self) -> Option<ReadContiguousData> {
|
||||
if !self.messages.contains_key(&self.next_index) {
|
||||
return None;
|
||||
}
|
||||
@@ -66,7 +73,10 @@ impl OrderedMessageBuffer {
|
||||
.collect();
|
||||
|
||||
trace!("Returning {} bytes from ordered message buffer", data.len());
|
||||
Some(data)
|
||||
Some(ReadContiguousData {
|
||||
data,
|
||||
last_index: index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,11 +112,11 @@ mod test_chunking_and_reassembling {
|
||||
};
|
||||
|
||||
buffer.write(first_message);
|
||||
let first_read = buffer.read().unwrap();
|
||||
let first_read = buffer.read().unwrap().data;
|
||||
assert_eq!(vec![1, 2, 3, 4], first_read);
|
||||
|
||||
buffer.write(second_message);
|
||||
let second_read = buffer.read().unwrap();
|
||||
let second_read = buffer.read().unwrap().data;
|
||||
assert_eq!(vec![5, 6, 7, 8], second_read);
|
||||
|
||||
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
|
||||
@@ -128,7 +138,7 @@ mod test_chunking_and_reassembling {
|
||||
buffer.write(first_message);
|
||||
buffer.write(second_message);
|
||||
let second_read = buffer.read();
|
||||
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap());
|
||||
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap().data);
|
||||
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
|
||||
}
|
||||
|
||||
@@ -147,8 +157,8 @@ mod test_chunking_and_reassembling {
|
||||
|
||||
buffer.write(second_message);
|
||||
buffer.write(first_message);
|
||||
let read = buffer.read();
|
||||
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read.unwrap());
|
||||
let read = buffer.read().unwrap().data;
|
||||
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read);
|
||||
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
|
||||
}
|
||||
}
|
||||
@@ -182,7 +192,7 @@ mod test_chunking_and_reassembling {
|
||||
#[test]
|
||||
fn everything_up_to_the_indexing_gap_is_returned() {
|
||||
let mut buffer = setup();
|
||||
let ordered_bytes = buffer.read().unwrap();
|
||||
let ordered_bytes = buffer.read().unwrap().data;
|
||||
assert_eq!([0, 0, 0, 0, 1, 1, 1, 1].to_vec(), ordered_bytes);
|
||||
|
||||
// we shouldn't get any more from a second attempt if nothing is added
|
||||
@@ -208,7 +218,7 @@ mod test_chunking_and_reassembling {
|
||||
};
|
||||
buffer.write(two_message);
|
||||
|
||||
let more_ordered_bytes = buffer.read().unwrap();
|
||||
let more_ordered_bytes = buffer.read().unwrap().data;
|
||||
assert_eq!([2, 2, 2, 2, 3, 3, 3, 3].to_vec(), more_ordered_bytes);
|
||||
|
||||
// let's add another message
|
||||
@@ -227,7 +237,10 @@ mod test_chunking_and_reassembling {
|
||||
};
|
||||
buffer.write(four_message);
|
||||
|
||||
assert_eq!([4, 4, 4, 4, 5, 5, 5, 5].to_vec(), buffer.read().unwrap());
|
||||
assert_eq!(
|
||||
[4, 4, 4, 4, 5, 5, 5, 5].to_vec(),
|
||||
buffer.read().unwrap().data
|
||||
);
|
||||
|
||||
// at this point we should again get back nothing if we try a read
|
||||
assert_eq!(None, buffer.read());
|
||||
|
||||
@@ -2,7 +2,7 @@ mod buffer;
|
||||
mod message;
|
||||
mod sender;
|
||||
|
||||
pub use buffer::OrderedMessageBuffer;
|
||||
pub use buffer::{OrderedMessageBuffer, ReadContiguousData};
|
||||
pub use message::MessageError;
|
||||
pub use message::OrderedMessage;
|
||||
pub use sender::OrderedMessageSender;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer};
|
||||
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData};
|
||||
use socks5_requests::ConnectionId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use task::ShutdownListener;
|
||||
@@ -38,12 +38,13 @@ pub enum ControllerCommand {
|
||||
|
||||
struct ActiveConnection {
|
||||
is_closed: bool,
|
||||
closed_at_index: Option<u64>,
|
||||
connection_sender: Option<ConnectionSender>,
|
||||
ordered_buffer: OrderedMessageBuffer,
|
||||
}
|
||||
|
||||
impl ActiveConnection {
|
||||
fn write_to_buf(&mut self, payload: Vec<u8>) {
|
||||
fn write_to_buf(&mut self, payload: Vec<u8>, is_closed: bool) {
|
||||
let ordered_message = match OrderedMessage::try_from_bytes(payload) {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
@@ -51,10 +52,13 @@ impl ActiveConnection {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if is_closed {
|
||||
self.closed_at_index = Some(ordered_message.index);
|
||||
}
|
||||
self.ordered_buffer.write(ordered_message);
|
||||
}
|
||||
|
||||
fn read_from_buf(&mut self) -> Option<Vec<u8>> {
|
||||
fn read_from_buf(&mut self) -> Option<ReadContiguousData> {
|
||||
self.ordered_buffer.read()
|
||||
}
|
||||
}
|
||||
@@ -99,6 +103,7 @@ impl Controller {
|
||||
is_closed: false,
|
||||
connection_sender: Some(connection_sender),
|
||||
ordered_buffer: OrderedMessageBuffer::new(),
|
||||
closed_at_index: None,
|
||||
};
|
||||
if let Some(_active_conn) = self.active_connections.insert(conn_id, active_connection) {
|
||||
error!("Received a duplicate 'Connect'!")
|
||||
@@ -127,21 +132,23 @@ impl Controller {
|
||||
fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) {
|
||||
if let Some(active_connection) = self.active_connections.get_mut(&conn_id) {
|
||||
if !payload.is_empty() {
|
||||
active_connection.write_to_buf(payload);
|
||||
active_connection.write_to_buf(payload, is_closed);
|
||||
} else if !is_closed {
|
||||
error!("Tried to write an empty message to a not-closing connection. Please let us know if you see this message");
|
||||
}
|
||||
// if messages get unordered, make sure we don't lose information about
|
||||
// remote socket getting closed!
|
||||
active_connection.is_closed |= is_closed;
|
||||
|
||||
if let Some(payload) = active_connection.read_from_buf() {
|
||||
if let Some(closed_at_index) = active_connection.closed_at_index {
|
||||
if payload.last_index > closed_at_index {
|
||||
active_connection.is_closed = true;
|
||||
}
|
||||
}
|
||||
if let Err(err) = active_connection
|
||||
.connection_sender
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.unbounded_send(ConnectionMessage {
|
||||
payload,
|
||||
payload: payload.data,
|
||||
socket_closed: active_connection.is_closed,
|
||||
})
|
||||
{
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct DelegationWithEverything {
|
||||
|
||||
// DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT!
|
||||
pub pending_events: Vec<DelegationEvent>,
|
||||
pub mixnode_is_unbonding: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
|
||||
@@ -5,11 +5,11 @@ use crate::currency::{DecCoin, RegisteredCoins};
|
||||
use crate::error::TypesError;
|
||||
use crate::mixnode::MixNodeCostParams;
|
||||
use mixnet_contract_common::{
|
||||
EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId,
|
||||
BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId,
|
||||
PendingEpochEvent as MixnetContractPendingEpochEvent,
|
||||
PendingEpochEventData as MixnetContractPendingEpochEventData,
|
||||
PendingEpochEventKind as MixnetContractPendingEpochEventKind,
|
||||
PendingIntervalEvent as MixnetContractPendingIntervalEvent,
|
||||
PendingIntervalEventData as MixnetContractPendingIntervalEventData,
|
||||
PendingIntervalEventKind as MixnetContractPendingIntervalEventKind,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct PendingEpochEvent {
|
||||
pub id: EpochEventId,
|
||||
pub created_at: BlockHeight,
|
||||
pub event: PendingEpochEventData,
|
||||
}
|
||||
|
||||
@@ -32,7 +33,8 @@ impl PendingEpochEvent {
|
||||
) -> Result<Self, TypesError> {
|
||||
Ok(PendingEpochEvent {
|
||||
id: pending_event.id,
|
||||
event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event, reg)?,
|
||||
created_at: pending_event.event.created_at,
|
||||
event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event.kind, reg)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,11 +67,11 @@ pub enum PendingEpochEventData {
|
||||
|
||||
impl PendingEpochEventData {
|
||||
pub fn try_from_mixnet_contract(
|
||||
pending_event: MixnetContractPendingEpochEventData,
|
||||
pending_event: MixnetContractPendingEpochEventKind,
|
||||
reg: &RegisteredCoins,
|
||||
) -> Result<Self, TypesError> {
|
||||
match pending_event {
|
||||
MixnetContractPendingEpochEventData::Delegate {
|
||||
MixnetContractPendingEpochEventKind::Delegate {
|
||||
owner,
|
||||
mix_id,
|
||||
amount,
|
||||
@@ -80,7 +82,7 @@ impl PendingEpochEventData {
|
||||
amount: reg.attempt_convert_to_display_dec_coin(amount.into())?,
|
||||
proxy: proxy.map(|p| p.into_string()),
|
||||
}),
|
||||
MixnetContractPendingEpochEventData::Undelegate {
|
||||
MixnetContractPendingEpochEventKind::Undelegate {
|
||||
owner,
|
||||
mix_id,
|
||||
proxy,
|
||||
@@ -89,10 +91,10 @@ impl PendingEpochEventData {
|
||||
mix_id,
|
||||
proxy: proxy.map(|p| p.into_string()),
|
||||
}),
|
||||
MixnetContractPendingEpochEventData::UnbondMixnode { mix_id } => {
|
||||
MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => {
|
||||
Ok(PendingEpochEventData::UnbondMixnode { mix_id })
|
||||
}
|
||||
MixnetContractPendingEpochEventData::UpdateActiveSetSize { new_size } => {
|
||||
MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => {
|
||||
Ok(PendingEpochEventData::UpdateActiveSetSize { new_size })
|
||||
}
|
||||
}
|
||||
@@ -107,6 +109,7 @@ impl PendingEpochEventData {
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct PendingIntervalEvent {
|
||||
pub id: IntervalEventId,
|
||||
pub created_at: BlockHeight,
|
||||
pub event: PendingIntervalEventData,
|
||||
}
|
||||
|
||||
@@ -117,7 +120,11 @@ impl PendingIntervalEvent {
|
||||
) -> Result<Self, TypesError> {
|
||||
Ok(PendingIntervalEvent {
|
||||
id: pending_event.id,
|
||||
event: PendingIntervalEventData::try_from_mixnet_contract(pending_event.event, reg)?,
|
||||
created_at: pending_event.event.created_at,
|
||||
event: PendingIntervalEventData::try_from_mixnet_contract(
|
||||
pending_event.event.kind,
|
||||
reg,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -145,11 +152,11 @@ pub enum PendingIntervalEventData {
|
||||
|
||||
impl PendingIntervalEventData {
|
||||
pub fn try_from_mixnet_contract(
|
||||
pending_event: MixnetContractPendingIntervalEventData,
|
||||
pending_event: MixnetContractPendingIntervalEventKind,
|
||||
reg: &RegisteredCoins,
|
||||
) -> Result<Self, TypesError> {
|
||||
match pending_event {
|
||||
MixnetContractPendingIntervalEventData::ChangeMixCostParams { mix_id, new_costs } => {
|
||||
MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => {
|
||||
Ok(PendingIntervalEventData::ChangeMixCostParams {
|
||||
mix_id,
|
||||
new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params(
|
||||
@@ -157,10 +164,10 @@ impl PendingIntervalEventData {
|
||||
)?,
|
||||
})
|
||||
}
|
||||
MixnetContractPendingIntervalEventData::UpdateRewardingParams { update } => {
|
||||
MixnetContractPendingIntervalEventKind::UpdateRewardingParams { update } => {
|
||||
Ok(PendingIntervalEventData::UpdateRewardingParams { update })
|
||||
}
|
||||
MixnetContractPendingIntervalEventData::UpdateIntervalConfig {
|
||||
MixnetContractPendingIntervalEventKind::UpdateIntervalConfig {
|
||||
epochs_in_interval,
|
||||
epoch_duration_secs,
|
||||
} => Ok(PendingIntervalEventData::UpdateIntervalConfig {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- vesting-contract: added query for obtaining contract build information ([#1726])
|
||||
|
||||
[#1726]: https://github.com/nymtech/nym/pull/1726
|
||||
|
||||
|
||||
## [nym-contracts-v1.0.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.2) (2022-09-13)
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+6
@@ -221,7 +221,9 @@ name = "contracts-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1606,12 +1608,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
name = "vesting-contract"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen",
|
||||
"vesting-contract-common",
|
||||
]
|
||||
|
||||
@@ -1619,7 +1623,9 @@ dependencies = [
|
||||
name = "vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
@@ -63,7 +63,7 @@ pub fn instantiate(
|
||||
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
|
||||
let reward_params = msg
|
||||
.initial_rewarding_params
|
||||
.into_rewarding_params(msg.epochs_in_interval);
|
||||
.into_rewarding_params(msg.epochs_in_interval)?;
|
||||
|
||||
interval_storage::initialise_storage(deps.storage, starting_interval)?;
|
||||
mixnet_params_storage::initialise_storage(deps.storage, state)?;
|
||||
@@ -169,17 +169,19 @@ pub fn execute(
|
||||
owner_signature,
|
||||
),
|
||||
ExecuteMsg::UnbondMixnode {} => {
|
||||
crate::mixnodes::transactions::try_remove_mixnode(deps, info)
|
||||
crate::mixnodes::transactions::try_remove_mixnode(deps, env, info)
|
||||
}
|
||||
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
|
||||
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner)
|
||||
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, env, info, owner)
|
||||
}
|
||||
ExecuteMsg::UpdateMixnodeCostParams { new_costs } => {
|
||||
crate::mixnodes::transactions::try_update_mixnode_cost_params(deps, info, new_costs)
|
||||
crate::mixnodes::transactions::try_update_mixnode_cost_params(
|
||||
deps, env, info, new_costs,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => {
|
||||
crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf(
|
||||
deps, info, new_costs, owner,
|
||||
deps, env, info, new_costs, owner,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::UpdateMixnodeConfig { new_config } => {
|
||||
@@ -223,19 +225,21 @@ pub fn execute(
|
||||
|
||||
// delegation-related:
|
||||
ExecuteMsg::DelegateToMixnode { mix_id } => {
|
||||
crate::delegations::transactions::try_delegate_to_mixnode(deps, info, mix_id)
|
||||
crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id)
|
||||
}
|
||||
ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => {
|
||||
crate::delegations::transactions::try_delegate_to_mixnode_on_behalf(
|
||||
deps, info, mix_id, delegate,
|
||||
deps, env, info, mix_id, delegate,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromMixnode { mix_id } => {
|
||||
crate::delegations::transactions::try_remove_delegation_from_mixnode(deps, info, mix_id)
|
||||
crate::delegations::transactions::try_remove_delegation_from_mixnode(
|
||||
deps, env, info, mix_id,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => {
|
||||
crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf(
|
||||
deps, info, mix_id, delegate,
|
||||
deps, env, info, mix_id, delegate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -493,6 +497,7 @@ mod tests {
|
||||
initial_rewarding_params: InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
|
||||
initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(1).unwrap(),
|
||||
@@ -531,6 +536,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget: expected_epoch_reward_budget,
|
||||
stake_saturation_point: expected_stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
|
||||
|
||||
@@ -453,6 +453,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_retrieved_delegations_are_from_the_specified_delegator() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
// it means we have, for example, delegation from "delegator1" towards mix1, mix2, ...., from "delegator2" towards mix1, mix2, ...., etc
|
||||
add_dummy_mixes_with_delegations(&mut test, 50, 100);
|
||||
|
||||
@@ -462,6 +463,7 @@ mod tests {
|
||||
for mix_id in 1..=25 {
|
||||
try_delegate_to_mixnode_on_behalf(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info(vesting_contract.as_ref(), &[coin(100_000, TEST_COIN_DENOM)]),
|
||||
mix_id,
|
||||
with_proxy.into(),
|
||||
|
||||
@@ -6,34 +6,37 @@ use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::support::helpers::validate_delegation_stake;
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response};
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
new_pending_delegation_event, new_pending_undelegation_event,
|
||||
};
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventData;
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventKind;
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
|
||||
pub(crate) fn try_delegate_to_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
_try_delegate_to_mixnode(deps, mix_id, info.sender, info.funds, None)
|
||||
_try_delegate_to_mixnode(deps, env, mix_id, info.sender, info.funds, None)
|
||||
}
|
||||
|
||||
pub(crate) fn try_delegate_to_mixnode_on_behalf(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
delegate: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let delegate = deps.api.addr_validate(&delegate)?;
|
||||
_try_delegate_to_mixnode(deps, mix_id, delegate, info.funds, Some(info.sender))
|
||||
_try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender))
|
||||
}
|
||||
|
||||
pub(crate) fn _try_delegate_to_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
mix_id: MixId,
|
||||
delegate: Addr,
|
||||
amount: Vec<Coin>,
|
||||
@@ -59,37 +62,40 @@ pub(crate) fn _try_delegate_to_mixnode(
|
||||
// push the event onto the queue and wait for it to be picked up at the end of the epoch
|
||||
let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id);
|
||||
|
||||
let epoch_event = PendingEpochEventData::Delegate {
|
||||
let epoch_event = PendingEpochEventKind::Delegate {
|
||||
owner: delegate,
|
||||
mix_id,
|
||||
amount: delegation,
|
||||
proxy,
|
||||
};
|
||||
interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?;
|
||||
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
|
||||
|
||||
Ok(Response::new().add_event(cosmos_event))
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_delegation_from_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
_try_remove_delegation_from_mixnode(deps, mix_id, info.sender, None)
|
||||
_try_remove_delegation_from_mixnode(deps, env, mix_id, info.sender, None)
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
delegate: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let delegate = deps.api.addr_validate(&delegate)?;
|
||||
_try_remove_delegation_from_mixnode(deps, mix_id, delegate, Some(info.sender))
|
||||
_try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender))
|
||||
}
|
||||
|
||||
pub(crate) fn _try_remove_delegation_from_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
mix_id: MixId,
|
||||
delegate: Addr,
|
||||
proxy: Option<Addr>,
|
||||
@@ -111,12 +117,12 @@ pub(crate) fn _try_remove_delegation_from_mixnode(
|
||||
// push the event onto the queue and wait for it to be picked up at the end of the epoch
|
||||
let cosmos_event = new_pending_undelegation_event(&delegate, &proxy, mix_id);
|
||||
|
||||
let epoch_event = PendingEpochEventData::Undelegate {
|
||||
let epoch_event = PendingEpochEventKind::Undelegate {
|
||||
owner: delegate,
|
||||
mix_id,
|
||||
proxy,
|
||||
};
|
||||
interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?;
|
||||
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
|
||||
|
||||
Ok(Response::new().add_event(cosmos_event))
|
||||
}
|
||||
@@ -138,10 +144,11 @@ mod tests {
|
||||
#[test]
|
||||
fn can_only_be_done_towards_an_existing_mixnode() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "delegator";
|
||||
let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender, 42);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 })
|
||||
@@ -151,17 +158,19 @@ mod tests {
|
||||
#[test]
|
||||
fn must_contain_non_zero_amount_of_coins() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]);
|
||||
let sender2 = mock_info(owner, &[]);
|
||||
let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
|
||||
assert_eq!(res, Err(MixnetContractError::EmptyDelegation));
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id);
|
||||
assert_eq!(res, Err(MixnetContractError::EmptyDelegation));
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender3, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::WrongDenom {
|
||||
@@ -174,6 +183,8 @@ mod tests {
|
||||
#[test]
|
||||
fn if_applicable_must_contain_at_least_the_minimum_pledge() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
|
||||
@@ -188,7 +199,7 @@ mod tests {
|
||||
.save(test.deps_mut().storage, &contract_state)
|
||||
.unwrap();
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::InsufficientDelegation {
|
||||
@@ -197,13 +208,14 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id);
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_towards_fully_bonded_mixnode() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "delegator";
|
||||
let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
|
||||
|
||||
@@ -226,17 +238,33 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonded", &[])).unwrap();
|
||||
try_remove_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("mix-owner-unbonded", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
try_remove_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("mix-owner-unbonded-leftover", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
test.execute_all_pending_events();
|
||||
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap();
|
||||
try_remove_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("mix-owner-unbonding", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonding);
|
||||
let res = try_delegate_to_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_unbonding,
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::MixnodeIsUnbonding {
|
||||
@@ -244,7 +272,12 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonded);
|
||||
let res = try_delegate_to_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_unbonded,
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::MixNodeBondNotFound {
|
||||
@@ -252,7 +285,8 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender, mix_id_unbonded_leftover);
|
||||
let res =
|
||||
try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::MixNodeBondNotFound {
|
||||
@@ -264,22 +298,26 @@ mod tests {
|
||||
#[test]
|
||||
fn can_still_be_done_if_prior_delegation_exists() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
|
||||
let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// still OK
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id);
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correctly_pushes_appropriate_epoch_event() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
@@ -289,15 +327,15 @@ mod tests {
|
||||
let sender1 = mock_info(owner, &[amount1.clone()]);
|
||||
let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]);
|
||||
|
||||
try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap();
|
||||
try_delegate_to_mixnode_on_behalf(test.deps_mut(), sender2, mix_id, owner.into())
|
||||
try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap();
|
||||
try_delegate_to_mixnode_on_behalf(test.deps_mut(), env, sender2, mix_id, owner.into())
|
||||
.unwrap();
|
||||
|
||||
let events = test.pending_epoch_events();
|
||||
|
||||
assert_eq!(
|
||||
events[0],
|
||||
PendingEpochEventData::Delegate {
|
||||
events[0].kind,
|
||||
PendingEpochEventKind::Delegate {
|
||||
owner: Addr::unchecked(owner),
|
||||
mix_id,
|
||||
amount: amount1,
|
||||
@@ -306,8 +344,8 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
events[1],
|
||||
PendingEpochEventData::Delegate {
|
||||
events[1].kind,
|
||||
PendingEpochEventKind::Delegate {
|
||||
owner: Addr::unchecked(owner),
|
||||
mix_id,
|
||||
amount: amount2,
|
||||
@@ -329,11 +367,12 @@ mod tests {
|
||||
#[test]
|
||||
fn cannot_be_performed_if_delegation_never_existed() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "delegator";
|
||||
let sender = mock_info(owner, &[]);
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender, mix_id);
|
||||
let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::NoMixnodeDelegationFound {
|
||||
@@ -347,14 +386,16 @@ mod tests {
|
||||
#[test]
|
||||
fn cannot_be_performed_if_the_delegation_is_still_pending() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
|
||||
let sender2 = mock_info(owner, &[]);
|
||||
|
||||
try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap();
|
||||
try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap();
|
||||
|
||||
let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender2, mix_id);
|
||||
let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::NoMixnodeDelegationFound {
|
||||
@@ -368,6 +409,8 @@ mod tests {
|
||||
#[test]
|
||||
fn as_long_as_delegation_exists_can_always_be_performed() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let sender = mock_info(owner, &[]);
|
||||
|
||||
@@ -382,19 +425,30 @@ mod tests {
|
||||
|
||||
try_remove_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("mix-owner-unbonded-leftover", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
test.execute_all_pending_events();
|
||||
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap();
|
||||
try_remove_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("mix-owner-unbonding", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res =
|
||||
try_remove_delegation_from_mixnode(test.deps_mut(), sender.clone(), normal_mix_id);
|
||||
let res = try_remove_delegation_from_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
normal_mix_id,
|
||||
);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let res = try_remove_delegation_from_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_unbonding,
|
||||
);
|
||||
@@ -402,6 +456,7 @@ mod tests {
|
||||
|
||||
let res = try_remove_delegation_from_mixnode(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
mix_id_unbonded_leftover,
|
||||
);
|
||||
|
||||
@@ -274,7 +274,7 @@ pub mod tests {
|
||||
assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result);
|
||||
|
||||
// but after he unbonds it, it's all fine again
|
||||
pending_events::unbond_mixnode(deps.as_mut(), &env, mix_id).unwrap();
|
||||
pending_events::unbond_mixnode(deps.as_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig);
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -6,11 +6,12 @@ use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::{Response, Storage};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::new_interval_config_update_event;
|
||||
use mixnet_contract_common::Interval;
|
||||
use mixnet_contract_common::{BlockHeight, Interval};
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) fn change_interval_config(
|
||||
store: &mut dyn Storage,
|
||||
request_creation: BlockHeight,
|
||||
mut current_interval: Interval,
|
||||
epochs_in_interval: u32,
|
||||
epoch_duration_secs: u64,
|
||||
@@ -25,6 +26,7 @@ pub(crate) fn change_interval_config(
|
||||
storage::save_interval(store, ¤t_interval)?;
|
||||
|
||||
Ok(Response::new().add_event(new_interval_config_update_event(
|
||||
request_creation,
|
||||
epochs_in_interval,
|
||||
epoch_duration_secs,
|
||||
rewarding_params.interval,
|
||||
@@ -50,6 +52,7 @@ mod tests {
|
||||
// if we half the number of epochs, the reward budget should get doubled
|
||||
change_interval_config(
|
||||
&mut deps.storage,
|
||||
123,
|
||||
initial_interval,
|
||||
initial_interval.epochs_in_interval() / 2,
|
||||
initial_interval.epoch_length_secs(),
|
||||
@@ -72,6 +75,7 @@ mod tests {
|
||||
// and similarly when we double number of epochs, the reward budget should get halved
|
||||
change_interval_config(
|
||||
&mut deps.storage,
|
||||
123,
|
||||
initial_interval,
|
||||
initial_interval.epochs_in_interval() * 2,
|
||||
initial_interval.epoch_length_secs(),
|
||||
|
||||
@@ -17,9 +17,12 @@ use mixnet_contract_common::events::{
|
||||
new_rewarding_params_update_event, new_undelegation_event,
|
||||
};
|
||||
use mixnet_contract_common::mixnode::MixNodeCostParams;
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::pending_events::{
|
||||
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
|
||||
PendingIntervalEventKind,
|
||||
};
|
||||
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
use mixnet_contract_common::{BlockHeight, Delegation, MixId};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
|
||||
|
||||
pub(crate) trait ContractExecutableEvent {
|
||||
@@ -32,6 +35,7 @@ pub(crate) trait ContractExecutableEvent {
|
||||
pub(crate) fn delegate(
|
||||
deps: DepsMut<'_>,
|
||||
env: &Env,
|
||||
created_at: BlockHeight,
|
||||
owner: Addr,
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
@@ -103,9 +107,16 @@ pub(crate) fn delegate(
|
||||
};
|
||||
|
||||
// add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one)
|
||||
mix_rewarding.add_base_delegation(stored_delegation_amount.amount);
|
||||
mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?;
|
||||
|
||||
let cosmos_event = new_delegation_event(&owner, &proxy, &new_delegation_amount, mix_id);
|
||||
let cosmos_event = new_delegation_event(
|
||||
created_at,
|
||||
&owner,
|
||||
&proxy,
|
||||
&new_delegation_amount,
|
||||
mix_id,
|
||||
mix_rewarding.total_unit_reward,
|
||||
);
|
||||
|
||||
let delegation = Delegation::new(
|
||||
owner,
|
||||
@@ -130,6 +141,7 @@ pub(crate) fn delegate(
|
||||
|
||||
pub(crate) fn undelegate(
|
||||
deps: DepsMut<'_>,
|
||||
created_at: BlockHeight,
|
||||
owner: Addr,
|
||||
mix_id: MixId,
|
||||
proxy: Option<Addr>,
|
||||
@@ -153,7 +165,7 @@ pub(crate) fn undelegate(
|
||||
let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]);
|
||||
let mut response = Response::new()
|
||||
.add_message(return_tokens)
|
||||
.add_event(new_undelegation_event(&owner, &proxy, mix_id));
|
||||
.add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id));
|
||||
|
||||
if let Some(proxy) = &proxy {
|
||||
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
|
||||
@@ -177,6 +189,7 @@ pub(crate) fn undelegate(
|
||||
pub(crate) fn unbond_mixnode(
|
||||
deps: DepsMut<'_>,
|
||||
env: &Env,
|
||||
created_at: BlockHeight,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be
|
||||
@@ -207,7 +220,7 @@ pub(crate) fn unbond_mixnode(
|
||||
|
||||
let mut response = Response::new()
|
||||
.add_message(return_tokens)
|
||||
.add_event(new_mixnode_unbonding_event(mix_id));
|
||||
.add_event(new_mixnode_unbonding_event(created_at, mix_id));
|
||||
|
||||
if let Some(proxy) = &proxy {
|
||||
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
|
||||
@@ -229,6 +242,7 @@ pub(crate) fn unbond_mixnode(
|
||||
|
||||
pub(crate) fn update_active_set_size(
|
||||
deps: DepsMut<'_>,
|
||||
created_at: BlockHeight,
|
||||
active_set_size: u32,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// We don't have to check for authorization as this event can only be pushed
|
||||
@@ -241,28 +255,30 @@ pub(crate) fn update_active_set_size(
|
||||
rewarding_params.try_change_active_set_size(active_set_size)?;
|
||||
rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
|
||||
|
||||
Ok(Response::new().add_event(new_active_set_update_event(active_set_size)))
|
||||
Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size)))
|
||||
}
|
||||
|
||||
impl ContractExecutableEvent for PendingEpochEventData {
|
||||
fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result<Response, MixnetContractError> {
|
||||
// note that the basic validation on all those events was already performed before
|
||||
// they were pushed onto the queue
|
||||
match self {
|
||||
PendingEpochEventData::Delegate {
|
||||
match self.kind {
|
||||
PendingEpochEventKind::Delegate {
|
||||
owner,
|
||||
mix_id,
|
||||
amount,
|
||||
proxy,
|
||||
} => delegate(deps, env, owner, mix_id, amount, proxy),
|
||||
PendingEpochEventData::Undelegate {
|
||||
} => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy),
|
||||
PendingEpochEventKind::Undelegate {
|
||||
owner,
|
||||
mix_id,
|
||||
proxy,
|
||||
} => undelegate(deps, owner, mix_id, proxy),
|
||||
PendingEpochEventData::UnbondMixnode { mix_id } => unbond_mixnode(deps, env, mix_id),
|
||||
PendingEpochEventData::UpdateActiveSetSize { new_size } => {
|
||||
update_active_set_size(deps, new_size)
|
||||
} => undelegate(deps, self.created_at, owner, mix_id, proxy),
|
||||
PendingEpochEventKind::UnbondMixnode { mix_id } => {
|
||||
unbond_mixnode(deps, env, self.created_at, mix_id)
|
||||
}
|
||||
PendingEpochEventKind::UpdateActiveSetSize { new_size } => {
|
||||
update_active_set_size(deps, self.created_at, new_size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +286,7 @@ impl ContractExecutableEvent for PendingEpochEventData {
|
||||
|
||||
pub(crate) fn change_mix_cost_params(
|
||||
deps: DepsMut<'_>,
|
||||
created_at: BlockHeight,
|
||||
mix_id: MixId,
|
||||
new_costs: MixNodeCostParams,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
@@ -285,7 +302,7 @@ pub(crate) fn change_mix_cost_params(
|
||||
_ => return Ok(Response::default()),
|
||||
};
|
||||
|
||||
let cosmos_event = new_mixnode_cost_params_update_event(mix_id, &new_costs);
|
||||
let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs);
|
||||
|
||||
// TODO: can we just change cost_params without breaking rewarding calculation?
|
||||
// (I'm almost certain we can, but well, it has to be tested)
|
||||
@@ -297,6 +314,7 @@ pub(crate) fn change_mix_cost_params(
|
||||
|
||||
pub(crate) fn update_rewarding_params(
|
||||
deps: DepsMut<'_>,
|
||||
created_at: BlockHeight,
|
||||
updated_params: IntervalRewardingParamsUpdate,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// We don't have to check for authorization as this event can only be pushed
|
||||
@@ -311,6 +329,7 @@ pub(crate) fn update_rewarding_params(
|
||||
rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
|
||||
|
||||
Ok(Response::new().add_event(new_rewarding_params_update_event(
|
||||
created_at,
|
||||
updated_params,
|
||||
rewarding_params.interval,
|
||||
)))
|
||||
@@ -318,6 +337,7 @@ pub(crate) fn update_rewarding_params(
|
||||
|
||||
pub(crate) fn update_interval_config(
|
||||
deps: DepsMut,
|
||||
created_at: BlockHeight,
|
||||
epochs_in_interval: u32,
|
||||
epoch_duration_secs: u64,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
@@ -326,8 +346,10 @@ pub(crate) fn update_interval_config(
|
||||
// Furthermore, we don't need to check whether the interval is finished as the
|
||||
// queue is only emptied upon the interval finishing.
|
||||
let interval = storage::current_interval(deps.storage)?;
|
||||
|
||||
change_interval_config(
|
||||
deps.storage,
|
||||
created_at,
|
||||
interval,
|
||||
epochs_in_interval,
|
||||
epoch_duration_secs,
|
||||
@@ -338,18 +360,23 @@ impl ContractExecutableEvent for PendingIntervalEventData {
|
||||
fn execute(self, deps: DepsMut<'_>, _env: &Env) -> Result<Response, MixnetContractError> {
|
||||
// note that the basic validation on all those events was already performed before
|
||||
// they were pushed onto the queue
|
||||
match self {
|
||||
PendingIntervalEventData::ChangeMixCostParams {
|
||||
match self.kind {
|
||||
PendingIntervalEventKind::ChangeMixCostParams {
|
||||
mix_id: mix,
|
||||
new_costs,
|
||||
} => change_mix_cost_params(deps, mix, new_costs),
|
||||
PendingIntervalEventData::UpdateRewardingParams { update } => {
|
||||
update_rewarding_params(deps, update)
|
||||
} => change_mix_cost_params(deps, self.created_at, mix, new_costs),
|
||||
PendingIntervalEventKind::UpdateRewardingParams { update } => {
|
||||
update_rewarding_params(deps, self.created_at, update)
|
||||
}
|
||||
PendingIntervalEventData::UpdateIntervalConfig {
|
||||
PendingIntervalEventKind::UpdateIntervalConfig {
|
||||
epochs_in_interval,
|
||||
epoch_duration_secs,
|
||||
} => update_interval_config(deps, epochs_in_interval, epoch_duration_secs),
|
||||
} => update_interval_config(
|
||||
deps,
|
||||
self.created_at,
|
||||
epochs_in_interval,
|
||||
epoch_duration_secs,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,11 +417,12 @@ mod tests {
|
||||
test.add_immediate_delegation(owner1, delegation, mix_id);
|
||||
|
||||
let env = test.env();
|
||||
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let res_increase = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -420,6 +448,7 @@ mod tests {
|
||||
let res_fresh = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner2),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -453,11 +482,12 @@ mod tests {
|
||||
test.add_immediate_delegation(owner1, delegation, mix_id);
|
||||
|
||||
let env = test.env();
|
||||
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner", &[])).unwrap();
|
||||
try_remove_mixnode(test.deps_mut(), env.clone(), mock_info("mix-owner", &[])).unwrap();
|
||||
|
||||
let res_increase = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -483,6 +513,7 @@ mod tests {
|
||||
let res_fresh = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner2),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -518,6 +549,7 @@ mod tests {
|
||||
let res = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
delegation_coin_new,
|
||||
@@ -580,6 +612,7 @@ mod tests {
|
||||
let res = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
delegation_coin_new,
|
||||
@@ -645,6 +678,7 @@ mod tests {
|
||||
let res = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -679,7 +713,7 @@ mod tests {
|
||||
let owner2 = "delegator2";
|
||||
|
||||
let env = test.env();
|
||||
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
@@ -688,6 +722,7 @@ mod tests {
|
||||
let res_vesting = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -734,6 +769,7 @@ mod tests {
|
||||
let res_other_proxy = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
@@ -778,7 +814,7 @@ mod tests {
|
||||
|
||||
let owner = Addr::unchecked("delegator");
|
||||
|
||||
let res = undelegate(test.deps_mut(), owner, mix_id, None).unwrap();
|
||||
let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap();
|
||||
assert!(get_bank_send_msg(&res).is_none());
|
||||
}
|
||||
|
||||
@@ -793,7 +829,7 @@ mod tests {
|
||||
// this should never happen in actual code, but if we manually messed something up,
|
||||
// lets make sure this throws an error
|
||||
rewards_storage::MIXNODE_REWARDING.remove(test.deps_mut().storage, mix_id);
|
||||
let res = undelegate(test.deps_mut(), owner, mix_id, None);
|
||||
let res = undelegate(test.deps_mut(), 123, owner, mix_id, None);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::InconsistentState { .. })
|
||||
@@ -827,7 +863,8 @@ mod tests {
|
||||
|
||||
let expected_return = delegation + truncated_reward.u128();
|
||||
|
||||
let res = undelegate(test.deps_mut(), Addr::unchecked(owner), mix_id, None).unwrap();
|
||||
let res =
|
||||
undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id, None).unwrap();
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap();
|
||||
assert_eq!(receiver, owner);
|
||||
assert_eq!(sent_amount[0].amount.u128(), expected_return);
|
||||
@@ -875,6 +912,7 @@ mod tests {
|
||||
// for a fresh delegation, nothing was added to the storage either
|
||||
let res_vesting = undelegate(
|
||||
test.deps_mut(),
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
mix_id,
|
||||
Some(vesting_contract.clone()),
|
||||
@@ -920,6 +958,7 @@ mod tests {
|
||||
|
||||
let res_other_proxy = undelegate(
|
||||
test.deps_mut(),
|
||||
123,
|
||||
Addr::unchecked(owner2),
|
||||
mix_id,
|
||||
Some(dummy_proxy.clone()),
|
||||
@@ -964,7 +1003,7 @@ mod tests {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let res = unbond_mixnode(test.deps_mut(), &env, 1);
|
||||
let res = unbond_mixnode(test.deps_mut(), &env, 123, 1);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::InconsistentState { .. })
|
||||
@@ -994,7 +1033,7 @@ mod tests {
|
||||
let expected_return = pledge + truncated_reward;
|
||||
|
||||
let env = test.env();
|
||||
let res = unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
|
||||
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap();
|
||||
assert_eq!(receiver, owner);
|
||||
assert_eq!(sent_amount[0].amount, expected_return);
|
||||
@@ -1042,7 +1081,7 @@ mod tests {
|
||||
test.add_dummy_mixnode_with_proxy(owner2, Some(pledge), dummy_proxy.clone());
|
||||
|
||||
let env = test.env();
|
||||
let res_vesting = unbond_mixnode(test.deps_mut(), &env, mix_id_vesting).unwrap();
|
||||
let res_vesting = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap();
|
||||
|
||||
assert!(mixnodes_storage::mixnode_bonds()
|
||||
.may_load(test.deps().storage, mix_id_vesting)
|
||||
@@ -1077,7 +1116,7 @@ mod tests {
|
||||
assert!(found_track);
|
||||
|
||||
let res_other_proxy =
|
||||
unbond_mixnode(test.deps_mut(), &env, mix_id_other_proxy).unwrap();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id_other_proxy).unwrap();
|
||||
assert!(mixnodes_storage::mixnode_bonds()
|
||||
.may_load(test.deps().storage, mix_id_other_proxy)
|
||||
.unwrap()
|
||||
@@ -1103,7 +1142,7 @@ mod tests {
|
||||
.load(test.deps().storage)
|
||||
.unwrap();
|
||||
|
||||
update_active_set_size(test.deps_mut(), 50).unwrap();
|
||||
update_active_set_size(test.deps_mut(), 123, 50).unwrap();
|
||||
let updated = rewards_storage::REWARDING_PARAMS
|
||||
.load(test.deps().storage)
|
||||
.unwrap();
|
||||
@@ -1124,14 +1163,14 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
let env = test.env();
|
||||
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let new_params = MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
|
||||
interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM),
|
||||
};
|
||||
|
||||
let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params);
|
||||
let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params);
|
||||
assert_eq!(res, Ok(Response::default()));
|
||||
}
|
||||
|
||||
@@ -1147,11 +1186,16 @@ mod tests {
|
||||
interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM),
|
||||
};
|
||||
|
||||
let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params.clone());
|
||||
let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone());
|
||||
assert_eq!(
|
||||
res,
|
||||
Ok(Response::new()
|
||||
.add_event(new_mixnode_cost_params_update_event(mix_id, &new_params)))
|
||||
Ok(
|
||||
Response::new().add_event(new_mixnode_cost_params_update_event(
|
||||
123,
|
||||
mix_id,
|
||||
&new_params
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
let after = test.mix_rewarding(mix_id).cost_params;
|
||||
@@ -1176,13 +1220,14 @@ mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: Some(before.interval.reward_pool / two),
|
||||
staking_supply: Some(before.interval.staking_supply * four),
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()),
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
rewarded_set_size: None,
|
||||
};
|
||||
|
||||
let res = update_rewarding_params(test.deps_mut(), update);
|
||||
let res = update_rewarding_params(test.deps_mut(), 123, update);
|
||||
assert!(res.is_ok());
|
||||
let after = rewards_storage::REWARDING_PARAMS
|
||||
.load(test.deps().storage)
|
||||
@@ -1230,6 +1275,7 @@ mod tests {
|
||||
// and change epoch length
|
||||
update_interval_config(
|
||||
test.deps_mut(),
|
||||
123,
|
||||
interval_before.epochs_in_interval() / 2,
|
||||
1234,
|
||||
)
|
||||
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
mod pending_epoch_events {
|
||||
use super::*;
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventData;
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventKind;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) {
|
||||
@@ -248,12 +248,13 @@ mod tests {
|
||||
}
|
||||
|
||||
fn push_dummy_epoch_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingEpochEventData::Undelegate {
|
||||
let dummy_action = PendingEpochEventKind::Undelegate {
|
||||
owner: Addr::unchecked("foomp"),
|
||||
mix_id: test.rng.next_u32(),
|
||||
proxy: None,
|
||||
};
|
||||
storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap();
|
||||
let env = test.env();
|
||||
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -379,7 +380,7 @@ mod tests {
|
||||
mod pending_interval_events {
|
||||
use super::*;
|
||||
use crate::support::tests::fixtures;
|
||||
use mixnet_contract_common::pending_events::PendingIntervalEventData;
|
||||
use mixnet_contract_common::pending_events::PendingIntervalEventKind;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) {
|
||||
@@ -389,11 +390,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn push_dummy_interval_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingIntervalEventData::ChangeMixCostParams {
|
||||
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
|
||||
mix_id: test.rng.next_u32(),
|
||||
new_costs: fixtures::mix_node_cost_params_fixture(),
|
||||
};
|
||||
storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap();
|
||||
let env = test.env();
|
||||
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,11 +6,13 @@ use crate::constants::{
|
||||
LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE,
|
||||
PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY,
|
||||
};
|
||||
use cosmwasm_std::{Order, StdResult, Storage};
|
||||
use cosmwasm_std::{Env, Order, StdResult, Storage};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::pending_events::{
|
||||
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
|
||||
};
|
||||
use mixnet_contract_common::{
|
||||
EpochEventId, Interval, IntervalEventId, MixId, RewardedSetNodeStatus,
|
||||
EpochEventId, Interval, IntervalEventId, MixId, PendingIntervalEventKind, RewardedSetNodeStatus,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -64,18 +66,22 @@ pub(crate) fn next_interval_event_id_counter(
|
||||
|
||||
pub(crate) fn push_new_epoch_event(
|
||||
storage: &mut dyn Storage,
|
||||
event: &PendingEpochEventData,
|
||||
env: &Env,
|
||||
event: PendingEpochEventKind,
|
||||
) -> StdResult<()> {
|
||||
let event_id = next_epoch_event_id_counter(storage)?;
|
||||
PENDING_EPOCH_EVENTS.save(storage, event_id, event)
|
||||
let event_data = event.attach_source_height(env.block.height);
|
||||
PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data)
|
||||
}
|
||||
|
||||
pub(crate) fn push_new_interval_event(
|
||||
storage: &mut dyn Storage,
|
||||
event: &PendingIntervalEventData,
|
||||
env: &Env,
|
||||
event: PendingIntervalEventKind,
|
||||
) -> StdResult<()> {
|
||||
let event_id = next_interval_event_id_counter(storage)?;
|
||||
PENDING_INTERVAL_EVENTS.save(storage, event_id, event)
|
||||
let event_data = event.attach_source_height(env.block.height);
|
||||
PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data)
|
||||
}
|
||||
|
||||
pub(crate) fn update_rewarded_set(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user