Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c61eb014c5 | |||
| c10f788a89 | |||
| 73fa863a6a | |||
| a561f1b854 | |||
| 1c379fa0db | |||
| 31778c6e51 | |||
| 9d0562168e | |||
| 701f603825 | |||
| 97b250433e | |||
| f1deebc0f1 | |||
| 9063a86d26 | |||
| d82fd620ad | |||
| fa95d15eac | |||
| b71a8708db | |||
| fea6f44a57 | |||
| 23e97e9643 | |||
| 3f5bfcc696 | |||
| f568673fbc | |||
| f6576939d9 | |||
| ce17196d48 | |||
| 6dde8ecd0a | |||
| 1db5e6af05 | |||
| c4ee964557 | |||
| 9337821712 | |||
| 279ba7034c | |||
| 1859ca0a30 | |||
| e30fd270a1 | |||
| 3ae16fbf1d | |||
| e3cda93919 | |||
| 87fb4daeda | |||
| 9f56796bf6 | |||
| 09b51226c2 | |||
| 6946151b25 | |||
| a4aee465fa | |||
| 0d71ac5e75 | |||
| ce14e40968 | |||
| d108edb424 | |||
| 18f5623b05 | |||
| 8e92801929 | |||
| 3fc1bc4e7c | |||
| 72de726762 | |||
| cb9dfa8188 | |||
| f102ed53a7 | |||
| a803c7f25e | |||
| f20b620cbb | |||
| d771d15959 | |||
| 49e6f387ff | |||
| 9568c0ba1d | |||
| 0b7b705e56 | |||
| 5daea675e7 | |||
| ebd18586a8 | |||
| 585610295f | |||
| 91653d13c6 | |||
| b84486c0f4 | |||
| b6a765481a | |||
| 8f52f34bc4 | |||
| 39798de1e8 | |||
| c650587e4c | |||
| 660d5d8b05 | |||
| 79f9db91ae | |||
| 43822f27a8 | |||
| e500d154dd | |||
| 3ceb00fae1 | |||
| d019343fd9 | |||
| f55a55b784 | |||
| 0ea8da79c8 | |||
| 0e12251773 | |||
| f886326014 | |||
| c73c2beb33 | |||
| b7aa84cd5a | |||
| b6b40163c6 | |||
| f46c0142e7 | |||
| 8774b22d84 | |||
| c74a880838 | |||
| ccbb254b1a | |||
| 2bd0cfc870 | |||
| aeaf31ed59 | |||
| 05820cfca7 | |||
| 0469d5b602 | |||
| d912844543 | |||
| 64757ebc83 | |||
| ba55affe0a | |||
| e9f826e705 | |||
| bfbd509e4b | |||
| b68fb4f5dd | |||
| 7461fe88d0 | |||
| 510d0333a1 | |||
| cea4887080 | |||
| 8f1cb67bf7 | |||
| 09b9601c7e | |||
| 2a1dd138e0 | |||
| 89925e49e8 | |||
| 96fc7208a2 | |||
| 9874daa061 | |||
| baa61c07d5 | |||
| b8cb683da0 | |||
| 62e9c8236a | |||
| d79c25861b | |||
| b89ec2e0be | |||
| 269f50bdd4 | |||
| e3c02dc80a | |||
| 65a9320d35 | |||
| cf268ffcd5 | |||
| bdcfe42a1e | |||
| 51e30b2a89 | |||
| 76d4d0e7cb | |||
| 9df432b8a2 | |||
| 4021059e76 | |||
| 43ef098aad | |||
| bf1d2a12bc | |||
| c3f214ffad | |||
| 324fb6afe7 | |||
| ace020b5cf | |||
| 4b8fa4805e | |||
| eb18b49f3e | |||
| 2dc45fda1e | |||
| c2a113f1b3 | |||
| f805eebce7 | |||
| ad81160760 | |||
| 0931236a98 | |||
| 9b14e00653 | |||
| ec8b5e6e9d | |||
| d4584c305a | |||
| afc53d4379 | |||
| 4278e88d3c | |||
| e12a34ce6b | |||
| 1de64f7b52 | |||
| 66dbe09e66 | |||
| dcce269921 | |||
| c043f0096a | |||
| fe6da046dc | |||
| 99b30c2570 | |||
| 2c5d31e685 | |||
| cf65bc1295 | |||
| 2d5f851252 | |||
| 5ce087dafe | |||
| caf03a09c8 | |||
| 5db47b8931 | |||
| 27c1b29615 | |||
| c80c8ef899 | |||
| d0cd22c4da | |||
| a721e97c06 | |||
| 6eb482fc4b | |||
| f9be735d4f | |||
| 0fd178a304 | |||
| 16ccbd9e48 | |||
| bd0ea45f35 | |||
| 28cc772d7b |
+30
-14
@@ -1,36 +1,52 @@
|
||||
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: |
|
||||
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
|
||||
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
|
||||
>(uniq &> .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
|
||||
|
||||
@@ -29,6 +29,12 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -48,12 +54,6 @@ jobs:
|
||||
command: test
|
||||
args: --workspace --all-features -- --ignored
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
name: Clippy checks
|
||||
with:
|
||||
@@ -66,6 +66,8 @@ jobs:
|
||||
command: clippy
|
||||
args: --workspace -- -D warnings
|
||||
|
||||
# COCONUT stuff
|
||||
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -82,4 +84,4 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --features=coconut -- -D warnings
|
||||
args: --all-targets --features=coconut -- -D warnings
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Continuous integration on dispatch
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ self-hosted, custom-linux ]
|
||||
# Enable sccache via environment variable
|
||||
env:
|
||||
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --all-features
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
name: Clippy checks
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace -- -D warnings
|
||||
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run all tests with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run clippy with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --features=coconut -- -D warnings
|
||||
@@ -0,0 +1,56 @@
|
||||
name: CI for Network Explorer API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
|
||||
|
||||
- name: Check the release tag starts with `nym-explorer-api-`
|
||||
if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch'
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Release tag did not start with nym-explorer-api-...')
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build all explorer-api
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path explorer-api/Cargo.toml --workspace --release
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
+42
-13
@@ -1,32 +1,49 @@
|
||||
name: Nightly builds on dispatch
|
||||
name: Nightly builds on latest release
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
schedule:
|
||||
- cron: '14 2 * * *'
|
||||
jobs:
|
||||
matrix_prep:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
# creates the matrix strategy from nightly_build_matrix_includes.json
|
||||
- uses: actions/checkout@v2
|
||||
# creates the matrix strategy from nightly_build_release_matrix.json
|
||||
- uses: actions/checkout@v3
|
||||
- id: set-matrix
|
||||
uses: JoshuaTheMiller/conditional-build-matrix@main
|
||||
with:
|
||||
inputFile: '.github/workflows/nightly_build_matrix_on_dispatch.json'
|
||||
inputFile: '.github/workflows/nightly_build_release_matrix.json'
|
||||
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
|
||||
build:
|
||||
get_release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: matrix_prep
|
||||
outputs:
|
||||
output1: ${{ steps.step2.outputs.latest_release }}
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
- name: Fetch all branches
|
||||
run: git fetch --all
|
||||
- name: Set output variable to latest release branch
|
||||
id: step2
|
||||
run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT
|
||||
build:
|
||||
needs: [get_release,matrix_prep]
|
||||
strategy:
|
||||
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }}
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check out latest release branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{needs.get_release.outputs.output1}}
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -42,6 +59,12 @@ jobs:
|
||||
command: build
|
||||
args: --workspace
|
||||
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -99,6 +122,12 @@ jobs:
|
||||
command: build
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
- name: Run all tests with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -151,7 +180,7 @@ jobs:
|
||||
- name: Collect jobs status
|
||||
uses: technote-space/workflow-conclusion-action@v2
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Keybase - Node Install
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
run: npm install
|
||||
@@ -160,14 +189,14 @@ jobs:
|
||||
if: env.WORKFLOW_CONCLUSION == 'failure'
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: nightly
|
||||
NYM_PROJECT_NAME: "Nym nightly build"
|
||||
NYM_PROJECT_NAME: "Nym nightly build on latest release"
|
||||
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_NYMTECH_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "${{ secrets.KEYBASE_CHANNEL_DEV_CORE_ID }}"
|
||||
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "ci-nightly-release"
|
||||
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
@@ -25,6 +25,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
|
||||
|
||||
- name: Check the release tag starts with `nym-binaries-`
|
||||
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch'
|
||||
uses: actions/github-script@v3
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
+43
-25
@@ -6,34 +6,52 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
### Added
|
||||
|
||||
- nym-cli: added CLI tool for interacting with the Nyx blockchain and Nym mixnet smart contracts ([#1577])
|
||||
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
|
||||
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611](https://github.com/nymtech/nym/pull/1611))
|
||||
- common/ledger: new library for communicating with a Ledger device ([#1640])
|
||||
- native-client/socks5-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666])
|
||||
- native-client/socks5-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664])
|
||||
- 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
|
||||
- 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])
|
||||
- binaries: add `-c` shortform for `--config-env-file`
|
||||
- websocket-requests: add server response signalling current packet queue length in the client
|
||||
|
||||
### Changed
|
||||
|
||||
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
|
||||
- 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])
|
||||
- clients: add concept of transmission lanes to better handle multiple data streams ([#1720])
|
||||
- socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783])
|
||||
|
||||
[#1720]: https://github.com/nymtech/nym/pull/1720
|
||||
[#1783]: https://github.com/nymtech/nym/pull/1783
|
||||
|
||||
|
||||
## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09)
|
||||
|
||||
### Added
|
||||
|
||||
- clients: add testing-only support for two more extended packet sizes (8kb and 16kb).
|
||||
- common/ledger: new library for communicating with a Ledger device ([#1640])
|
||||
- native-client/socks5-client/wasm-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666])
|
||||
- native-client/socks5-client/wasm-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664])
|
||||
- native-client/socks5-client/wasm-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])
|
||||
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611])
|
||||
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to compute reward estimation endpoint
|
||||
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
|
||||
- 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])
|
||||
|
||||
### Fixed
|
||||
|
||||
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724])
|
||||
- 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])
|
||||
|
||||
### Changed
|
||||
|
||||
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725])
|
||||
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
|
||||
- moved `Percent` struct to `contracts-common`, change affects explorer-api
|
||||
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
|
||||
- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681])
|
||||
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
|
||||
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
|
||||
- validator-api: changes to internal SQL schema due to the mixnet contract revamp ([#1472])
|
||||
- validator-api: changes to internal data structures due to the mixnet contract revamp ([#1472])
|
||||
- validator-api: split epoch-operations into multiple separate transactions ([#1472])
|
||||
|
||||
[#1472]: https://github.com/nymtech/nym/pull/1472
|
||||
[#1541]: https://github.com/nymtech/nym/pull/1541
|
||||
[#1558]: https://github.com/nymtech/nym/pull/1558
|
||||
[#1577]: https://github.com/nymtech/nym/pull/1577
|
||||
@@ -41,13 +59,13 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
[#1591]: https://github.com/nymtech/nym/pull/1591
|
||||
[#1640]: https://github.com/nymtech/nym/pull/1640
|
||||
[#1645]: https://github.com/nymtech/nym/pull/1645
|
||||
[#1611]: https://github.com/nymtech/nym/pull/1611
|
||||
[#1664]: https://github.com/nymtech/nym/pull/1664
|
||||
[#1666]: https://github.com/nymtech/nym/pull/1645
|
||||
[#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
|
||||
[#1721]: https://github.com/nymtech/nym/pull/1721
|
||||
|
||||
Generated
+54
-18
@@ -577,9 +577,18 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.0.1"
|
||||
name = "client-connections"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"client-connections",
|
||||
"config",
|
||||
"crypto",
|
||||
"dirs",
|
||||
@@ -627,6 +636,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coconut-dkg-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coconut-interface"
|
||||
version = "0.1.0"
|
||||
@@ -736,7 +755,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
name = "contracts-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-std",
|
||||
"dkg",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -850,9 +871,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
|
||||
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -1402,6 +1423,7 @@ dependencies = [
|
||||
"ff 0.11.0",
|
||||
"group 0.11.0",
|
||||
"lazy_static",
|
||||
"pemstore",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.3",
|
||||
@@ -1453,9 +1475,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "1.4.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4"
|
||||
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature",
|
||||
@@ -1582,7 +1604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "explorer-api"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 3.2.8",
|
||||
@@ -3070,7 +3092,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3122,9 +3144,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"clap 3.2.8",
|
||||
"client-connections",
|
||||
"client-core",
|
||||
"coconut-interface",
|
||||
"completions",
|
||||
@@ -3146,7 +3169,9 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sled",
|
||||
"tap",
|
||||
"task",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.14.0",
|
||||
"topology",
|
||||
@@ -3159,7 +3184,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-gateway"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3206,7 +3231,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnode"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
@@ -3248,10 +3273,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"clap 3.2.8",
|
||||
"client-connections",
|
||||
"completions",
|
||||
"dirs",
|
||||
"futures",
|
||||
@@ -3295,9 +3321,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"clap 3.2.8",
|
||||
"client-connections",
|
||||
"client-core",
|
||||
"coconut-interface",
|
||||
"completions",
|
||||
@@ -3322,7 +3349,9 @@ dependencies = [
|
||||
"serde",
|
||||
"snafu 0.6.10",
|
||||
"socks5-requests",
|
||||
"tap",
|
||||
"task",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"topology",
|
||||
"url",
|
||||
@@ -3359,13 +3388,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-validator-api"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bs58",
|
||||
"cfg-if 1.0.0",
|
||||
"clap 3.2.8",
|
||||
"coconut-bandwidth-contract-common",
|
||||
"coconut-dkg-common",
|
||||
"coconut-interface",
|
||||
"config",
|
||||
"console-subscriber",
|
||||
@@ -3377,6 +3408,7 @@ dependencies = [
|
||||
"cw-utils",
|
||||
"cw3",
|
||||
"dirs",
|
||||
"dkg",
|
||||
"dotenv",
|
||||
"futures",
|
||||
"gateway-client",
|
||||
@@ -3390,6 +3422,7 @@ dependencies = [
|
||||
"nymcoconut",
|
||||
"nymsphinx",
|
||||
"okapi",
|
||||
"pemstore",
|
||||
"pin-project",
|
||||
"pretty_env_logger",
|
||||
"rand 0.7.3",
|
||||
@@ -3442,16 +3475,18 @@ name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bls12_381 0.5.0",
|
||||
"bls12_381 0.6.0",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"dkg",
|
||||
"doc-comment",
|
||||
"ff 0.10.1",
|
||||
"ff 0.11.0",
|
||||
"getrandom 0.2.6",
|
||||
"group 0.10.0",
|
||||
"group 0.11.0",
|
||||
"itertools",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2 0.9.9",
|
||||
@@ -4121,6 +4156,7 @@ name = "proxy-helpers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"client-connections",
|
||||
"futures",
|
||||
"log",
|
||||
"ordered-buffer",
|
||||
@@ -6405,6 +6441,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bip39",
|
||||
"coconut-bandwidth-contract-common",
|
||||
"coconut-dkg-common",
|
||||
"coconut-interface",
|
||||
"colored",
|
||||
"config",
|
||||
@@ -6514,7 +6551,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -26,10 +26,12 @@ members = [
|
||||
"common/client-libs/gateway-client",
|
||||
"common/client-libs/mixnet-client",
|
||||
"common/client-libs/validator-client",
|
||||
"common/client-connections",
|
||||
"common/coconut-interface",
|
||||
"common/commands",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
|
||||
@@ -92,6 +92,9 @@ build-wallet:
|
||||
build-connect:
|
||||
cargo build --manifest-path nym-connect/Cargo.toml --workspace
|
||||
|
||||
build-explorer-api:
|
||||
cargo build --manifest-path explorer-api/Cargo.toml --workspace
|
||||
|
||||
build-wasm-client:
|
||||
cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "client-core"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -14,11 +14,13 @@ log = "0.4"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sled = { version = "0.34", optional = true }
|
||||
tap = "1.0.1"
|
||||
thiserror = "1.0.34"
|
||||
url = { version ="2.2", features = ["serde"] }
|
||||
|
||||
# internal
|
||||
config = { path = "../../common/config" }
|
||||
client-connections = { path = "../../common/client-connections" }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
#gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
|
||||
@@ -28,7 +30,6 @@ nymsphinx = { path = "../../common/nymsphinx" }
|
||||
pemstore = { path = "../../common/pemstore" }
|
||||
topology = { path = "../../common/topology" }
|
||||
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
|
||||
tap = "1.0.1"
|
||||
|
||||
tokio = { version = "1.21.2", features = ["time", "macros"]}
|
||||
|
||||
@@ -56,4 +57,5 @@ tempfile = "3.1.0"
|
||||
default = ["reply-surb"]
|
||||
wasm = ["gateway-client/wasm"]
|
||||
coconut = ["gateway-client/coconut", "gateway-requests/coconut"]
|
||||
reply-surb = ["sled"]
|
||||
reply-surb = ["sled"]
|
||||
|
||||
|
||||
@@ -178,6 +178,10 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
// 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");
|
||||
// However it's still useful to alert the user that the gateway or the link to
|
||||
// the gateway can't keep up. Either due to insufficient bandwidth on the
|
||||
// client side, or that the gateway is overloaded.
|
||||
log::warn!("Failed to send: gateway appears to not keep up");
|
||||
}
|
||||
TrySendError::Closed(_) => {
|
||||
log::warn!("Failed to send cover message - channel closed");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use futures::channel::mpsc;
|
||||
use client_connections::TransmissionLane;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
|
||||
pub type InputMessageSender = mpsc::UnboundedSender<InputMessage>;
|
||||
pub type InputMessageReceiver = mpsc::UnboundedReceiver<InputMessage>;
|
||||
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
|
||||
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputMessage {
|
||||
@@ -11,6 +11,7 @@ pub enum InputMessage {
|
||||
recipient: Recipient,
|
||||
data: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
lane: TransmissionLane,
|
||||
},
|
||||
Reply {
|
||||
reply_surb: ReplySurb,
|
||||
@@ -19,11 +20,17 @@ pub enum InputMessage {
|
||||
}
|
||||
|
||||
impl InputMessage {
|
||||
pub fn new_fresh(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Self {
|
||||
pub fn new_fresh(
|
||||
recipient: Recipient,
|
||||
data: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
lane: TransmissionLane,
|
||||
) -> Self {
|
||||
InputMessage::Fresh {
|
||||
recipient,
|
||||
data,
|
||||
with_reply_surb,
|
||||
lane,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ impl AcknowledgementListener {
|
||||
}
|
||||
|
||||
async fn on_ack(&mut self, ack_content: Vec<u8>) {
|
||||
debug!("Received an ack");
|
||||
trace!("Received an ack");
|
||||
let frag_id = match recover_identifier(&self.ack_key, &ack_content)
|
||||
.map(FragmentIdentifier::try_from_bytes)
|
||||
{
|
||||
|
||||
+23
-13
@@ -8,7 +8,7 @@ use crate::client::{
|
||||
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
|
||||
topology_control::TopologyAccessor,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use client_connections::TransmissionLane;
|
||||
use log::*;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::preparer::MessagePreparer;
|
||||
@@ -104,6 +104,7 @@ where
|
||||
content: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
) -> Option<Vec<RealMessage>> {
|
||||
log::trace!("handling msg size: {}", content.len());
|
||||
let topology_permit = self.topology_access.get_read_permit().await;
|
||||
let topology = match topology_permit
|
||||
.try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient))
|
||||
@@ -164,27 +165,36 @@ where
|
||||
}
|
||||
|
||||
async fn on_input_message(&mut self, msg: InputMessage) {
|
||||
let real_messages = match msg {
|
||||
let (real_messages, lane) = match msg {
|
||||
InputMessage::Fresh {
|
||||
recipient,
|
||||
data,
|
||||
with_reply_surb,
|
||||
} => {
|
||||
lane,
|
||||
} => (
|
||||
self.handle_fresh_message(recipient, data, with_reply_surb)
|
||||
.await,
|
||||
lane,
|
||||
),
|
||||
InputMessage::Reply { reply_surb, data } => (
|
||||
self.handle_reply(reply_surb, data)
|
||||
.await
|
||||
}
|
||||
InputMessage::Reply { reply_surb, data } => self
|
||||
.handle_reply(reply_surb, data)
|
||||
.await
|
||||
.map(|message| vec![message]),
|
||||
.map(|message| vec![message]),
|
||||
TransmissionLane::Reply,
|
||||
),
|
||||
};
|
||||
|
||||
// there's no point in trying to send nothing
|
||||
if let Some(real_messages) = real_messages {
|
||||
// tells real message sender (with the poisson timer) to send this to the mix network
|
||||
self.real_message_sender
|
||||
.unbounded_send(real_messages)
|
||||
.unwrap();
|
||||
if self
|
||||
.real_message_sender
|
||||
.send((real_messages, lane))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +204,7 @@ where
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
input_msg = self.input_receiver.next() => match input_msg {
|
||||
input_msg = self.input_receiver.recv() => match input_msg {
|
||||
Some(input_msg) => {
|
||||
self.on_input_message(input_msg).await;
|
||||
},
|
||||
@@ -215,7 +225,7 @@ where
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) async fn run(&mut self) {
|
||||
debug!("Started InputMessageListener without graceful shutdown support");
|
||||
while let Some(input_msg) = self.input_receiver.next().await {
|
||||
while let Some(input_msg) = self.input_receiver.recv().await {
|
||||
self.on_input_message(input_msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-11
@@ -1,17 +1,21 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::action_controller::{Action, ActionSender};
|
||||
use super::PendingAcknowledgement;
|
||||
use super::RetransmissionRequestReceiver;
|
||||
use super::{
|
||||
action_controller::{Action, ActionSender},
|
||||
PendingAcknowledgement, RetransmissionRequestReceiver,
|
||||
};
|
||||
use crate::client::{
|
||||
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
|
||||
topology_control::TopologyAccessor,
|
||||
};
|
||||
|
||||
use client_connections::TransmissionLane;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nymsphinx::preparer::MessagePreparer;
|
||||
use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient};
|
||||
use nymsphinx::{
|
||||
acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer,
|
||||
};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
@@ -112,12 +116,17 @@ where
|
||||
.unwrap();
|
||||
|
||||
// send to `OutQueueControl` to eventually send to the mix network
|
||||
self.real_message_sender
|
||||
.unbounded_send(vec![RealMessage::new(
|
||||
prepared_fragment.mix_packet,
|
||||
frag_id,
|
||||
)])
|
||||
.unwrap();
|
||||
if self
|
||||
.real_message_sender
|
||||
.send((
|
||||
vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)],
|
||||
TransmissionLane::Retransmission,
|
||||
))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
use self::{
|
||||
acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl,
|
||||
};
|
||||
use crate::client::real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors;
|
||||
use crate::client::{
|
||||
inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender,
|
||||
topology_control::TopologyAccessor,
|
||||
use crate::{
|
||||
client::{
|
||||
inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender,
|
||||
real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors,
|
||||
topology_control::TopologyAccessor,
|
||||
},
|
||||
spawn_future,
|
||||
};
|
||||
use crate::spawn_future;
|
||||
use client_connections::{ClosedConnectionReceiver, LaneQueueLengths};
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::AcknowledgementReceiver;
|
||||
use log::*;
|
||||
@@ -103,6 +106,7 @@ where
|
||||
// obviously when we finally make shared rng that is on 'higher' level, this should become
|
||||
// generic `R`
|
||||
impl RealMessagesController<OsRng> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
config: Config,
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
@@ -110,10 +114,12 @@ impl RealMessagesController<OsRng> {
|
||||
mix_sender: BatchMixMessageSender,
|
||||
topology_access: TopologyAccessor,
|
||||
#[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
) -> Self {
|
||||
let rng = OsRng;
|
||||
|
||||
let (real_message_sender, real_message_receiver) = mpsc::unbounded();
|
||||
let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1);
|
||||
let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded();
|
||||
|
||||
let ack_controller_connectors = AcknowledgementControllerConnectors::new(
|
||||
@@ -159,6 +165,8 @@ impl RealMessagesController<OsRng> {
|
||||
rng,
|
||||
config.self_recipient,
|
||||
topology_access,
|
||||
lane_queue_lengths,
|
||||
closed_connection_rx,
|
||||
);
|
||||
|
||||
RealMessagesController {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
use crate::client::mix_traffic::BatchMixMessageSender;
|
||||
use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender;
|
||||
use crate::client::topology_control::TopologyAccessor;
|
||||
use futures::channel::mpsc;
|
||||
use client_connections::{
|
||||
ClosedConnectionReceiver, ConnectionCommand, ConnectionId, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use futures::task::{Context, Poll};
|
||||
use futures::{Future, Stream, StreamExt};
|
||||
use log::*;
|
||||
@@ -16,7 +18,6 @@ use nymsphinx::forwarding::packet::MixPacket;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use nymsphinx::utils::sample_poisson_duration;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -27,22 +28,22 @@ 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;
|
||||
use self::{
|
||||
sending_delay_controller::SendingDelayController, transmission_buffer::TransmissionBuffer,
|
||||
};
|
||||
|
||||
mod sending_delay_controller;
|
||||
mod transmission_buffer;
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
/// Configurable parameters of the `OutQueueControl`
|
||||
pub(crate) struct Config {
|
||||
@@ -85,101 +86,6 @@ 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,
|
||||
@@ -203,7 +109,7 @@ where
|
||||
|
||||
// To make sure we don't overload the mix_tx channel, we limit the rate we are pushing
|
||||
// messages.
|
||||
sending_rate_controller: SendingDelayController,
|
||||
sending_delay_controller: SendingDelayController,
|
||||
|
||||
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
|
||||
/// out to the network without any further delays.
|
||||
@@ -222,8 +128,16 @@ where
|
||||
/// Accessor to the common instance of network topology.
|
||||
topology_access: TopologyAccessor,
|
||||
|
||||
/// Buffer containing all real messages received. It is first exhausted before more are pulled.
|
||||
received_buffer: VecDeque<RealMessage>,
|
||||
/// Buffer containing all incoming real messages keyed by transmission lane, that we will send
|
||||
/// out to the mixnet.
|
||||
transmission_buffer: TransmissionBuffer,
|
||||
|
||||
/// Incoming channel for being notified of closed connections, so that we can close lanes
|
||||
/// corresponding to connections. To avoid sending traffic unnecessary
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
|
||||
/// Report queue lengths so that upstream can backoff sending data, and keep connections open.
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
}
|
||||
|
||||
pub(crate) struct RealMessage {
|
||||
@@ -242,8 +156,9 @@ impl RealMessage {
|
||||
|
||||
// messages are already prepared, etc. the real point of it is to forward it to mix_traffic
|
||||
// after sufficient delay
|
||||
pub(crate) type BatchRealMessageSender = mpsc::UnboundedSender<Vec<RealMessage>>;
|
||||
type BatchRealMessageReceiver = mpsc::UnboundedReceiver<Vec<RealMessage>>;
|
||||
pub(crate) type BatchRealMessageSender =
|
||||
tokio::sync::mpsc::Sender<(Vec<RealMessage>, TransmissionLane)>;
|
||||
type BatchRealMessageReceiver = tokio::sync::mpsc::Receiver<(Vec<RealMessage>, TransmissionLane)>;
|
||||
|
||||
pub(crate) enum StreamMessage {
|
||||
Cover,
|
||||
@@ -266,22 +181,23 @@ where
|
||||
rng: R,
|
||||
our_full_destination: Recipient,
|
||||
topology_access: TopologyAccessor,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
) -> Self {
|
||||
OutQueueControl {
|
||||
config,
|
||||
ack_key,
|
||||
sent_notifier,
|
||||
next_delay: None,
|
||||
sending_rate_controller: SendingDelayController::new(
|
||||
MIN_DELAY_MULTIPLIER,
|
||||
MAX_DELAY_MULTIPLIER,
|
||||
),
|
||||
sending_delay_controller: Default::default(),
|
||||
mix_tx,
|
||||
real_receiver,
|
||||
our_full_destination,
|
||||
rng,
|
||||
topology_access,
|
||||
received_buffer: VecDeque::with_capacity(0), // we won't be putting any data into this guy directly
|
||||
transmission_buffer: Default::default(),
|
||||
closed_connection_rx,
|
||||
lane_queue_lengths,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +212,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
|
||||
@@ -315,20 +231,24 @@ 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))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,6 +256,16 @@ where
|
||||
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);
|
||||
}
|
||||
|
||||
// In addition to closing connections on receiving messages throught closed_connection_rx,
|
||||
// also close connections when sufficiently stale.
|
||||
self.transmission_buffer.prune_stale_connections();
|
||||
|
||||
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
|
||||
// the UnboundedReceiver [of mix_rx] will not get a chance to read anything
|
||||
// JS2: Basically it was the case that with high enough rate, the stream had already a next value
|
||||
@@ -347,44 +277,71 @@ where
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
fn on_close_connection(&mut self, connection_id: ConnectionId) {
|
||||
log::debug!("Removing lane for connection: {connection_id}");
|
||||
self.transmission_buffer
|
||||
.remove(&TransmissionLane::ConnectionId(connection_id));
|
||||
}
|
||||
|
||||
fn current_average_message_sending_delay(&self) -> Duration {
|
||||
self.config.average_message_sending_delay
|
||||
* self.sending_rate_controller.current_multiplier()
|
||||
* self.sending_delay_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()
|
||||
self.sending_delay_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();
|
||||
self.sending_delay_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_delay_controller.not_increased_delay_recently()
|
||||
{
|
||||
self.sending_rate_controller.increase_delay_multiplier();
|
||||
self.sending_delay_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();
|
||||
if self.sending_delay_controller.is_sending_reliable() {
|
||||
self.sending_delay_controller.decrease_delay_multiplier();
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_next_message(&mut self) -> Option<RealMessage> {
|
||||
// Pop the next message from the transmission buffer
|
||||
let (lane, real_next) = self.transmission_buffer.pop_next_message_at_random()?;
|
||||
|
||||
// Update the published queue length
|
||||
let lane_length = self.transmission_buffer.lane_length(&lane);
|
||||
self.lane_queue_lengths.set(&lane, lane_length);
|
||||
|
||||
Some(real_next)
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Start by checking if we have any incoming messages about closed connections
|
||||
// NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to
|
||||
// something simpler.
|
||||
if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) {
|
||||
match id {
|
||||
ConnectionCommand::Close(id) => self.on_close_connection(id),
|
||||
ConnectionCommand::ActiveConnections(_) => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -409,28 +366,32 @@ where
|
||||
next_delay.as_mut().reset(next_poisson_delay);
|
||||
}
|
||||
|
||||
// check if we have anything immediately available
|
||||
if let Some(real_available) = self.received_buffer.pop_front() {
|
||||
return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available))));
|
||||
}
|
||||
|
||||
// decide what kind of message to send
|
||||
match Pin::new(&mut self.real_receiver).poll_next(cx) {
|
||||
// On every iteration we get new messages from upstream. Given that these come bunched
|
||||
// in `Vec`, this ensures that on average we will fetch messages faster than we can
|
||||
// send, which is a condition for being able to multiplex sphinx packets from multiple
|
||||
// data streams.
|
||||
match Pin::new(&mut self.real_receiver).poll_recv(cx) {
|
||||
// in the case our real message channel stream was closed, we should also indicate we are closed
|
||||
// (and whoever is using the stream should panic)
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
|
||||
// if there are more messages available, return first one and store the rest
|
||||
Poll::Ready(Some(real_messages)) => {
|
||||
self.received_buffer = real_messages.into();
|
||||
// we MUST HAVE received at least ONE message
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(
|
||||
self.received_buffer.pop_front().unwrap(),
|
||||
))))
|
||||
Poll::Ready(Some((real_messages, conn_id))) => {
|
||||
log::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
let real_next = self.pop_next_message().expect("Just stored one");
|
||||
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
}
|
||||
|
||||
// otherwise construct a dummy one
|
||||
Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)),
|
||||
Poll::Pending => {
|
||||
if let Some(real_next) = self.pop_next_message() {
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
} else {
|
||||
// otherwise construct a dummy one
|
||||
Poll::Ready(Some(StreamMessage::Cover))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we never set an initial delay - let's do it now
|
||||
@@ -452,32 +413,36 @@ where
|
||||
}
|
||||
|
||||
fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
|
||||
// check if we have anything immediately available
|
||||
if let Some(real_available) = self.received_buffer.pop_front() {
|
||||
// if there are more messages immediately available, notify the runtime
|
||||
// because we should be polled again
|
||||
if !self.received_buffer.is_empty() {
|
||||
cx.waker().wake_by_ref()
|
||||
// Start by checking if we have any incoming messages about closed connections
|
||||
if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) {
|
||||
match id {
|
||||
ConnectionCommand::Close(id) => self.on_close_connection(id),
|
||||
ConnectionCommand::ActiveConnections(_) => panic!(),
|
||||
}
|
||||
return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available))));
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.real_receiver).poll_next(cx) {
|
||||
match Pin::new(&mut self.real_receiver).poll_recv(cx) {
|
||||
// in the case our real message channel stream was closed, we should also indicate we are closed
|
||||
// (and whoever is using the stream should panic)
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
|
||||
// if there are more messages available, return first one and store the rest
|
||||
Poll::Ready(Some(real_messages)) => {
|
||||
self.received_buffer = real_messages.into();
|
||||
// we MUST HAVE received at least ONE message
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(
|
||||
self.received_buffer.pop_front().unwrap(),
|
||||
))))
|
||||
Poll::Ready(Some((real_messages, conn_id))) => {
|
||||
log::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
|
||||
// First store what we got for the given connection id
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
let real_next = self.pop_next_message().expect("we just added one");
|
||||
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
}
|
||||
|
||||
// if there's nothing, then there's nothing
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Pending => {
|
||||
if let Some(real_next) = self.pop_next_message() {
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,24 +457,47 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn log_status(&self) {
|
||||
let packets = self.transmission_buffer.total_size();
|
||||
let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0;
|
||||
let lanes = self.transmission_buffer.num_lanes();
|
||||
let mult = self.sending_delay_controller.current_multiplier();
|
||||
let delay = self.current_average_message_sending_delay().as_millis();
|
||||
if self.config.disable_poisson_packet_distribution {
|
||||
log::info!(
|
||||
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay",
|
||||
backlog
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})",
|
||||
backlog,
|
||||
delay
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
|
||||
debug!("Started OutQueueControl with graceful shutdown support");
|
||||
|
||||
let mut status_timer = tokio::time::interval(Duration::from_secs(1));
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("OutQueueControl: Received shutdown");
|
||||
}
|
||||
next_message = self.next() => match next_message {
|
||||
Some(next_message) => {
|
||||
self.on_message(next_message).await;
|
||||
},
|
||||
None => {
|
||||
log::trace!("OutQueueControl: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
_ = status_timer.tick() => {
|
||||
self.log_status();
|
||||
}
|
||||
next_message = self.next() => if let Some(next_message) = next_message {
|
||||
self.on_message(next_message).await;
|
||||
} else {
|
||||
log::trace!("OutQueueControl: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::get_time_now;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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;
|
||||
|
||||
pub(crate) 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,
|
||||
}
|
||||
|
||||
impl Default for SendingDelayController {
|
||||
fn default() -> Self {
|
||||
SendingDelayController::new(MIN_DELAY_MULTIPLIER, MAX_DELAY_MULTIPLIER)
|
||||
}
|
||||
}
|
||||
|
||||
impl SendingDelayController {
|
||||
pub(crate) 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_multiplier(&self) -> u32 {
|
||||
self.current_multiplier
|
||||
}
|
||||
|
||||
pub(crate) fn increase_delay_multiplier(&mut self) {
|
||||
if self.current_multiplier < self.upper_bound {
|
||||
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
|
||||
);
|
||||
} else {
|
||||
log::warn!("Trying to increase delay multipler higher than allowed");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decrease_delay_multiplier(&mut self) {
|
||||
if self.current_multiplier > self.lower_bound {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_backpressure_detected(&mut self) {
|
||||
self.time_when_backpressure_detected = get_time_now();
|
||||
}
|
||||
|
||||
pub(crate) fn not_increased_delay_recently(&self) -> bool {
|
||||
get_time_now()
|
||||
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
|
||||
}
|
||||
|
||||
pub(crate) 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
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use client_connections::TransmissionLane;
|
||||
use rand::seq::SliceRandom;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_timer;
|
||||
|
||||
use super::{get_time_now, RealMessage};
|
||||
|
||||
// The number of lanes included in the oldest set. Used when we need to prioritize traffic.
|
||||
const OLDEST_LANE_SET_SIZE: usize = 5;
|
||||
// As a way of prune connections we also check for timeouts.
|
||||
const MSG_CONSIDERED_STALE_AFTER_SECS: u64 = 10 * 60;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TransmissionBuffer {
|
||||
buffer: HashMap<TransmissionLane, LaneBufferEntry>,
|
||||
}
|
||||
|
||||
impl TransmissionBuffer {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.buffer.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, lane: &TransmissionLane) -> Option<LaneBufferEntry> {
|
||||
self.buffer.remove(lane)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn num_lanes(&self) -> usize {
|
||||
self.buffer.keys().count()
|
||||
}
|
||||
|
||||
pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option<usize> {
|
||||
self.buffer.get(lane).map(LaneBufferEntry::len)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn connections(&self) -> HashSet<u64> {
|
||||
self.buffer
|
||||
.keys()
|
||||
.filter_map(|lane| match lane {
|
||||
TransmissionLane::ConnectionId(id) => Some(id),
|
||||
_ => None,
|
||||
})
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn total_size(&self) -> usize {
|
||||
self.buffer.values().map(LaneBufferEntry::len).sum()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn total_size_in_bytes(&self) -> usize {
|
||||
self.buffer
|
||||
.values()
|
||||
.map(|lane_buffer_entry| {
|
||||
lane_buffer_entry
|
||||
.real_messages
|
||||
.iter()
|
||||
.map(|real_message| real_message.mix_packet.sphinx_packet().len())
|
||||
.sum::<usize>()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn get_oldest_set(&self) -> Vec<TransmissionLane> {
|
||||
let mut buffer: Vec<_> = self
|
||||
.buffer
|
||||
.iter()
|
||||
.map(|(k, v)| (k, v.messages_transmitted))
|
||||
.collect();
|
||||
buffer.sort_by_key(|v| v.1);
|
||||
buffer
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|(k, _)| *k)
|
||||
.take(OLDEST_LANE_SET_SIZE)
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn store(&mut self, lane: &TransmissionLane, real_messages: Vec<RealMessage>) {
|
||||
if let Some(lane_buffer_entry) = self.buffer.get_mut(lane) {
|
||||
lane_buffer_entry.append(real_messages);
|
||||
} else {
|
||||
self.buffer
|
||||
.insert(*lane, LaneBufferEntry::new(real_messages));
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_random_lane(&self) -> Option<&TransmissionLane> {
|
||||
let lanes: Vec<&TransmissionLane> = self.buffer.keys().collect();
|
||||
lanes.choose(&mut rand::thread_rng()).copied()
|
||||
}
|
||||
|
||||
fn pick_random_small_lane(&self) -> Option<&TransmissionLane> {
|
||||
let lanes: Vec<&TransmissionLane> = self
|
||||
.buffer
|
||||
.iter()
|
||||
.filter(|(_, v)| v.is_small())
|
||||
.map(|(k, _)| k)
|
||||
.collect();
|
||||
lanes.choose(&mut rand::thread_rng()).copied()
|
||||
}
|
||||
|
||||
fn pick_random_old_lane(&self) -> Option<TransmissionLane> {
|
||||
let lanes = self.get_oldest_set();
|
||||
lanes.choose(&mut rand::thread_rng()).copied()
|
||||
}
|
||||
|
||||
fn pop_front_from_lane(&mut self, lane: &TransmissionLane) -> Option<RealMessage> {
|
||||
let real_msgs_queued = self.buffer.get_mut(lane)?;
|
||||
let real_next = real_msgs_queued.pop_front()?;
|
||||
real_msgs_queued.messages_transmitted += 1;
|
||||
if real_msgs_queued.is_empty() {
|
||||
self.buffer.remove(lane);
|
||||
}
|
||||
Some(real_next)
|
||||
}
|
||||
|
||||
pub(crate) fn pop_next_message_at_random(&mut self) -> Option<(TransmissionLane, RealMessage)> {
|
||||
if self.buffer.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Very basic heuristic where we prioritize according to small lanes first, the older lanes
|
||||
// to try to finish lanes when possible, then the rest.
|
||||
let lane = if let Some(small_lane) = self.pick_random_small_lane() {
|
||||
*small_lane
|
||||
} else if let Some(old_lane) = self.pick_random_old_lane() {
|
||||
old_lane
|
||||
} else {
|
||||
*self.pick_random_lane()?
|
||||
};
|
||||
|
||||
let msg = self.pop_front_from_lane(&lane)?;
|
||||
log::trace!("picking to send from lane: {:?}", lane);
|
||||
Some((lane, msg))
|
||||
}
|
||||
|
||||
pub(crate) fn prune_stale_connections(&mut self) {
|
||||
let stale_entries: Vec<_> = self
|
||||
.buffer
|
||||
.iter()
|
||||
.filter_map(|(lane, entry)| if entry.is_stale() { Some(lane) } else { None })
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
for lane in stale_entries {
|
||||
self.remove(&lane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LaneBufferEntry {
|
||||
pub real_messages: VecDeque<RealMessage>,
|
||||
pub messages_transmitted: usize,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub time_for_last_activity: time::Instant,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub time_for_last_activity: wasm_timer::Instant,
|
||||
}
|
||||
|
||||
impl LaneBufferEntry {
|
||||
fn new(real_messages: Vec<RealMessage>) -> Self {
|
||||
LaneBufferEntry {
|
||||
real_messages: real_messages.into(),
|
||||
messages_transmitted: 0,
|
||||
time_for_last_activity: get_time_now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append(&mut self, real_messages: Vec<RealMessage>) {
|
||||
self.real_messages.append(&mut real_messages.into());
|
||||
self.time_for_last_activity = get_time_now();
|
||||
}
|
||||
|
||||
fn pop_front(&mut self) -> Option<RealMessage> {
|
||||
self.real_messages.pop_front()
|
||||
}
|
||||
|
||||
fn is_small(&self) -> bool {
|
||||
self.real_messages.len() < 100
|
||||
}
|
||||
|
||||
fn is_stale(&self) -> bool {
|
||||
get_time_now() - self.time_for_last_activity
|
||||
> Duration::from_secs(MSG_CONSIDERED_STALE_AFTER_SECS)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.real_messages.len()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.real_messages.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ impl ReceivedMessagesBuffer {
|
||||
}
|
||||
|
||||
async fn handle_new_received(&mut self, msgs: Vec<Vec<u8>>) {
|
||||
debug!(
|
||||
trace!(
|
||||
"Processing {:?} new message that might get added to the buffer!",
|
||||
msgs.len()
|
||||
);
|
||||
|
||||
@@ -8,10 +8,13 @@ use nymsphinx::anonymous_replies::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ReplyKeyStorageError {
|
||||
#[error("DB Read Error: {0}")]
|
||||
DbReadError(sled::Error),
|
||||
#[error("DB Write Error: {0}")]
|
||||
DbWriteError(sled::Error),
|
||||
#[error("DB Open Error: {0}")]
|
||||
DbOpenError(sled::Error),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ cfg_if::cfg_if! {
|
||||
#[clap(author = "Nymtech", version, about)]
|
||||
struct Cli {
|
||||
/// Path pointing to an env file that configures the client.
|
||||
#[clap(long)]
|
||||
#[clap(short, long)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
|
||||
/// Path where the sqlite credental database will be located.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
@@ -27,11 +27,13 @@ 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
|
||||
|
||||
## internal
|
||||
client-core = { path = "../client-core" }
|
||||
client-connections = { path = "../../common/client-connections" }
|
||||
coconut-interface = { path = "../../common/coconut-interface", optional = true }
|
||||
config = { path = "../../common/config" }
|
||||
completions = { path = "../../common/completions" }
|
||||
@@ -49,6 +51,7 @@ topology = { path = "../../common/topology" }
|
||||
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
|
||||
version-checker = { path = "../../common/version-checker" }
|
||||
websocket-requests = { path = "websocket-requests" }
|
||||
tap = "1.0.1"
|
||||
|
||||
[features]
|
||||
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"]
|
||||
|
||||
@@ -43,6 +43,7 @@ async fn send_file_with_reply() {
|
||||
recipient,
|
||||
message: read_data,
|
||||
with_reply_surb: true,
|
||||
connection_id: 0,
|
||||
};
|
||||
|
||||
println!("sending content of 'dummy_file' over the mix network...");
|
||||
@@ -91,6 +92,7 @@ async fn send_file_without_reply() {
|
||||
recipient,
|
||||
message: read_data,
|
||||
with_reply_surb: false,
|
||||
connection_id: 0,
|
||||
};
|
||||
|
||||
println!("sending content of 'dummy_file' over the mix network...");
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use client_connections::{
|
||||
ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -18,6 +21,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;
|
||||
@@ -30,9 +34,11 @@ use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use tap::TapFallible;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::error::ClientError;
|
||||
use crate::websocket;
|
||||
|
||||
pub(crate) mod config;
|
||||
@@ -108,6 +114,7 @@ impl NymClient {
|
||||
stream.start_with_shutdown(shutdown);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_real_traffic_controller(
|
||||
&self,
|
||||
topology_accessor: TopologyAccessor,
|
||||
@@ -115,6 +122,8 @@ impl NymClient {
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
input_receiver: InputMessageReceiver,
|
||||
mix_sender: BatchMixMessageSender,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
let mut controller_config = real_messages_control::Config::new(
|
||||
@@ -144,6 +153,8 @@ impl NymClient {
|
||||
mix_sender,
|
||||
topology_accessor,
|
||||
reply_key_storage,
|
||||
lane_queue_lengths,
|
||||
closed_connection_rx,
|
||||
)
|
||||
.start_with_shutdown(shutdown);
|
||||
}
|
||||
@@ -232,7 +243,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(),
|
||||
@@ -247,14 +258,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)
|
||||
@@ -275,11 +288,18 @@ impl NymClient {
|
||||
&self,
|
||||
buffer_requester: ReceivedBufferRequestSender,
|
||||
msg_input: InputMessageSender,
|
||||
shared_lane_queue_lengths: LaneQueueLengths,
|
||||
closed_connection_tx: ClosedConnectionSender,
|
||||
) {
|
||||
info!("Starting websocket listener...");
|
||||
|
||||
let websocket_handler =
|
||||
websocket::Handler::new(msg_input, buffer_requester, self.as_mix_recipient());
|
||||
let websocket_handler = websocket::Handler::new(
|
||||
msg_input,
|
||||
closed_connection_tx,
|
||||
buffer_requester,
|
||||
&self.as_mix_recipient(),
|
||||
shared_lane_queue_lengths,
|
||||
);
|
||||
|
||||
websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler);
|
||||
}
|
||||
@@ -287,27 +307,43 @@ impl NymClient {
|
||||
/// EXPERIMENTAL DIRECT RUST API
|
||||
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
|
||||
/// well enough in local tests)
|
||||
pub fn send_message(&mut self, recipient: Recipient, message: Vec<u8>, with_reply_surb: bool) {
|
||||
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
|
||||
pub async fn send_message(
|
||||
&mut self,
|
||||
recipient: Recipient,
|
||||
message: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
) {
|
||||
let lane = TransmissionLane::General;
|
||||
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane);
|
||||
|
||||
self.input_tx
|
||||
if self
|
||||
.input_tx
|
||||
.as_ref()
|
||||
.expect("start method was not called before!")
|
||||
.unbounded_send(input_msg)
|
||||
.unwrap();
|
||||
.send(input_msg)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL DIRECT RUST API
|
||||
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
|
||||
/// well enough in local tests)
|
||||
pub fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec<u8>) {
|
||||
pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec<u8>) {
|
||||
let input_msg = InputMessage::new_reply(reply_surb, message);
|
||||
|
||||
self.input_tx
|
||||
if self
|
||||
.input_tx
|
||||
.as_ref()
|
||||
.expect("start method was not called before!")
|
||||
.unbounded_send(input_msg)
|
||||
.unwrap();
|
||||
.send(input_msg)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPERIMENTAL DIRECT RUST API
|
||||
@@ -328,8 +364,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!(
|
||||
@@ -346,9 +382,10 @@ 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
|
||||
@@ -363,7 +400,7 @@ impl NymClient {
|
||||
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
|
||||
|
||||
// channels responsible for controlling real messages
|
||||
let (input_sender, input_receiver) = mpsc::unbounded::<InputMessage>();
|
||||
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
|
||||
|
||||
// channels responsible for controlling ack messages
|
||||
let (ack_sender, ack_receiver) = mpsc::unbounded();
|
||||
@@ -371,7 +408,10 @@ impl NymClient {
|
||||
|
||||
let reply_key_storage =
|
||||
ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path())
|
||||
.expect("Failed to load reply key storage!");
|
||||
.tap_err(|err| {
|
||||
log::error!("Failed to load reply key storage - is it perhaps already in use?");
|
||||
log::error!("{}", err);
|
||||
})?;
|
||||
|
||||
// Shutdown notifier for signalling tasks to stop
|
||||
let shutdown = ShutdownNotifier::default();
|
||||
@@ -379,7 +419,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,
|
||||
@@ -398,12 +438,22 @@ impl NymClient {
|
||||
let sphinx_message_sender =
|
||||
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
|
||||
|
||||
// Channels that the websocket listener can use to signal downstream to the real traffic
|
||||
// controller that connections are closed.
|
||||
let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
|
||||
|
||||
// Shared queue length data. Published by the `OutQueueController` in the client, and used
|
||||
// primarily to throttle incoming connections (e.g socks5 for attached network-requesters)
|
||||
let shared_lane_queue_lengths = LaneQueueLengths::new();
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
reply_key_storage,
|
||||
ack_receiver,
|
||||
input_receiver,
|
||||
sphinx_message_sender.clone(),
|
||||
shared_lane_queue_lengths.clone(),
|
||||
closed_connection_rx,
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
|
||||
@@ -420,9 +470,12 @@ impl NymClient {
|
||||
}
|
||||
|
||||
match self.config.get_socket_type() {
|
||||
SocketType::WebSocket => {
|
||||
self.start_websocket_listener(received_buffer_request_sender, input_sender)
|
||||
}
|
||||
SocketType::WebSocket => self.start_websocket_listener(
|
||||
received_buffer_request_sender,
|
||||
input_sender,
|
||||
shared_lane_queue_lengths,
|
||||
closed_connection_tx,
|
||||
),
|
||||
SocketType::None => {
|
||||
// if we did not start the socket, it means we're running (supposedly) in the native mode
|
||||
// and hence we should announce 'ourselves' to the buffer
|
||||
@@ -443,6 +496,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};
|
||||
@@ -49,7 +50,7 @@ fn long_version_static() -> &'static str {
|
||||
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
|
||||
pub(crate) struct Cli {
|
||||
/// Path pointing to an env file that configures the client.
|
||||
#[clap(long)]
|
||||
#[clap(short, long)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
@@ -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,25 @@
|
||||
use client_core::{client::reply_key_storage::ReplyKeyStorageError, 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("Reply key storage error: {0}")]
|
||||
ReplyKeyStorageError(#[from] ReplyKeyStorageError),
|
||||
|
||||
#[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,21 +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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use client_connections::{
|
||||
ClosedConnectionSender, ConnectionCommand, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use client_core::client::{
|
||||
inbound_messages::{InputMessage, InputMessageSender},
|
||||
received_buffer::{
|
||||
@@ -34,10 +37,12 @@ impl Default for ReceivedResponseType {
|
||||
|
||||
pub(crate) struct Handler {
|
||||
msg_input: InputMessageSender,
|
||||
closed_connection_tx: ClosedConnectionSender,
|
||||
buffer_requester: ReceivedBufferRequestSender,
|
||||
self_full_address: Recipient,
|
||||
socket: Option<WebSocketStream<TcpStream>>,
|
||||
received_response_type: ReceivedResponseType,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
}
|
||||
|
||||
// clone is used to use handler on a new connection, which initially is `None`
|
||||
@@ -45,10 +50,12 @@ impl Clone for Handler {
|
||||
fn clone(&self) -> Self {
|
||||
Handler {
|
||||
msg_input: self.msg_input.clone(),
|
||||
closed_connection_tx: self.closed_connection_tx.clone(),
|
||||
buffer_requester: self.buffer_requester.clone(),
|
||||
self_full_address: self.self_full_address,
|
||||
socket: None,
|
||||
received_response_type: Default::default(),
|
||||
lane_queue_lengths: self.lane_queue_lengths.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,38 +71,61 @@ impl Drop for Handler {
|
||||
impl Handler {
|
||||
pub(crate) fn new(
|
||||
msg_input: InputMessageSender,
|
||||
closed_connection_tx: ClosedConnectionSender,
|
||||
buffer_requester: ReceivedBufferRequestSender,
|
||||
self_full_address: Recipient,
|
||||
self_full_address: &Recipient,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
) -> Self {
|
||||
Handler {
|
||||
msg_input,
|
||||
closed_connection_tx,
|
||||
buffer_requester,
|
||||
self_full_address,
|
||||
self_full_address: *self_full_address,
|
||||
socket: None,
|
||||
received_response_type: Default::default(),
|
||||
lane_queue_lengths,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_send(
|
||||
async fn handle_send(
|
||||
&mut self,
|
||||
recipient: Recipient,
|
||||
recipient: &Recipient,
|
||||
message: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
connection_id: u64,
|
||||
) -> Option<ServerResponse> {
|
||||
// the ack control is now responsible for chunking, etc.
|
||||
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
|
||||
self.msg_input.unbounded_send(input_msg).unwrap();
|
||||
let lane = TransmissionLane::ConnectionId(connection_id);
|
||||
let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane);
|
||||
if self.msg_input.send(input_msg).await.is_err() {
|
||||
panic!();
|
||||
}
|
||||
|
||||
// on receiving a send, we reply back the current lane queue length for that connection id.
|
||||
// Note that this does _NOT_ take into account the packets that have been received but not
|
||||
// yet reach `OutQueueControl`, so it might be a tad low.
|
||||
if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() {
|
||||
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
|
||||
return Some(ServerResponse::LaneQueueLength(connection_id, queue_length));
|
||||
}
|
||||
|
||||
log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length");
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_reply(&mut self, reply_surb: ReplySurb, message: Vec<u8>) -> Option<ServerResponse> {
|
||||
async fn handle_reply(
|
||||
&mut self,
|
||||
reply_surb: ReplySurb,
|
||||
message: Vec<u8>,
|
||||
) -> Option<ServerResponse> {
|
||||
if message.len() > ReplySurb::max_msg_len(Default::default()) {
|
||||
return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default()))));
|
||||
}
|
||||
|
||||
let input_msg = InputMessage::new_reply(reply_surb, message);
|
||||
self.msg_input.unbounded_send(input_msg).unwrap();
|
||||
if self.msg_input.send(input_msg).await.is_err() {
|
||||
panic!();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -104,22 +134,48 @@ impl Handler {
|
||||
ServerResponse::SelfAddress(self.self_full_address)
|
||||
}
|
||||
|
||||
fn handle_request(&mut self, request: ClientRequest) -> Option<ServerResponse> {
|
||||
fn handle_closed_connection(&self, connection_id: u64) -> Option<ServerResponse> {
|
||||
self.closed_connection_tx
|
||||
.unbounded_send(ConnectionCommand::Close(connection_id))
|
||||
.unwrap();
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option<ServerResponse> {
|
||||
let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else {
|
||||
log::warn!(
|
||||
"Failed to get the lane queue length lock, not responding back with the current queue length"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
let lane = TransmissionLane::ConnectionId(connection_id);
|
||||
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
|
||||
Some(ServerResponse::LaneQueueLength(connection_id, queue_length))
|
||||
}
|
||||
|
||||
async fn handle_request(&mut self, request: ClientRequest) -> Option<ServerResponse> {
|
||||
match request {
|
||||
ClientRequest::Send {
|
||||
recipient,
|
||||
message,
|
||||
with_reply_surb,
|
||||
} => self.handle_send(recipient, message, with_reply_surb),
|
||||
connection_id,
|
||||
} => {
|
||||
self.handle_send(&recipient, message, with_reply_surb, connection_id)
|
||||
.await
|
||||
}
|
||||
ClientRequest::Reply {
|
||||
message,
|
||||
reply_surb,
|
||||
} => self.handle_reply(reply_surb, message),
|
||||
} => self.handle_reply(reply_surb, message).await,
|
||||
ClientRequest::SelfAddress => Some(self.handle_self_address()),
|
||||
ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id),
|
||||
ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> {
|
||||
async fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> {
|
||||
debug!("Handling text message request");
|
||||
trace!("Content: {:?}", msg);
|
||||
|
||||
@@ -128,13 +184,13 @@ impl Handler {
|
||||
|
||||
let response = match client_request {
|
||||
Err(err) => Some(ServerResponse::Error(err)),
|
||||
Ok(req) => self.handle_request(req),
|
||||
Ok(req) => self.handle_request(req).await,
|
||||
};
|
||||
|
||||
response.map(|resp| WsMessage::text(resp.into_text()))
|
||||
}
|
||||
|
||||
fn handle_binary_message(&mut self, msg: Vec<u8>) -> Option<WsMessage> {
|
||||
async fn handle_binary_message(&mut self, msg: &[u8]) -> Option<WsMessage> {
|
||||
debug!("Handling binary message request");
|
||||
|
||||
self.received_response_type = ReceivedResponseType::Binary;
|
||||
@@ -142,49 +198,23 @@ impl Handler {
|
||||
|
||||
let response = match client_request {
|
||||
Err(err) => Some(ServerResponse::Error(err)),
|
||||
Ok(req) => self.handle_request(req),
|
||||
Ok(req) => self.handle_request(req).await,
|
||||
};
|
||||
|
||||
response.map(|resp| WsMessage::Binary(resp.into_binary()))
|
||||
}
|
||||
|
||||
fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option<WsMessage> {
|
||||
async fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option<WsMessage> {
|
||||
// apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore
|
||||
// them and let's test that claim. If that's not the case, just copy code from
|
||||
// old version of this file.
|
||||
match raw_request {
|
||||
WsMessage::Text(text_message) => self.handle_text_message(text_message),
|
||||
WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message),
|
||||
WsMessage::Text(text_message) => self.handle_text_message(text_message).await,
|
||||
WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message).await,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
|
||||
// let's just play along for now
|
||||
fn prepare_reconstructed_binary(
|
||||
&self,
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
|
||||
// let's just play along for now
|
||||
fn prepare_reconstructed_text(
|
||||
&self,
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn push_websocket_received_plaintexts(
|
||||
&mut self,
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
@@ -193,10 +223,8 @@ impl Handler {
|
||||
// if it's text or binary, but for time being we use the naive assumption that if
|
||||
// client is sending Message::Text it expects text back. Same for Message::Binary
|
||||
let response_messages = match self.received_response_type {
|
||||
ReceivedResponseType::Binary => {
|
||||
self.prepare_reconstructed_binary(reconstructed_messages)
|
||||
}
|
||||
ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages),
|
||||
ReceivedResponseType::Binary => prepare_reconstructed_binary(reconstructed_messages),
|
||||
ReceivedResponseType::Text => prepare_reconstructed_text(reconstructed_messages),
|
||||
};
|
||||
|
||||
let mut send_stream = futures::stream::iter(response_messages);
|
||||
@@ -244,7 +272,7 @@ impl Handler {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(response) = self.handle_ws_request(socket_msg) {
|
||||
if let Some(response) = self.handle_ws_request(socket_msg).await {
|
||||
if let Err(err) = self.send_websocket_response(response).await {
|
||||
warn!(
|
||||
"Failed to send message over websocket: {}. Assuming the connection is dead.",
|
||||
@@ -291,3 +319,27 @@ impl Handler {
|
||||
self.listen_for_requests(reconstructed_receiver).await;
|
||||
}
|
||||
}
|
||||
|
||||
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
|
||||
// let's just play along for now
|
||||
fn prepare_reconstructed_binary(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
|
||||
// let's just play along for now
|
||||
fn prepare_reconstructed_text(
|
||||
reconstructed_messages: Vec<ReconstructedMessage>,
|
||||
) -> Vec<Result<WsMessage, WsError>> {
|
||||
reconstructed_messages
|
||||
.into_iter()
|
||||
.map(ServerResponse::Received)
|
||||
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ pub const REPLY_REQUEST_TAG: u8 = 0x01;
|
||||
/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`]
|
||||
pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02;
|
||||
|
||||
/// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`]
|
||||
pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03;
|
||||
|
||||
/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`]
|
||||
pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug)]
|
||||
pub enum ClientRequest {
|
||||
@@ -28,32 +34,42 @@ pub enum ClientRequest {
|
||||
message: Vec<u8>,
|
||||
// Perhaps we could change it to a number to indicate how many reply_SURBs we want to include?
|
||||
with_reply_surb: bool,
|
||||
connection_id: u64,
|
||||
},
|
||||
Reply {
|
||||
message: Vec<u8>,
|
||||
reply_surb: ReplySurb,
|
||||
},
|
||||
SelfAddress,
|
||||
ClosedConnection(u64),
|
||||
GetLaneQueueLength(u64),
|
||||
}
|
||||
|
||||
// we could have been parsing it directly TryFrom<WsMessage>, but we want to retain
|
||||
// information about whether it came from binary or text to send appropriate response back
|
||||
impl ClientRequest {
|
||||
// SEND_REQUEST_TAG || with_surb || recipient || data_len || data
|
||||
fn serialize_send(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Vec<u8> {
|
||||
// SEND_REQUEST_TAG || with_surb || recipient || conn_id || data_len || data
|
||||
fn serialize_send(
|
||||
recipient: Recipient,
|
||||
data: Vec<u8>,
|
||||
with_reply_surb: bool,
|
||||
connection_id: u64,
|
||||
) -> Vec<u8> {
|
||||
let data_len_bytes = (data.len() as u64).to_be_bytes();
|
||||
let conn_id_bytes = connection_id.to_be_bytes();
|
||||
std::iter::once(SEND_REQUEST_TAG)
|
||||
.chain(std::iter::once(with_reply_surb as u8))
|
||||
.chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant
|
||||
.chain(conn_id_bytes.iter().cloned())
|
||||
.chain(data_len_bytes.iter().cloned())
|
||||
.chain(data.into_iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// SEND_REQUEST_TAG || with_reply || recipient || data_len || data
|
||||
// SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data
|
||||
fn deserialize_send(b: &[u8]) -> Result<Self, error::Error> {
|
||||
// we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof<u64> bytes
|
||||
if b.len() < 2 + Recipient::LEN + size_of::<u64>() {
|
||||
// we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof<u64> bytes
|
||||
if b.len() < 2 + Recipient::LEN + 2 * size_of::<u64>() {
|
||||
return Err(error::Error::new(
|
||||
ErrorKind::TooShortRequest,
|
||||
"not enough data provided to recover 'send'".to_string(),
|
||||
@@ -86,9 +102,15 @@ impl ClientRequest {
|
||||
}
|
||||
};
|
||||
|
||||
let data_len_bytes = &b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::<u64>()];
|
||||
let mut connection_id_bytes = [0u8; size_of::<u64>()];
|
||||
connection_id_bytes
|
||||
.copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::<u64>()]);
|
||||
let connection_id = u64::from_be_bytes(connection_id_bytes);
|
||||
|
||||
let data_len_bytes =
|
||||
&b[2 + Recipient::LEN + size_of::<u64>()..2 + Recipient::LEN + 2 * size_of::<u64>()];
|
||||
let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap());
|
||||
let data = &b[2 + Recipient::LEN + size_of::<u64>()..];
|
||||
let data = &b[2 + Recipient::LEN + 2 * size_of::<u64>()..];
|
||||
if data.len() as u64 != data_len {
|
||||
return Err(error::Error::new(
|
||||
ErrorKind::MalformedRequest,
|
||||
@@ -104,11 +126,12 @@ impl ClientRequest {
|
||||
with_reply_surb,
|
||||
recipient,
|
||||
message: data.to_vec(),
|
||||
connection_id,
|
||||
})
|
||||
}
|
||||
|
||||
// REPLY_REQUEST_TAG || surb_len || surb || message_len || message
|
||||
fn serialize_reply(message: Vec<u8>, reply_surb: ReplySurb) -> Vec<u8> {
|
||||
fn serialize_reply(message: Vec<u8>, reply_surb: &ReplySurb) -> Vec<u8> {
|
||||
let reply_surb_bytes = reply_surb.to_bytes();
|
||||
let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes();
|
||||
let message_len_bytes = (message.len() as u64).to_be_bytes();
|
||||
@@ -202,20 +225,65 @@ impl ClientRequest {
|
||||
ClientRequest::SelfAddress
|
||||
}
|
||||
|
||||
// CLOSED_CONNECTION_REQUEST_TAG
|
||||
fn serialize_closed_connection(connection_id: u64) -> Vec<u8> {
|
||||
let conn_id_bytes = connection_id.to_be_bytes();
|
||||
std::iter::once(CLOSED_CONNECTION_REQUEST_TAG)
|
||||
.chain(conn_id_bytes.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// CLOSED_CONNECTION_REQUEST_TAG
|
||||
fn deserialize_closed_connection(b: &[u8]) -> Self {
|
||||
// this MUST match because it was called by 'deserialize'
|
||||
debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG);
|
||||
|
||||
let mut connection_id_bytes = [0u8; size_of::<u64>()];
|
||||
connection_id_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
|
||||
let connection_id = u64::from_be_bytes(connection_id_bytes);
|
||||
|
||||
ClientRequest::ClosedConnection(connection_id)
|
||||
}
|
||||
|
||||
// GET_LANE_QUEUE_LENGHT_TAG
|
||||
fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec<u8> {
|
||||
let conn_id_bytes = connection_id.to_be_bytes();
|
||||
std::iter::once(GET_LANE_QUEUE_LENGHT_TAG)
|
||||
.chain(conn_id_bytes.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// GET_LANE_QUEUE_LENGHT_TAG
|
||||
fn deserialize_get_lane_queue_length(b: &[u8]) -> Self {
|
||||
// this MUST match because it was called by 'deserialize'
|
||||
debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG);
|
||||
|
||||
let mut connection_id_bytes = [0u8; size_of::<u64>()];
|
||||
connection_id_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
|
||||
let connection_id = u64::from_be_bytes(connection_id_bytes);
|
||||
|
||||
ClientRequest::GetLaneQueueLength(connection_id)
|
||||
}
|
||||
|
||||
pub fn serialize(self) -> Vec<u8> {
|
||||
match self {
|
||||
ClientRequest::Send {
|
||||
recipient,
|
||||
message,
|
||||
with_reply_surb,
|
||||
} => Self::serialize_send(recipient, message, with_reply_surb),
|
||||
connection_id,
|
||||
} => Self::serialize_send(recipient, message, with_reply_surb, connection_id),
|
||||
|
||||
ClientRequest::Reply {
|
||||
message,
|
||||
reply_surb,
|
||||
} => Self::serialize_reply(message, reply_surb),
|
||||
} => Self::serialize_reply(message, &reply_surb),
|
||||
|
||||
ClientRequest::SelfAddress => Self::serialize_self_address(),
|
||||
|
||||
ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id),
|
||||
|
||||
ClientRequest::GetLaneQueueLength(id) => Self::serialize_get_lane_queue_lengths(id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,15 +313,17 @@ impl ClientRequest {
|
||||
SEND_REQUEST_TAG => Self::deserialize_send(b),
|
||||
REPLY_REQUEST_TAG => Self::deserialize_reply(b),
|
||||
SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)),
|
||||
CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)),
|
||||
GET_LANE_QUEUE_LENGHT_TAG => Ok(Self::deserialize_get_lane_queue_length(b)),
|
||||
n => Err(error::Error::new(
|
||||
ErrorKind::UnknownRequest,
|
||||
format!("type {}", n),
|
||||
format!("type {n}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_binary(raw_req: Vec<u8>) -> Result<Self, error::Error> {
|
||||
Self::deserialize(&raw_req)
|
||||
pub fn try_from_binary(raw_req: &[u8]) -> Result<Self, error::Error> {
|
||||
Self::deserialize(raw_req)
|
||||
}
|
||||
|
||||
pub fn try_from_text(raw_req: String) -> Result<Self, error::Error> {
|
||||
@@ -280,6 +350,7 @@ mod tests {
|
||||
recipient,
|
||||
message: b"foomp".to_vec(),
|
||||
with_reply_surb: false,
|
||||
connection_id: 42,
|
||||
};
|
||||
|
||||
let bytes = send_request_no_surb.serialize();
|
||||
@@ -289,10 +360,12 @@ mod tests {
|
||||
recipient,
|
||||
message,
|
||||
with_reply_surb,
|
||||
connection_id,
|
||||
} => {
|
||||
assert_eq!(recipient.to_string(), recipient_string);
|
||||
assert_eq!(message, b"foomp".to_vec());
|
||||
assert!(!with_reply_surb)
|
||||
assert!(!with_reply_surb);
|
||||
assert_eq!(connection_id, 42)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -301,6 +374,7 @@ mod tests {
|
||||
recipient,
|
||||
message: b"foomp".to_vec(),
|
||||
with_reply_surb: true,
|
||||
connection_id: 213,
|
||||
};
|
||||
|
||||
let bytes = send_request_surb.serialize();
|
||||
@@ -310,10 +384,12 @@ mod tests {
|
||||
recipient,
|
||||
message,
|
||||
with_reply_surb,
|
||||
connection_id,
|
||||
} => {
|
||||
assert_eq!(recipient.to_string(), recipient_string);
|
||||
assert_eq!(message, b"foomp".to_vec());
|
||||
assert!(with_reply_surb)
|
||||
assert!(with_reply_surb);
|
||||
assert_eq!(connection_id, 213)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -352,4 +428,15 @@ mod tests {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_connection_request_serialization_works() {
|
||||
let close_connection_request = ClientRequest::ClosedConnection(42);
|
||||
let bytes = close_connection_request.serialize();
|
||||
let recovered = ClientRequest::deserialize(&bytes).unwrap();
|
||||
match recovered {
|
||||
ClientRequest::ClosedConnection(id) => assert_eq!(id, 42),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,14 @@ pub const RECEIVED_RESPONSE_TAG: u8 = 0x01;
|
||||
/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`]
|
||||
pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02;
|
||||
|
||||
/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`]
|
||||
pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ServerResponse {
|
||||
Received(ReconstructedMessage),
|
||||
SelfAddress(Recipient),
|
||||
LaneQueueLength(u64, usize),
|
||||
Error(error::Error),
|
||||
}
|
||||
|
||||
@@ -193,6 +197,31 @@ impl ServerResponse {
|
||||
Ok(ServerResponse::SelfAddress(recipient))
|
||||
}
|
||||
|
||||
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
|
||||
fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec<u8> {
|
||||
std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG)
|
||||
.chain(lane.to_be_bytes().iter().cloned())
|
||||
.chain(queue_length.to_be_bytes().iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
|
||||
fn deserialize_lane_queue_length(b: &[u8]) -> Result<Self, error::Error> {
|
||||
// this MUST match because it was called by 'deserialize'
|
||||
debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG);
|
||||
|
||||
let mut lane_bytes = [0u8; size_of::<u64>()];
|
||||
lane_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
|
||||
let lane = u64::from_be_bytes(lane_bytes);
|
||||
|
||||
let mut queue_length_bytes = [0u8; size_of::<usize>()];
|
||||
queue_length_bytes
|
||||
.copy_from_slice(&b[1 + size_of::<u64>()..1 + size_of::<u64>() + size_of::<usize>()]);
|
||||
let queue_length = usize::from_be_bytes(queue_length_bytes);
|
||||
|
||||
Ok(ServerResponse::LaneQueueLength(lane, queue_length))
|
||||
}
|
||||
|
||||
// ERROR_RESPONSE_TAG || err_code || msg_len || msg
|
||||
fn serialize_error(error: error::Error) -> Vec<u8> {
|
||||
let message_len_bytes = (error.message.len() as u64).to_be_bytes();
|
||||
@@ -272,6 +301,9 @@ impl ServerResponse {
|
||||
Self::serialize_received(reconstructed_message)
|
||||
}
|
||||
ServerResponse::SelfAddress(address) => Self::serialize_self_address(address),
|
||||
ServerResponse::LaneQueueLength(lane, queue_length) => {
|
||||
Self::serialize_lane_queue_length(lane, queue_length)
|
||||
}
|
||||
ServerResponse::Error(err) => Self::serialize_error(err),
|
||||
}
|
||||
}
|
||||
@@ -302,6 +334,7 @@ impl ServerResponse {
|
||||
match response_tag {
|
||||
RECEIVED_RESPONSE_TAG => Self::deserialize_received(b),
|
||||
SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b),
|
||||
LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b),
|
||||
ERROR_RESPONSE_TAG => Self::deserialize_error(b),
|
||||
n => Err(error::Error::new(
|
||||
ErrorKind::UnknownResponse,
|
||||
@@ -378,6 +411,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_queue_length_response_serialization_works() {
|
||||
let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42);
|
||||
let bytes = lane_queue_length_response.serialize();
|
||||
let recovered = ServerResponse::deserialize(&bytes).unwrap();
|
||||
match recovered {
|
||||
ServerResponse::LaneQueueLength(lane, queue_length) => {
|
||||
assert_eq!(lane, 13);
|
||||
assert_eq!(queue_length, 42)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_response_serialization_works() {
|
||||
let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string());
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(super) enum ClientRequestText {
|
||||
message: String,
|
||||
recipient: String,
|
||||
with_reply_surb: bool,
|
||||
connection_id: u64,
|
||||
},
|
||||
SelfAddress,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -46,6 +47,7 @@ impl TryInto<ClientRequest> for ClientRequestText {
|
||||
message,
|
||||
recipient,
|
||||
with_reply_surb,
|
||||
connection_id,
|
||||
} => {
|
||||
let message_bytes = message.into_bytes();
|
||||
let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| {
|
||||
@@ -56,6 +58,7 @@ impl TryInto<ClientRequest> for ClientRequestText {
|
||||
message: message_bytes,
|
||||
recipient,
|
||||
with_reply_surb,
|
||||
connection_id,
|
||||
})
|
||||
}
|
||||
ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress),
|
||||
@@ -91,6 +94,10 @@ pub(super) enum ServerResponseText {
|
||||
SelfAddress {
|
||||
address: String,
|
||||
},
|
||||
LaneQueueLength {
|
||||
lane: u64,
|
||||
queue_length: usize,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
@@ -132,6 +139,9 @@ impl From<ServerResponse> for ServerResponseText {
|
||||
ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress {
|
||||
address: recipient.to_string(),
|
||||
},
|
||||
ServerResponse::LaneQueueLength(lane, queue_length) => {
|
||||
ServerResponseText::LaneQueueLength { lane, queue_length }
|
||||
}
|
||||
ServerResponse::Error(err) => ServerResponseText::Error {
|
||||
message: err.to_string(),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.0.2"
|
||||
version = "1.1.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
@@ -20,11 +20,13 @@ 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"
|
||||
|
||||
# internal
|
||||
client-core = { path = "../client-core" }
|
||||
client-connections = { path = "../../common/client-connections" }
|
||||
coconut-interface = { path = "../../common/coconut-interface", optional = true }
|
||||
config = { path = "../../common/config" }
|
||||
completions = { path = "../../common/completions" }
|
||||
@@ -44,6 +46,7 @@ task = { path = "../../common/task" }
|
||||
topology = { path = "../../common/topology" }
|
||||
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
|
||||
version-checker = { path = "../../common/version-checker" }
|
||||
tap = "1.0.1"
|
||||
|
||||
[features]
|
||||
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"]
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::error::Socks5ClientError;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
};
|
||||
use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths};
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -23,6 +25,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;
|
||||
@@ -34,6 +37,7 @@ use gateway_client::{
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use tap::TapFallible;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
pub mod config;
|
||||
@@ -108,6 +112,7 @@ impl NymClient {
|
||||
stream.start_with_shutdown(shutdown);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_real_traffic_controller(
|
||||
&self,
|
||||
topology_accessor: TopologyAccessor,
|
||||
@@ -115,6 +120,8 @@ impl NymClient {
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
input_receiver: InputMessageReceiver,
|
||||
mix_sender: BatchMixMessageSender,
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
let mut controller_config = client_core::client::real_messages_control::Config::new(
|
||||
@@ -144,6 +151,8 @@ impl NymClient {
|
||||
mix_sender,
|
||||
topology_accessor,
|
||||
reply_key_storage,
|
||||
lane_queue_lengths,
|
||||
closed_connection_rx,
|
||||
)
|
||||
.start_with_shutdown(shutdown);
|
||||
}
|
||||
@@ -232,7 +241,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(),
|
||||
@@ -247,14 +256,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)
|
||||
@@ -275,6 +286,8 @@ impl NymClient {
|
||||
&self,
|
||||
buffer_requester: ReceivedBufferRequestSender,
|
||||
msg_input: InputMessageSender,
|
||||
closed_connection_tx: ClosedConnectionSender,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown: ShutdownListener,
|
||||
) {
|
||||
info!("Starting socks5 listener...");
|
||||
@@ -287,14 +300,19 @@ impl NymClient {
|
||||
authenticator,
|
||||
self.config.get_provider_mix_address(),
|
||||
self.as_mix_recipient(),
|
||||
lane_queue_lengths,
|
||||
shutdown,
|
||||
);
|
||||
tokio::spawn(async move { sphinx_socks.serve(msg_input, buffer_requester).await });
|
||||
tokio::spawn(async move {
|
||||
sphinx_socks
|
||||
.serve(msg_input, buffer_requester, closed_connection_tx)
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
/// 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");
|
||||
@@ -305,11 +323,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);
|
||||
@@ -335,9 +357,10 @@ 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
|
||||
@@ -352,7 +375,7 @@ impl NymClient {
|
||||
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
|
||||
|
||||
// channels responsible for controlling real messages
|
||||
let (input_sender, input_receiver) = mpsc::unbounded::<InputMessage>();
|
||||
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
|
||||
|
||||
// channels responsible for controlling ack messages
|
||||
let (ack_sender, ack_receiver) = mpsc::unbounded();
|
||||
@@ -360,7 +383,10 @@ impl NymClient {
|
||||
|
||||
let reply_key_storage =
|
||||
ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path())
|
||||
.expect("Failed to load reply key storage!");
|
||||
.tap_err(|err| {
|
||||
log::error!("Failed to load reply key storage - is it perhaps already in use?");
|
||||
log::error!("{}", err);
|
||||
})?;
|
||||
|
||||
// Shutdown notifier for signalling tasks to stop
|
||||
let shutdown = ShutdownNotifier::default();
|
||||
@@ -368,7 +394,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,
|
||||
@@ -387,12 +413,22 @@ impl NymClient {
|
||||
let sphinx_message_sender =
|
||||
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
|
||||
|
||||
// Channel for announcing closed (socks5) connections by the controller.
|
||||
// This will be forwarded to `OutQueueControl`
|
||||
let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
|
||||
|
||||
// Shared queue length data. Published by the `OutQueueController` in the client, and used
|
||||
// primarily to throttle incoming connections
|
||||
let shared_lane_queue_lengths = LaneQueueLengths::new();
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
reply_key_storage,
|
||||
ack_receiver,
|
||||
input_receiver,
|
||||
sphinx_message_sender.clone(),
|
||||
closed_connection_rx,
|
||||
shared_lane_queue_lengths.clone(),
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
|
||||
@@ -411,12 +447,14 @@ impl NymClient {
|
||||
self.start_socks5_listener(
|
||||
received_buffer_request_sender,
|
||||
input_sender,
|
||||
closed_connection_tx,
|
||||
shared_lane_queue_lengths,
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
|
||||
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};
|
||||
@@ -50,7 +51,7 @@ fn long_version_static() -> &'static str {
|
||||
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
|
||||
pub(crate) struct Cli {
|
||||
/// Path pointing to an env file that configures the client.
|
||||
#[clap(long)]
|
||||
#[clap(short, long)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
@@ -61,8 +62,10 @@ pub(crate) struct Cli {
|
||||
pub(crate) enum Commands {
|
||||
/// Initialise a Nym client. Do this first!
|
||||
Init(init::Init),
|
||||
|
||||
/// Run the Nym client with provided configuration client optionally overriding set parameters
|
||||
Run(run::Run),
|
||||
|
||||
/// Try to upgrade the client
|
||||
Upgrade(upgrade::Upgrade),
|
||||
|
||||
@@ -83,16 +86,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,25 @@
|
||||
use client_core::{client::reply_key_storage::ReplyKeyStorageError, 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("Reply key storage error: {0}")]
|
||||
ReplyKeyStorageError(#[from] ReplyKeyStorageError),
|
||||
|
||||
#[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,21 +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 {
|
||||
|
||||
@@ -4,8 +4,8 @@ use super::authentication::{AuthenticationMethods, Authenticator, User};
|
||||
use super::request::{SocksCommand, SocksRequest};
|
||||
use super::types::{ResponseCode, SocksProxyError};
|
||||
use super::{RESERVED, SOCKS_VERSION};
|
||||
use client_core::client::inbound_messages::InputMessage;
|
||||
use client_core::client::inbound_messages::InputMessageSender;
|
||||
use client_connections::{LaneQueueLengths, TransmissionLane};
|
||||
use client_core::client::inbound_messages::{InputMessage, InputMessageSender};
|
||||
use futures::channel::mpsc;
|
||||
use futures::task::{Context, Poll};
|
||||
use log::*;
|
||||
@@ -141,6 +141,7 @@ pub(crate) struct SocksClient {
|
||||
service_provider: Recipient,
|
||||
self_address: Recipient,
|
||||
started_proxy: bool,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown_listener: ShutdownListener,
|
||||
}
|
||||
|
||||
@@ -158,6 +159,7 @@ impl Drop for SocksClient {
|
||||
|
||||
impl SocksClient {
|
||||
/// Create a new SOCKClient
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
stream: TcpStream,
|
||||
authenticator: Authenticator,
|
||||
@@ -165,6 +167,7 @@ impl SocksClient {
|
||||
service_provider: Recipient,
|
||||
controller_sender: ControllerSender,
|
||||
self_address: Recipient,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown_listener: ShutdownListener,
|
||||
) -> Self {
|
||||
let connection_id = Self::generate_random();
|
||||
@@ -179,6 +182,7 @@ impl SocksClient {
|
||||
service_provider,
|
||||
self_address,
|
||||
started_proxy: false,
|
||||
lane_queue_lengths,
|
||||
shutdown_listener,
|
||||
}
|
||||
}
|
||||
@@ -230,8 +234,15 @@ impl SocksClient {
|
||||
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
|
||||
let msg = Message::Request(req);
|
||||
|
||||
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
|
||||
self.input_sender.unbounded_send(input_message).unwrap();
|
||||
let input_message = InputMessage::new_fresh(
|
||||
self.service_provider,
|
||||
msg.into_bytes(),
|
||||
false,
|
||||
TransmissionLane::ConnectionId(self.connection_id),
|
||||
);
|
||||
if self.input_sender.send(input_message).await.is_err() {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) {
|
||||
@@ -254,12 +265,14 @@ impl SocksClient {
|
||||
conn_receiver,
|
||||
input_sender,
|
||||
connection_id,
|
||||
Some(self.lane_queue_lengths.clone()),
|
||||
self.shutdown_listener.clone(),
|
||||
)
|
||||
.run(move |conn_id, read_data, socket_closed| {
|
||||
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
|
||||
let provider_message = Message::Request(provider_request);
|
||||
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
|
||||
let lane = TransmissionLane::ConnectionId(conn_id);
|
||||
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false, lane)
|
||||
})
|
||||
.await
|
||||
.into_inner();
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::{
|
||||
mixnet_responses::MixnetResponseListener,
|
||||
types::{ResponseCode, SocksProxyError},
|
||||
};
|
||||
use client_connections::{ClosedConnectionSender, LaneQueueLengths};
|
||||
use client_core::client::{
|
||||
inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender,
|
||||
};
|
||||
@@ -20,6 +21,7 @@ pub struct SphinxSocksServer {
|
||||
listening_address: SocketAddr,
|
||||
service_provider: Recipient,
|
||||
self_address: Recipient,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown: ShutdownListener,
|
||||
}
|
||||
|
||||
@@ -30,6 +32,7 @@ impl SphinxSocksServer {
|
||||
authenticator: Authenticator,
|
||||
service_provider: Recipient,
|
||||
self_address: Recipient,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
shutdown: ShutdownListener,
|
||||
) -> Self {
|
||||
// hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can
|
||||
@@ -41,6 +44,7 @@ impl SphinxSocksServer {
|
||||
listening_address: format!("{}:{}", ip, port).parse().unwrap(),
|
||||
service_provider,
|
||||
self_address,
|
||||
lane_queue_lengths,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
@@ -51,13 +55,14 @@ impl SphinxSocksServer {
|
||||
&mut self,
|
||||
input_sender: InputMessageSender,
|
||||
buffer_requester: ReceivedBufferRequestSender,
|
||||
closed_connection_tx: ClosedConnectionSender,
|
||||
) -> Result<(), SocksProxyError> {
|
||||
let listener = TcpListener::bind(self.listening_address).await.unwrap();
|
||||
info!("Serving Connections...");
|
||||
|
||||
// controller for managing all active connections
|
||||
let (mut active_streams_controller, controller_sender) =
|
||||
Controller::new(self.shutdown.clone());
|
||||
Controller::new(closed_connection_tx, false, self.shutdown.clone());
|
||||
tokio::spawn(async move {
|
||||
active_streams_controller.run().await;
|
||||
});
|
||||
@@ -83,6 +88,7 @@ impl SphinxSocksServer {
|
||||
self.service_provider,
|
||||
controller_sender.clone(),
|
||||
self.self_address,
|
||||
self.lane_queue_lengths.clone(),
|
||||
self.shutdown.clone(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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.1.0"
|
||||
edition = "2021"
|
||||
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
@@ -19,16 +19,18 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"]
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.4"
|
||||
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.4"
|
||||
tokio = { version = "1.21.2", features = ["sync"] }
|
||||
url = "2.2"
|
||||
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
# internal
|
||||
client-core = { path = "../client-core", default-features = false, features = ["wasm"] }
|
||||
client-connections = { path = "../../common/client-connections" }
|
||||
coconut-interface = { path = "../../common/coconut-interface", optional = true }
|
||||
credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
@@ -55,6 +57,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'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
clippy:
|
||||
cargo clippy --all-features --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
test:
|
||||
wasm-pack test --node
|
||||
@@ -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
@@ -0,0 +1,217 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TS_DEFS: &'static str = r#"
|
||||
export interface BinaryMessage {
|
||||
kind: number,
|
||||
payload: Uint8Array;
|
||||
headers: string,
|
||||
}
|
||||
|
||||
export interface StringMessage {
|
||||
kind: number,
|
||||
payload: string;
|
||||
}
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "BinaryMessage")]
|
||||
pub type IBinaryMessage;
|
||||
#[wasm_bindgen(typescript_type = "StringMessage")]
|
||||
pub type IStringMessage;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BinaryMessage {
|
||||
pub kind: u8,
|
||||
pub payload: Vec<u8>,
|
||||
pub headers: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct StringMessage {
|
||||
pub kind: u8,
|
||||
pub payload: String,
|
||||
}
|
||||
|
||||
/// Create a new binary message with a user-specified `kind`.
|
||||
#[wasm_bindgen]
|
||||
pub fn create_binary_message(kind: u8, payload: Vec<u8>) -> Vec<u8> {
|
||||
create_binary_message_with_headers(kind, payload, "".to_string())
|
||||
}
|
||||
|
||||
/// Create a new message with a UTF-8 encoded string `payload` and a user-specified `kind`.
|
||||
#[wasm_bindgen]
|
||||
pub fn create_binary_message_from_string(kind: u8, payload: String) -> Vec<u8> {
|
||||
create_binary_message_with_headers(kind, payload.as_bytes().to_vec(), "".to_string())
|
||||
}
|
||||
|
||||
/// Create a new binary message with a user-specified `kind`, and `headers` as a string.
|
||||
#[wasm_bindgen]
|
||||
pub fn create_binary_message_with_headers(kind: u8, payload: Vec<u8>, headers: String) -> Vec<u8> {
|
||||
let headers = headers.as_bytes().to_vec();
|
||||
let size = (headers.len() as u64).to_be_bytes().to_vec();
|
||||
vec![vec![kind], size, headers, payload].concat()
|
||||
}
|
||||
|
||||
/// Parse the `kind` and byte array `payload` from a byte array
|
||||
#[wasm_bindgen]
|
||||
pub async fn parse_binary_message(message: Vec<u8>) -> Result<IBinaryMessage, JsError> {
|
||||
if message.len() < 2 {
|
||||
return Err(JsError::new(
|
||||
"Could not parse message, as less than 2 bytes long",
|
||||
));
|
||||
}
|
||||
|
||||
let (kind, _headers, payload) = parse_binary_payload(&message);
|
||||
|
||||
Ok(serde_wasm_bindgen::to_value(&BinaryMessage {
|
||||
kind,
|
||||
payload: payload.to_vec(),
|
||||
headers: "".to_string(),
|
||||
})
|
||||
.unwrap()
|
||||
.unchecked_into::<IBinaryMessage>())
|
||||
}
|
||||
|
||||
/// Parse the `kind` and byte array `payload` from a byte array with headers
|
||||
#[wasm_bindgen]
|
||||
pub async fn parse_binary_message_with_headers(
|
||||
message: Vec<u8>,
|
||||
) -> Result<IBinaryMessage, JsError> {
|
||||
if message.len() < 2 {
|
||||
return Err(JsError::new(
|
||||
"Could not parse message, as less than 2 bytes long",
|
||||
));
|
||||
}
|
||||
|
||||
let (kind, headers, payload) = parse_binary_payload(&message);
|
||||
|
||||
Ok(serde_wasm_bindgen::to_value(&BinaryMessage {
|
||||
kind,
|
||||
payload: payload.to_vec(),
|
||||
headers,
|
||||
})
|
||||
.unwrap()
|
||||
.unchecked_into::<IBinaryMessage>())
|
||||
}
|
||||
|
||||
/// Parse the `kind` and UTF-8 string `payload` from a byte array with headers
|
||||
#[wasm_bindgen]
|
||||
pub async fn parse_string_message_with_headers(
|
||||
message: Vec<u8>,
|
||||
) -> Result<IStringMessage, JsError> {
|
||||
if message.len() < 2 {
|
||||
return Err(JsError::new(
|
||||
"Could not parse message, as less than 2 bytes long",
|
||||
));
|
||||
}
|
||||
|
||||
let (kind, _headers, payload) = parse_binary_payload(&message);
|
||||
let payload = String::from_utf8_lossy(payload).into_owned();
|
||||
|
||||
Ok(
|
||||
serde_wasm_bindgen::to_value(&StringMessage { kind, payload })
|
||||
.unwrap()
|
||||
.unchecked_into::<IStringMessage>(),
|
||||
)
|
||||
}
|
||||
pub(crate) fn parse_binary_payload(message: &[u8]) -> (u8, String, &[u8]) {
|
||||
// 1st byte is the kind
|
||||
let kind = message[0];
|
||||
|
||||
// then the size as u64 big endian
|
||||
let mut size = [0u8; 8];
|
||||
size.clone_from_slice(&message[1..9]);
|
||||
let size = u64::from_be_bytes(size) as usize;
|
||||
|
||||
// then the headers
|
||||
let headers = String::from_utf8_lossy(&message[9..9 + size]).into_owned();
|
||||
|
||||
// finally the payload
|
||||
let payload = &message[9 + size..];
|
||||
|
||||
(kind, headers, payload)
|
||||
}
|
||||
|
||||
/// Parse the `kind` and UTF-8 string `payload` from a byte array
|
||||
#[wasm_bindgen]
|
||||
pub async fn parse_string_message(message: Vec<u8>) -> Result<IStringMessage, JsError> {
|
||||
if message.len() < 2 {
|
||||
return Err(JsError::new(
|
||||
"Could not parse message, as less than 2 bytes long",
|
||||
));
|
||||
}
|
||||
|
||||
let kind = message[0];
|
||||
let payload = String::from_utf8_lossy(&message[1..]).into_owned();
|
||||
|
||||
Ok(
|
||||
serde_wasm_bindgen::to_value(&StringMessage { kind, payload })
|
||||
.unwrap()
|
||||
.unchecked_into::<IStringMessage>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{create_binary_message_with_headers, parse_binary_payload};
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_binary_with_headers() {
|
||||
let message_as_bytes = create_binary_message_with_headers(
|
||||
42u8,
|
||||
vec![0u8, 1u8, 2u8],
|
||||
"test headers".to_string(),
|
||||
);
|
||||
|
||||
// calculate header size
|
||||
let headers = "test headers".as_bytes().to_vec();
|
||||
let size = headers.len();
|
||||
|
||||
// the expected size
|
||||
let expected_size = 12;
|
||||
assert_eq!(size, expected_size);
|
||||
|
||||
assert_eq!(message_as_bytes[0], 42u8);
|
||||
assert_eq!(message_as_bytes[1..9], 12u64.to_be_bytes());
|
||||
assert_eq!(
|
||||
message_as_bytes[9 + expected_size..9 + expected_size + 3],
|
||||
vec![0u8, 1u8, 2u8]
|
||||
);
|
||||
|
||||
let res = parse_binary_payload(&message_as_bytes);
|
||||
|
||||
assert_eq!(res.0, 42u8);
|
||||
assert_eq!(res.1, "test headers".to_string());
|
||||
assert_eq!(res.2, vec![0u8, 1u8, 2u8]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_binary_with_empty_headers() {
|
||||
let message_as_bytes =
|
||||
create_binary_message_with_headers(42u8, vec![0u8, 1u8, 2u8], "".to_string());
|
||||
|
||||
let expected_size = 0;
|
||||
|
||||
assert_eq!(message_as_bytes[0], 42u8);
|
||||
assert_eq!(message_as_bytes[1..9], 0u64.to_be_bytes());
|
||||
assert_eq!(
|
||||
message_as_bytes[9 + expected_size..9 + expected_size + 3],
|
||||
vec![0u8, 1u8, 2u8]
|
||||
);
|
||||
|
||||
let res = parse_binary_payload(&message_as_bytes);
|
||||
|
||||
assert_eq!(res.0, 42u8);
|
||||
assert_eq!(res.1, "".to_string());
|
||||
assert_eq!(res.2, vec![0u8, 1u8, 2u8]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use self::config::Config;
|
||||
use client_connections::{ClosedConnectionReceiver, LaneQueueLengths, TransmissionLane};
|
||||
use client_core::client::{
|
||||
cover_traffic_stream::LoopCoverTrafficStream,
|
||||
inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender},
|
||||
@@ -25,7 +26,7 @@ use nymsphinx::addressing::clients::Recipient;
|
||||
use rand::rngs::OsRng;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
use wasm_utils::console_log;
|
||||
|
||||
pub mod config;
|
||||
|
||||
@@ -44,6 +45,7 @@ pub struct NymClient {
|
||||
|
||||
// callbacks
|
||||
on_message: Option<js_sys::Function>,
|
||||
on_binary_message: Option<js_sys::Function>,
|
||||
on_gateway_connect: Option<js_sys::Function>,
|
||||
}
|
||||
|
||||
@@ -55,6 +57,7 @@ impl NymClient {
|
||||
config,
|
||||
key_manager: Self::setup_key_manager(),
|
||||
on_message: None,
|
||||
on_binary_message: None,
|
||||
on_gateway_connect: None,
|
||||
input_tx: None,
|
||||
}
|
||||
@@ -72,8 +75,11 @@ impl NymClient {
|
||||
self.on_message = Some(on_message);
|
||||
}
|
||||
|
||||
pub fn set_on_binary_message(&mut self, on_binary_message: js_sys::Function) {
|
||||
self.on_binary_message = Some(on_binary_message);
|
||||
}
|
||||
|
||||
pub fn set_on_gateway_connect(&mut self, on_connect: js_sys::Function) {
|
||||
console_log!("setting on connect...");
|
||||
self.on_gateway_connect = Some(on_connect)
|
||||
}
|
||||
|
||||
@@ -122,6 +128,8 @@ impl NymClient {
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
input_receiver: InputMessageReceiver,
|
||||
mix_sender: BatchMixMessageSender,
|
||||
closed_connection_rx: ClosedConnectionReceiver,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
) {
|
||||
let mut controller_config = real_messages_control::Config::new(
|
||||
self.key_manager.ack_key(),
|
||||
@@ -146,6 +154,8 @@ impl NymClient {
|
||||
input_receiver,
|
||||
mix_sender,
|
||||
topology_accessor,
|
||||
lane_queue_lengths,
|
||||
closed_connection_rx,
|
||||
)
|
||||
.start();
|
||||
}
|
||||
@@ -266,6 +276,7 @@ impl NymClient {
|
||||
received_buffer_request_sender: ReceivedBufferRequestSender,
|
||||
) {
|
||||
let on_message = self.on_message.take();
|
||||
let on_binary_message = self.on_binary_message.take();
|
||||
|
||||
spawn_local(async move {
|
||||
let (reconstructed_sender, mut reconstructed_receiver) = mpsc::unbounded();
|
||||
@@ -280,8 +291,14 @@ impl NymClient {
|
||||
let this = JsValue::null();
|
||||
|
||||
while let Some(reconstructed) = reconstructed_receiver.next().await {
|
||||
if let Some(ref callback) = on_message {
|
||||
for msg in reconstructed {
|
||||
for msg in reconstructed {
|
||||
if let Some(ref callback_binary) = on_binary_message {
|
||||
let arg1 = serde_wasm_bindgen::to_value(&msg.message).unwrap();
|
||||
callback_binary
|
||||
.call1(&this, &arg1)
|
||||
.expect("on binary message failed!");
|
||||
}
|
||||
if let Some(ref callback) = on_message {
|
||||
if msg.reply_surb.is_some() {
|
||||
console_log!("the received message contained a reply-surb that we do not know how to handle (yet)")
|
||||
}
|
||||
@@ -289,9 +306,6 @@ impl NymClient {
|
||||
let arg1 = serde_wasm_bindgen::to_value(&stringified).unwrap();
|
||||
callback.call1(&this, &arg1).expect("on message failed!");
|
||||
}
|
||||
} else {
|
||||
console_warn!("no on_message callback was specified. the received message content is getting dropped");
|
||||
console_log!("the raw messages: {:?}", reconstructed)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -312,12 +326,16 @@ impl NymClient {
|
||||
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
|
||||
|
||||
// channels responsible for controlling real messages
|
||||
let (input_sender, input_receiver) = mpsc::unbounded::<InputMessage>();
|
||||
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
|
||||
|
||||
// channels responsible for controlling ack messages
|
||||
let (ack_sender, ack_receiver) = mpsc::unbounded();
|
||||
let shared_topology_accessor = TopologyAccessor::new();
|
||||
|
||||
// Channel that the real traffix controller can listed to for closing connections.
|
||||
// Currently unused in the wasm client.
|
||||
let (_closed_connection_tx, closed_connection_rx) = mpsc::unbounded();
|
||||
|
||||
// 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())
|
||||
@@ -337,11 +355,17 @@ impl NymClient {
|
||||
// The MixTrafficController then sends the actual traffic
|
||||
let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client);
|
||||
|
||||
// Shared queue length data. Published by the `OutQueueController` in the client, and used
|
||||
// primarily to throttle incoming connections
|
||||
let shared_lane_queue_lengths = LaneQueueLengths::new();
|
||||
|
||||
self.start_real_traffic_controller(
|
||||
shared_topology_accessor.clone(),
|
||||
ack_receiver,
|
||||
input_receiver,
|
||||
sphinx_message_sender.clone(),
|
||||
closed_connection_rx,
|
||||
shared_lane_queue_lengths,
|
||||
);
|
||||
|
||||
if !self.config.debug.disable_loop_cover_traffic_stream {
|
||||
@@ -367,14 +391,20 @@ impl NymClient {
|
||||
console_log!("Sending {} bytes to {}", message.len(), recipient);
|
||||
|
||||
let recipient = Recipient::try_from_base58_string(recipient).unwrap();
|
||||
let lane = TransmissionLane::General;
|
||||
|
||||
let input_msg = InputMessage::new_fresh(recipient, message, false);
|
||||
let input_msg = InputMessage::new_fresh(recipient, message, false, lane);
|
||||
|
||||
self.input_tx
|
||||
if self
|
||||
.input_tx
|
||||
.as_ref()
|
||||
.expect("start method was not called before!")
|
||||
.unbounded_send(input_msg)
|
||||
.unwrap();
|
||||
.send(input_msg)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!();
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod binary_message_helper;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod client;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "client-connections"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
log = "0.4.17"
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use futures::channel::mpsc;
|
||||
|
||||
pub type ConnectionId = u64;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum TransmissionLane {
|
||||
General,
|
||||
Reply,
|
||||
Retransmission,
|
||||
Control,
|
||||
ConnectionId(ConnectionId),
|
||||
}
|
||||
|
||||
/// Announce connections that are closed, for whoever is interested.
|
||||
/// One usecase is that the network-requester and socks5-client wants to know about this, so that
|
||||
/// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester)
|
||||
pub type ClosedConnectionSender = mpsc::UnboundedSender<ConnectionCommand>;
|
||||
pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver<ConnectionCommand>;
|
||||
|
||||
pub enum ConnectionCommand {
|
||||
// Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard
|
||||
// transmission lanes.
|
||||
Close(ConnectionId),
|
||||
|
||||
// In the network requester for example, we usually want to broadcast active connections
|
||||
// regularly, so we know what connections we need to request lane queue lengths for from the
|
||||
// client.
|
||||
// In the socks5-client, this is not needed since have direct access to the lane queue lengths.
|
||||
ActiveConnections(Vec<ConnectionId>),
|
||||
}
|
||||
|
||||
// The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down
|
||||
// if needed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LaneQueueLengths(std::sync::Arc<std::sync::Mutex<LaneQueueLengthsInner>>);
|
||||
|
||||
impl LaneQueueLengths {
|
||||
pub fn new() -> Self {
|
||||
LaneQueueLengths(std::sync::Arc::new(std::sync::Mutex::new(
|
||||
LaneQueueLengthsInner {
|
||||
map: HashMap::new(),
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn set(&mut self, lane: &TransmissionLane, lane_length: Option<usize>) {
|
||||
match self.0.lock() {
|
||||
Ok(mut inner) => {
|
||||
if let Some(length) = lane_length {
|
||||
inner
|
||||
.map
|
||||
.entry(*lane)
|
||||
.and_modify(|e| *e = length)
|
||||
.or_insert(length);
|
||||
} else {
|
||||
inner.map.remove(lane);
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Failed to set lane queue length: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, lane: &TransmissionLane) -> Option<usize> {
|
||||
match self.0.lock() {
|
||||
Ok(inner) => inner.get(lane),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to get lane queue length: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LaneQueueLengths {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LaneQueueLengths {
|
||||
type Target = std::sync::Arc<std::sync::Mutex<LaneQueueLengthsInner>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LaneQueueLengthsInner {
|
||||
pub map: HashMap<TransmissionLane, usize>,
|
||||
}
|
||||
|
||||
impl LaneQueueLengthsInner {
|
||||
pub fn get(&self, lane: &TransmissionLane) -> Option<usize> {
|
||||
self.map.get(lane).copied()
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &usize> {
|
||||
self.map.values()
|
||||
}
|
||||
|
||||
pub fn modify<F>(&mut self, lane: &TransmissionLane, f: F)
|
||||
where
|
||||
F: FnOnce(&mut usize),
|
||||
{
|
||||
self.map.entry(*lane).and_modify(f);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,11 @@ rust-version = "1.56"
|
||||
base64 = "0.13"
|
||||
colored = "2.0"
|
||||
cw3 = "0.13.1"
|
||||
|
||||
coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" }
|
||||
contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" }
|
||||
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" }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{validator_api, ValidatorClientError};
|
||||
use coconut_dkg_common::dealer::ContractDealing;
|
||||
use coconut_dkg_common::types::DealerDetails;
|
||||
use mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use mixnet_contract_common::MixId;
|
||||
use mixnet_contract_common::{GatewayBond, IdentityKeyRef};
|
||||
@@ -16,7 +18,7 @@ use validator_api_requests::models::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
use crate::nymd::traits::MixnetQueryClient;
|
||||
use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient};
|
||||
#[cfg(feature = "nymd-client")]
|
||||
use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient};
|
||||
#[cfg(feature = "nymd-client")]
|
||||
@@ -43,6 +45,7 @@ pub struct Config {
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
rewarded_set_page_limit: Option<u32>,
|
||||
dealers_page_limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "nymd-client")]
|
||||
@@ -72,6 +75,7 @@ impl Config {
|
||||
gateway_page_limit: None,
|
||||
mixnode_delegations_page_limit: None,
|
||||
rewarded_set_page_limit: None,
|
||||
dealers_page_limit: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,6 +123,7 @@ pub struct Client<C> {
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
rewarded_set_page_limit: Option<u32>,
|
||||
dealers_page_limit: Option<u32>,
|
||||
|
||||
// ideally they would have been read-only, but unfortunately rust doesn't have such features
|
||||
pub validator_api: validator_api::Client,
|
||||
@@ -145,6 +150,7 @@ impl Client<SigningNymdClient> {
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
rewarded_set_page_limit: config.rewarded_set_page_limit,
|
||||
dealers_page_limit: config.dealers_page_limit,
|
||||
validator_api: validator_api_client,
|
||||
nymd: nymd_client,
|
||||
})
|
||||
@@ -178,6 +184,7 @@ impl Client<QueryNymdClient> {
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
rewarded_set_page_limit: config.rewarded_set_page_limit,
|
||||
dealers_page_limit: config.dealers_page_limit,
|
||||
validator_api: validator_api_client,
|
||||
nymd: nymd_client,
|
||||
})
|
||||
@@ -519,6 +526,82 @@ impl<C> Client<C> {
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_current_dealers(
|
||||
&self,
|
||||
) -> Result<Vec<DealerDetails>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync + Send,
|
||||
{
|
||||
let mut dealers = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_current_dealers_paged(start_after.take(), self.dealers_page_limit)
|
||||
.await?;
|
||||
dealers.append(&mut paged_response.dealers);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res.into_string())
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dealers)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_past_dealers(
|
||||
&self,
|
||||
) -> Result<Vec<DealerDetails>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync + Send,
|
||||
{
|
||||
let mut dealers = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_past_dealers_paged(start_after.take(), self.dealers_page_limit)
|
||||
.await?;
|
||||
dealers.append(&mut paged_response.dealers);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res.into_string())
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dealers)
|
||||
}
|
||||
|
||||
pub async fn get_all_nymd_epoch_dealings(
|
||||
&self,
|
||||
idx: usize,
|
||||
) -> Result<Vec<ContractDealing>, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync + Send,
|
||||
{
|
||||
let mut dealings = Vec::new();
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let mut paged_response = self
|
||||
.nymd
|
||||
.get_dealings_paged(idx, start_after.take(), self.dealers_page_limit)
|
||||
.await?;
|
||||
dealings.append(&mut paged_response.dealings);
|
||||
|
||||
if let Some(start_after_res) = paged_response.start_next_after {
|
||||
start_after = Some(start_after_res.into_string())
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dealings)
|
||||
}
|
||||
}
|
||||
|
||||
// validator-api wrappers
|
||||
|
||||
@@ -6,6 +6,9 @@ use cosmrs::tendermint::abci;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use coconut_bandwidth_contract_common::event_attributes::*;
|
||||
pub use coconut_dkg_common::event_attributes::*;
|
||||
|
||||
// it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response)
|
||||
// as theirs logs
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -65,6 +65,7 @@ pub struct Config {
|
||||
pub(crate) bandwidth_claim_contract_address: Option<AccountId>,
|
||||
pub(crate) coconut_bandwidth_contract_address: Option<AccountId>,
|
||||
pub(crate) multisig_contract_address: Option<AccountId>,
|
||||
pub(crate) coconut_dkg_contract_address: Option<AccountId>,
|
||||
// TODO: add this in later commits
|
||||
// pub(crate) gas_price: GasPrice,
|
||||
}
|
||||
@@ -118,6 +119,10 @@ impl Config {
|
||||
details.contracts.multisig_contract_address.as_ref(),
|
||||
prefix,
|
||||
)?,
|
||||
coconut_dkg_contract_address: Self::parse_optional_account(
|
||||
details.contracts.coconut_dkg_contract_address.as_ref(),
|
||||
prefix,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -275,6 +280,14 @@ impl<C> NymdClient<C> {
|
||||
self.config.multisig_contract_address.as_ref().unwrap()
|
||||
}
|
||||
|
||||
// TODO: this should get changed into Result<&AccountId, NymdError> (or Option<&AccountId> in future commits
|
||||
// note: what unwrap is doing here is just moving a failure that would have normally
|
||||
// occurred in `connect` when attempting to parse an empty address,
|
||||
// so it's not introducing new source of failure (just moves it)
|
||||
pub fn coconut_dkg_contract_address(&self) -> &AccountId {
|
||||
self.config.coconut_dkg_contract_address.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::{CosmWasmClient, NymdClient};
|
||||
use async_trait::async_trait;
|
||||
use coconut_dkg_common::dealer::{
|
||||
DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse,
|
||||
};
|
||||
use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg;
|
||||
use coconut_dkg_common::types::{EpochState, MinimumDepositResponse};
|
||||
use cosmrs::AccountId;
|
||||
|
||||
#[async_trait]
|
||||
pub trait DkgQueryClient {
|
||||
async fn get_current_epoch_state(&self) -> Result<EpochState, NymdError>;
|
||||
async fn get_dealer_details(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
) -> Result<DealerDetailsResponse, NymdError>;
|
||||
async fn get_current_dealers_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NymdError>;
|
||||
async fn get_past_dealers_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NymdError>;
|
||||
|
||||
async fn get_deposit_amount(&self) -> Result<MinimumDepositResponse, NymdError>;
|
||||
async fn get_dealings_paged(
|
||||
&self,
|
||||
idx: usize,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealingsResponse, NymdError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> DkgQueryClient for NymdClient<C>
|
||||
where
|
||||
C: CosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn get_current_epoch_state(&self) -> Result<EpochState, NymdError> {
|
||||
let request = DkgQueryMsg::GetCurrentEpochState {};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
async fn get_dealer_details(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
) -> Result<DealerDetailsResponse, NymdError> {
|
||||
let request = DkgQueryMsg::GetDealerDetails {
|
||||
dealer_address: address.to_string(),
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_current_dealers_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NymdError> {
|
||||
let request = DkgQueryMsg::GetCurrentDealers {
|
||||
start_after,
|
||||
limit: page_limit,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_past_dealers_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NymdError> {
|
||||
let request = DkgQueryMsg::GetPastDealers {
|
||||
start_after,
|
||||
limit: page_limit,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_deposit_amount(&self) -> Result<MinimumDepositResponse, NymdError> {
|
||||
let request = DkgQueryMsg::GetDepositAmount {};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_dealings_paged(
|
||||
&self,
|
||||
idx: usize,
|
||||
start_after: Option<String>,
|
||||
page_limit: Option<u32>,
|
||||
) -> Result<PagedDealingsResponse, NymdError> {
|
||||
let request = DkgQueryMsg::GetDealing {
|
||||
idx: idx as u64,
|
||||
limit: page_limit,
|
||||
start_after,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.coconut_dkg_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nymd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::traits::dkg_query_client::DkgQueryClient;
|
||||
use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient};
|
||||
use async_trait::async_trait;
|
||||
use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg;
|
||||
use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
|
||||
use contracts_common::dealings::ContractSafeBytes;
|
||||
|
||||
#[async_trait]
|
||||
pub trait DkgSigningClient {
|
||||
async fn register_dealer(
|
||||
&self,
|
||||
bte_key: EncodedBTEPublicKeyWithProof,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError>;
|
||||
|
||||
async fn submit_dealing_bytes(
|
||||
&self,
|
||||
commitment: ContractSafeBytes,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> DkgSigningClient for NymdClient<C>
|
||||
where
|
||||
C: SigningCosmWasmClient + Send + Sync,
|
||||
{
|
||||
async fn register_dealer(
|
||||
&self,
|
||||
bte_key: EncodedBTEPublicKeyWithProof,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError> {
|
||||
let req = DkgExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof: bte_key,
|
||||
};
|
||||
let deposit = self.get_deposit_amount().await?;
|
||||
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.coconut_dkg_contract_address(),
|
||||
&req,
|
||||
fee.unwrap_or_default(),
|
||||
format!("registering {} as a dealer", self.address()),
|
||||
vec![deposit.amount.into()],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_dealing_bytes(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError> {
|
||||
let req = DkgExecuteMsg::CommitDealing { dealing_bytes };
|
||||
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
self.coconut_dkg_contract_address(),
|
||||
&req,
|
||||
fee.unwrap_or_default(),
|
||||
"dealing commitment",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
mod coconut_bandwidth_query_client;
|
||||
mod coconut_bandwidth_signing_client;
|
||||
mod dkg_query_client;
|
||||
mod dkg_signing_client;
|
||||
mod mixnet_query_client;
|
||||
mod mixnet_signing_client;
|
||||
mod multisig_query_client;
|
||||
@@ -12,6 +14,8 @@ mod vesting_signing_client;
|
||||
|
||||
pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient;
|
||||
pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient;
|
||||
pub use dkg_query_client::DkgQueryClient;
|
||||
pub use dkg_signing_client::DkgSigningClient;
|
||||
pub use mixnet_query_client::MixnetQueryClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_query_client::MultisigQueryClient;
|
||||
|
||||
@@ -13,7 +13,7 @@ use validator_client::nymd::wallet::DirectSecp256k1HdWallet;
|
||||
pub struct SignatureOutputJson {
|
||||
pub account_id: String,
|
||||
pub public_key: PublicKey,
|
||||
pub signature: String,
|
||||
pub signature_as_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -46,7 +46,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) {
|
||||
let output = SignatureOutputJson {
|
||||
account_id: account.address().to_string(),
|
||||
public_key: account.public_key(),
|
||||
signature: signature.to_string(),
|
||||
signature_as_hex: signature.to_string(),
|
||||
};
|
||||
println!("{}", json!(output));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub const PROPOSAL_ID: &str = "proposal_id";
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod deposit;
|
||||
pub mod event_attributes;
|
||||
pub mod events;
|
||||
pub mod msg;
|
||||
pub mod spend_credential;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "coconut-dkg-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
|
||||
contracts-common = { path = "../contracts-common" }
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex};
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DealerDetails {
|
||||
pub address: Addr,
|
||||
pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof,
|
||||
pub assigned_index: NodeIndex,
|
||||
pub deposit: Coin,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DealerType {
|
||||
Current,
|
||||
Past,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl DealerType {
|
||||
pub fn is_current(&self) -> bool {
|
||||
matches!(&self, DealerType::Current)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DealerDetailsResponse {
|
||||
pub details: Option<DealerDetails>,
|
||||
pub dealer_type: DealerType,
|
||||
}
|
||||
|
||||
impl DealerDetailsResponse {
|
||||
pub fn new(details: Option<DealerDetails>, dealer_type: DealerType) -> Self {
|
||||
DealerDetailsResponse {
|
||||
details,
|
||||
dealer_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct PagedDealerResponse {
|
||||
pub dealers: Vec<DealerDetails>,
|
||||
pub per_page: usize,
|
||||
pub start_next_after: Option<Addr>,
|
||||
}
|
||||
|
||||
impl PagedDealerResponse {
|
||||
pub fn new(
|
||||
dealers: Vec<DealerDetails>,
|
||||
per_page: usize,
|
||||
start_next_after: Option<Addr>,
|
||||
) -> Self {
|
||||
PagedDealerResponse {
|
||||
dealers,
|
||||
per_page,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ContractDealing {
|
||||
pub dealing: ContractSafeBytes,
|
||||
pub dealer: Addr,
|
||||
}
|
||||
|
||||
impl ContractDealing {
|
||||
pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self {
|
||||
ContractDealing { dealing, dealer }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct PagedDealingsResponse {
|
||||
pub dealings: Vec<ContractDealing>,
|
||||
pub per_page: usize,
|
||||
pub start_next_after: Option<Addr>,
|
||||
}
|
||||
|
||||
impl PagedDealingsResponse {
|
||||
pub fn new(
|
||||
dealings: Vec<ContractDealing>,
|
||||
per_page: usize,
|
||||
start_next_after: Option<Addr>,
|
||||
) -> Self {
|
||||
PagedDealingsResponse {
|
||||
dealings,
|
||||
per_page,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub const NODE_INDEX: &str = "node_index";
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod dealer;
|
||||
pub mod event_attributes;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct InstantiateMsg {
|
||||
pub group_addr: String,
|
||||
pub mix_denom: String,
|
||||
pub admin: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
RegisterDealer {
|
||||
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
|
||||
},
|
||||
|
||||
CommitDealing {
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
},
|
||||
|
||||
AdvanceEpochState {},
|
||||
|
||||
// DEBUG ONLY TXs. THEY SHALL BE REMOVED BEFORE FINALISING THE CODE
|
||||
// only exists for debugging purposes on local network to reset the entire state of the contract
|
||||
DebugUnsafeResetAll {
|
||||
init_msg: InstantiateMsg,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GetCurrentEpochState {},
|
||||
GetDealerDetails {
|
||||
dealer_address: String,
|
||||
},
|
||||
GetCurrentDealers {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
GetPastDealers {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
GetDepositAmount {},
|
||||
GetDealing {
|
||||
idx: u64,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub use crate::dealer::{DealerDetails, PagedDealerResponse};
|
||||
pub use contracts_common::dealings::ContractSafeBytes;
|
||||
pub use cosmwasm_std::{Addr, Coin};
|
||||
|
||||
pub type EncodedBTEPublicKeyWithProof = String;
|
||||
pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str;
|
||||
pub type NodeIndex = u64;
|
||||
|
||||
// 2 public attributes, 2 private attributes, 1 fixed for coconut credential
|
||||
pub const TOTAL_DEALINGS: usize = 2 + 2 + 1;
|
||||
|
||||
// currently (it is still extremely likely to change, we might be able to get rid of verification key-related complaints),
|
||||
// the epoch can be in the following states (in order):
|
||||
// 1. PublicKeySubmission -> potential dealers are submitting their BTE and ed25519 public keys to participate in dealing exchange
|
||||
// 2. DealingExchange -> the actual (off-chain) dealing exchange is happening
|
||||
// 3. ComplaintSubmission -> receivers submitting evidence of other dealers sending malformed data
|
||||
// 4. ComplaintVoting -> (if any complaints were submitted) receivers voting on the validity of the evidence provided
|
||||
// 5. VerificationKeySubmission -> receivers submitting their partial (and master) verification keys
|
||||
// 6. VerificationKeyMismatchSubmission -> receivers / watchers raising issue that the submitted VK are mismatched with their local derivations
|
||||
// 7. VerificationKeyMismatchVoting -> (if any complaints were submitted) receivers voting on received mismatches
|
||||
// 8. InProgress -> all receivers have all their secrets derived and all is good
|
||||
//
|
||||
// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd`
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EpochState {
|
||||
PublicKeySubmission,
|
||||
DealingExchange,
|
||||
VerificationKeySubmission,
|
||||
InProgress,
|
||||
}
|
||||
|
||||
impl Default for EpochState {
|
||||
fn default() -> Self {
|
||||
Self::PublicKeySubmission
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EpochState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"),
|
||||
EpochState::DealingExchange => write!(f, "DealingExchange"),
|
||||
EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"),
|
||||
EpochState::InProgress => write!(f, "InProgress"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EpochState {
|
||||
pub fn next(self) -> Option<Self> {
|
||||
match self {
|
||||
EpochState::PublicKeySubmission => Some(EpochState::DealingExchange),
|
||||
EpochState::DealingExchange => Some(EpochState::VerificationKeySubmission),
|
||||
EpochState::VerificationKeySubmission => Some(EpochState::InProgress),
|
||||
EpochState::InProgress => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_until(&self, end: Self) -> Vec<Self> {
|
||||
let mut states = vec![*self];
|
||||
while states.last().unwrap() != &end {
|
||||
let next_state = states.last().unwrap().next().expect("somehow reached the end of state diff -> this should be impossible under any circumstances!");
|
||||
states.push(next_state);
|
||||
}
|
||||
|
||||
states
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MinimumDepositResponse {
|
||||
pub amount: Coin,
|
||||
}
|
||||
|
||||
impl MinimumDepositResponse {
|
||||
pub fn new(amount: Coin) -> Self {
|
||||
MinimumDepositResponse { amount }
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,15 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bs58 = "0.4.0"
|
||||
cosmwasm-std = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
dkg = { path = "../../../common/crypto/dkg", optional = true }
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.0"
|
||||
|
||||
[features]
|
||||
coconut = ["dkg"]
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use dkg::{error::DkgError, Dealing};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
|
||||
// some sane upper-bound size on byte sizes
|
||||
// currently set to 128 bytes
|
||||
pub const MAX_DISPLAY_SIZE: usize = 128;
|
||||
|
||||
// TODO: if we are to use this for different types, it might make sense to introduce something like
|
||||
// CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait
|
||||
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)]
|
||||
pub struct ContractSafeBytes(Vec<u8>);
|
||||
|
||||
impl Deref for ContractSafeBytes {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContractSafeBytes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if !self.0.is_empty() {
|
||||
write!(f, "0x")?;
|
||||
}
|
||||
for byte in self.0.iter().take(MAX_DISPLAY_SIZE) {
|
||||
write!(f, "{:02X}", byte)?;
|
||||
}
|
||||
// just some sanity safeguards
|
||||
if self.0.len() > MAX_DISPLAY_SIZE {
|
||||
write!(f, "...")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// since cosmwasm stores everything with byte representation of stringified json, it's actually more efficient
|
||||
// to serialize this as a string as opposed to keeping it as vector of bytes.
|
||||
// for example vec![255,255] would have string representation of "[255,255]" and will be serialized to
|
||||
// [91, 50, 53, 53, 44, 50, 53, 53, 93]. the equivalent base58 encoded string `"LUv"` will be serialized to
|
||||
// [34, 76, 85, 118, 34]
|
||||
//
|
||||
// the difference between base58 and base64 is rather minimal and I've gone with base58 for consistency sake
|
||||
impl Serialize for ContractSafeBytes {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&bs58::encode(&self.0).into_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ContractSafeBytes {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
let bytes = bs58::decode(&s)
|
||||
.into_vec()
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(ContractSafeBytes(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
impl<'a> From<&'a Dealing> for ContractSafeBytes {
|
||||
fn from(dealing: &'a Dealing) -> Self {
|
||||
ContractSafeBytes(dealing.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
impl<'a> TryFrom<&'a ContractSafeBytes> for Dealing {
|
||||
type Error = DkgError;
|
||||
|
||||
fn try_from(value: &'a ContractSafeBytes) -> Result<Self, Self::Error> {
|
||||
Dealing::try_from_bytes(&value.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,10 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
pub mod dealings;
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Percent {
|
||||
|
||||
impl Display for Percent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
|
||||
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
|
||||
write!(f, "{}%", adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -60,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,
|
||||
@@ -87,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 {
|
||||
|
||||
@@ -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."),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,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;
|
||||
|
||||
@@ -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)
|
||||
@@ -107,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,
|
||||
@@ -135,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!"
|
||||
@@ -164,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();
|
||||
@@ -301,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
|
||||
@@ -335,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)
|
||||
}
|
||||
|
||||
@@ -368,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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -41,14 +43,17 @@ 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,
|
||||
@@ -61,7 +66,7 @@ impl InitialRewardingParams {
|
||||
},
|
||||
rewarded_set_size: self.rewarded_set_size,
|
||||
active_set_size: self.active_set_size,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,4 +413,6 @@ pub enum QueryMsg {
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct MigrateMsg {}
|
||||
pub struct MigrateMsg {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -110,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;
|
||||
}
|
||||
|
||||
@@ -200,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(())
|
||||
|
||||
@@ -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,7 +34,7 @@ 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
|
||||
@@ -47,10 +48,13 @@ impl Simulator {
|
||||
.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 {
|
||||
@@ -71,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(
|
||||
@@ -83,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)
|
||||
}
|
||||
|
||||
@@ -103,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<_>>();
|
||||
@@ -147,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,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");
|
||||
@@ -238,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();
|
||||
@@ -269,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(
|
||||
@@ -282,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,
|
||||
@@ -297,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];
|
||||
@@ -317,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);
|
||||
@@ -364,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);
|
||||
@@ -378,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);
|
||||
@@ -402,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);
|
||||
@@ -424,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);
|
||||
@@ -467,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();
|
||||
@@ -503,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
|
||||
@@ -563,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();
|
||||
@@ -673,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);
|
||||
|
||||
@@ -35,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
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ 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,13 +1,15 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
// 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};
|
||||
|
||||
@@ -59,29 +61,18 @@ impl FromStr for PledgeCap {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
Ok(p) => Ok(PledgeCap::Percent(p)),
|
||||
Err(_) => match cap.parse::<u128>() {
|
||||
Ok(i) => 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 {
|
||||
#[allow(clippy::expect_used)]
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).expect("This can never fail!"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ serde_derive = "1.0"
|
||||
thiserror = "1.0"
|
||||
zeroize = { version = "1.4", features = ["zeroize_derive"] }
|
||||
|
||||
pemstore = { path = "../../pemstore" }
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.11"
|
||||
default-features = false
|
||||
|
||||
@@ -9,7 +9,7 @@ use dkg::bte::proof_discrete_log::ProofOfDiscreteLog;
|
||||
use dkg::bte::proof_sharing::ProofOfSecretSharing;
|
||||
use dkg::bte::{
|
||||
decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey,
|
||||
Epoch, PublicKey,
|
||||
PublicKey,
|
||||
};
|
||||
use dkg::interpolation::polynomial::Polynomial;
|
||||
use dkg::{Dealing, NodeIndex, Share};
|
||||
@@ -54,7 +54,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 2;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 3);
|
||||
|
||||
@@ -66,7 +65,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
@@ -80,7 +78,6 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 2;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, mut dks) = prepare_keys(&mut rng, 3);
|
||||
let (dealing, _) = Dealing::create(
|
||||
@@ -88,22 +85,18 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
"verifying single dealing made for 3 parties (threshold 2) and recovering share",
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -114,7 +107,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 14;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 20);
|
||||
|
||||
@@ -128,7 +120,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
@@ -143,7 +134,6 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 14;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, mut dks) = prepare_keys(&mut rng, 20);
|
||||
let (dealing, _) = Dealing::create(
|
||||
@@ -151,22 +141,18 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
"verifying single dealing made for 20 parties (threshold 14) and recovering share",
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -177,7 +163,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 67;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
|
||||
@@ -191,7 +176,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
)
|
||||
@@ -206,7 +190,6 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let threshold = 67;
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, mut dks) = prepare_keys(&mut rng, 100);
|
||||
let (dealing, _) = Dealing::create(
|
||||
@@ -214,22 +197,18 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
¶ms,
|
||||
receivers.keys().next().copied().unwrap(),
|
||||
threshold,
|
||||
epoch,
|
||||
&receivers,
|
||||
None,
|
||||
);
|
||||
|
||||
let first_key = dks.get_mut(0).unwrap();
|
||||
first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
"verifying single dealing made for 100 parties (threshold 67) and recovering share",
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing
|
||||
.verify(¶ms, epoch, threshold, &receivers, None)
|
||||
.is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -266,7 +245,6 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
|
||||
@@ -283,7 +261,7 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
.collect::<Vec<_>>();
|
||||
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng);
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
c.bench_function("creating proof of chunking for 100 parties", |b| {
|
||||
b.iter(|| {
|
||||
@@ -301,7 +279,6 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
|
||||
@@ -318,7 +295,7 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
.collect::<Vec<_>>();
|
||||
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng);
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
|
||||
let proof_of_chunking =
|
||||
@@ -338,7 +315,6 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
|
||||
@@ -354,7 +330,7 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng);
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
|
||||
let combined_r = hazmat.combine_rs();
|
||||
@@ -381,7 +357,6 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
|
||||
@@ -397,7 +372,7 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng);
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
|
||||
let combined_r = hazmat.combine_rs();
|
||||
@@ -430,7 +405,6 @@ pub fn single_share_encryption(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
let (_, pk) = keygen(¶ms, &mut rng);
|
||||
|
||||
let polynomial = Polynomial::new_random(&mut rng, 3);
|
||||
@@ -440,7 +414,6 @@ pub fn single_share_encryption(c: &mut Criterion) {
|
||||
b.iter(|| {
|
||||
black_box(encrypt_shares(
|
||||
&[(&share, pk.public_key())],
|
||||
epoch,
|
||||
¶ms,
|
||||
&mut rng,
|
||||
))
|
||||
@@ -452,7 +425,6 @@ pub fn share_encryption_100(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
|
||||
let (receivers, _) = prepare_keys(&mut rng, 100);
|
||||
let polynomial = Polynomial::new_random(&mut rng, 3);
|
||||
@@ -468,14 +440,7 @@ pub fn share_encryption_100(c: &mut Criterion) {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
c.bench_function("100 shares encryption", |b| {
|
||||
b.iter(|| {
|
||||
black_box(encrypt_shares(
|
||||
&remote_share_key_pairs,
|
||||
epoch,
|
||||
¶ms,
|
||||
&mut rng,
|
||||
))
|
||||
})
|
||||
b.iter(|| black_box(encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng)))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,16 +448,14 @@ pub fn share_decryption(c: &mut Criterion) {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
let epoch = Epoch::new(2);
|
||||
let (mut dk, pk) = keygen(¶ms, &mut rng);
|
||||
let (dk, pk) = keygen(¶ms, &mut rng);
|
||||
|
||||
let polynomial = Polynomial::new_random(&mut rng, 3);
|
||||
let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into();
|
||||
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, ¶ms, &mut rng);
|
||||
dk.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng);
|
||||
|
||||
c.bench_function("single share decryption", |b| {
|
||||
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None)))
|
||||
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::bte::keys::{DecryptionKey, PublicKey};
|
||||
use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE};
|
||||
use crate::bte::{evaluate_f, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE};
|
||||
use crate::error::DkgError;
|
||||
use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2};
|
||||
use crate::{Chunk, ChunkedShare, Share};
|
||||
@@ -24,7 +24,7 @@ pub struct Ciphertexts {
|
||||
}
|
||||
|
||||
impl Ciphertexts {
|
||||
pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool {
|
||||
pub fn verify_integrity(&self, params: &Params) -> bool {
|
||||
// if this checks fails it means the ciphertext is undefined as values
|
||||
// in `r`, `s` and `z` are meaningless since technically this ciphertext
|
||||
// has been created for 0 parties
|
||||
@@ -33,9 +33,7 @@ impl Ciphertexts {
|
||||
}
|
||||
|
||||
let g1_neg = G1Affine::generator().neg();
|
||||
let f = epoch
|
||||
.as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks)
|
||||
.evaluate_f(params);
|
||||
let f = evaluate_f(params);
|
||||
|
||||
// we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid),
|
||||
// so perform some precomputation on it
|
||||
@@ -192,7 +190,6 @@ impl HazmatRandomness {
|
||||
|
||||
pub fn encrypt_shares(
|
||||
shares: &[(&Share, &PublicKey)],
|
||||
epoch: Epoch,
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
) -> (Ciphertexts, HazmatRandomness) {
|
||||
@@ -242,7 +239,7 @@ pub fn encrypt_shares(
|
||||
let rr = rr.try_into().unwrap();
|
||||
let ss = ss.try_into().unwrap();
|
||||
|
||||
let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params);
|
||||
let f = evaluate_f(params);
|
||||
|
||||
let mut zz = Vec::with_capacity(NUM_CHUNKS);
|
||||
for i in 0..NUM_CHUNKS {
|
||||
@@ -269,35 +266,22 @@ pub fn decrypt_share(
|
||||
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
|
||||
i: usize,
|
||||
ciphertext: &Ciphertexts,
|
||||
epoch: Epoch,
|
||||
lookup_table: Option<&BabyStepGiantStepLookup>,
|
||||
) -> Result<Share, DkgError> {
|
||||
let mut plaintext = ChunkedShare::default();
|
||||
|
||||
let decryption_node = dk.try_get_compatible_node(epoch)?;
|
||||
let extended_tau = epoch.as_extended_tau(
|
||||
&ciphertext.rr,
|
||||
&ciphertext.ss,
|
||||
&ciphertext.ciphertext_chunks,
|
||||
);
|
||||
|
||||
if i >= ciphertext.ciphertext_chunks.len() {
|
||||
return Err(DkgError::UnavailableCiphertext(i));
|
||||
}
|
||||
|
||||
let height = decryption_node.tau.height();
|
||||
let b_neg = decryption_node
|
||||
.ds
|
||||
let b_neg = dk
|
||||
.dh
|
||||
.iter()
|
||||
.chain(decryption_node.dh.iter())
|
||||
.zip(extended_tau.0.iter().by_vals().skip(height))
|
||||
.filter(|(_, i)| *i)
|
||||
.map(|(d_i, _)| d_i)
|
||||
.fold(decryption_node.b, |acc, d_i| acc + d_i)
|
||||
.fold(dk.b, |acc, d_i| acc + d_i)
|
||||
.neg()
|
||||
.to_affine();
|
||||
|
||||
let e_neg = decryption_node.e.neg().to_affine();
|
||||
let e_neg = dk.e.neg().to_affine();
|
||||
|
||||
for j in 0..NUM_CHUNKS {
|
||||
let rr_j = &ciphertext.rr[j];
|
||||
@@ -308,7 +292,7 @@ pub fn decrypt_share(
|
||||
let miller = bls12_381::multi_miller_loop(&[
|
||||
(&cc_ij.to_affine(), &G2_GENERATOR_PREPARED),
|
||||
(&rr_j.to_affine(), &G2Prepared::from(b_neg)),
|
||||
(&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)),
|
||||
(&dk.a.to_affine(), &G2Prepared::from(zz_j)),
|
||||
(&ss_j.to_affine(), &G2Prepared::from(e_neg)),
|
||||
]);
|
||||
let m = miller.final_exponentiation();
|
||||
@@ -466,7 +450,6 @@ mod tests {
|
||||
|
||||
let (decryption_key1, public_key1) = keygen(¶ms, &mut rng);
|
||||
let (decryption_key2, public_key2) = keygen(¶ms, &mut rng);
|
||||
let epoch = Epoch::new(0);
|
||||
|
||||
let lookup_table = &DEFAULT_BSGS_TABLE;
|
||||
|
||||
@@ -475,13 +458,13 @@ mod tests {
|
||||
let m2 = Share::random(&mut rng);
|
||||
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
|
||||
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng);
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -494,15 +477,8 @@ mod tests {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let (mut decryption_key1, public_key1) = keygen(¶ms, &mut rng);
|
||||
let (mut decryption_key2, public_key2) = keygen(¶ms, &mut rng);
|
||||
let epoch = Epoch::new(12345);
|
||||
decryption_key1
|
||||
.try_update_to(epoch, ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
decryption_key2
|
||||
.try_update_to(epoch, ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
let (decryption_key1, public_key1) = keygen(¶ms, &mut rng);
|
||||
let (decryption_key2, public_key2) = keygen(¶ms, &mut rng);
|
||||
|
||||
let lookup_table = &DEFAULT_BSGS_TABLE;
|
||||
|
||||
@@ -511,121 +487,18 @@ mod tests {
|
||||
let m2 = Share::random(&mut rng);
|
||||
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
|
||||
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng);
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decryption_with_root_key() {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let (root_key, public_key) = keygen(¶ms, &mut rng);
|
||||
|
||||
let share = Share::random(&mut rng);
|
||||
|
||||
let epoch0 = Epoch::new(0);
|
||||
let epoch42 = Epoch::new(42);
|
||||
let epoch_big = Epoch::new(3292547435);
|
||||
|
||||
let (ciphertext1, hazmat1) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch0, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext1, &hazmat1);
|
||||
|
||||
let (ciphertext2, hazmat2) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch42, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext2, &hazmat2);
|
||||
|
||||
let (ciphertext3, hazmat3) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch_big, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext3, &hazmat3);
|
||||
|
||||
let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap();
|
||||
let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap();
|
||||
let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap();
|
||||
|
||||
assert_eq!(share, recovered1);
|
||||
assert_eq!(share, recovered2);
|
||||
assert_eq!(share, recovered3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn update_and_decrypt_10() {
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let (mut decryption_key, public_key) = keygen(¶ms, &mut rng);
|
||||
|
||||
for epoch_value in 0..10 {
|
||||
let epoch = Epoch::new(epoch_value);
|
||||
let share = Share::random(&mut rng);
|
||||
decryption_key
|
||||
.try_update_to(epoch, ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
|
||||
let (ciphertext, hazmat) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
|
||||
assert_eq!(share, recovered);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn reblinding_node_doesnt_affect_decryption() {
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let (mut decryption_key, public_key) = keygen(¶ms, &mut rng);
|
||||
|
||||
let epoch = Epoch::new(12345);
|
||||
decryption_key
|
||||
.try_update_to(epoch, ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
for node in decryption_key.nodes.iter_mut() {
|
||||
node.reblind(¶ms, &mut rng);
|
||||
}
|
||||
let share = Share::random(&mut rng);
|
||||
|
||||
let (ciphertext, hazmat) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
|
||||
assert_eq!(share, recovered);
|
||||
|
||||
// attempt to update the key again so we have to derive fresh nodes using previous reblinded results
|
||||
let epoch2 = Epoch::new(67890);
|
||||
decryption_key
|
||||
.try_update_to(epoch2, ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
for node in decryption_key.nodes.iter_mut() {
|
||||
node.reblind(¶ms, &mut rng);
|
||||
}
|
||||
let share2 = Share::random(&mut rng);
|
||||
|
||||
let (ciphertext, hazmat) =
|
||||
encrypt_shares(&[(&share2, &public_key.key)], epoch2, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap();
|
||||
assert_eq!(share2, recovered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn ciphertext_integrity_check_passes_for_valid_data() {
|
||||
@@ -634,14 +507,11 @@ mod tests {
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, public_key) = keygen(¶ms, &mut rng);
|
||||
let epoch = Epoch::new(1);
|
||||
let (_, public_key) = keygen(¶ms, &mut rng);
|
||||
|
||||
dk.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
let share = Share::random(&mut rng);
|
||||
let (ciphertext, _) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng);
|
||||
assert!(ciphertext.verify_integrity(¶ms, epoch))
|
||||
let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng);
|
||||
assert!(ciphertext.verify_integrity(¶ms))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -652,45 +522,22 @@ mod tests {
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, public_key) = keygen(¶ms, &mut rng);
|
||||
let epoch = Epoch::new(1);
|
||||
let (_, public_key) = keygen(¶ms, &mut rng);
|
||||
|
||||
dk.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
let share = Share::random(&mut rng);
|
||||
let (ciphertext, _) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng);
|
||||
let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng);
|
||||
|
||||
let mut bad_cipher1 = ciphertext.clone();
|
||||
bad_cipher1.rr[4] = G1Projective::generator();
|
||||
assert!(!bad_cipher1.verify_integrity(¶ms, epoch));
|
||||
assert!(!bad_cipher1.verify_integrity(¶ms));
|
||||
|
||||
let mut bad_cipher2 = ciphertext.clone();
|
||||
bad_cipher2.ss[4] = G1Projective::generator();
|
||||
assert!(!bad_cipher2.verify_integrity(¶ms, epoch));
|
||||
assert!(!bad_cipher2.verify_integrity(¶ms));
|
||||
|
||||
let mut bad_cipher3 = ciphertext;
|
||||
bad_cipher3.zz[4] = G2Projective::generator();
|
||||
assert!(!bad_cipher3.verify_integrity(¶ms, epoch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() {
|
||||
let params = setup();
|
||||
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, public_key) = keygen(¶ms, &mut rng);
|
||||
let epoch = Epoch::new(1);
|
||||
|
||||
dk.try_update_to(epoch, ¶ms, &mut rng).unwrap();
|
||||
let share = Share::random(&mut rng);
|
||||
let (ciphertext, _) =
|
||||
encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng);
|
||||
|
||||
let another_epoch = Epoch::new(2);
|
||||
assert!(!ciphertext.verify_integrity(¶ms, another_epoch))
|
||||
assert!(!bad_cipher3.verify_integrity(¶ms));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -711,7 +558,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let refs = shares.iter().zip(public_keys.iter()).collect::<Vec<_>>();
|
||||
let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), ¶ms, &mut rng);
|
||||
let (ciphertext, hazmat) = encrypt_shares(&refs, ¶ms, &mut rng);
|
||||
|
||||
let combined_r = combine_scalar_chunks(hazmat.r());
|
||||
let combined_rr = ciphertext.combine_rs();
|
||||
|
||||
+146
-712
@@ -2,306 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::bte::proof_discrete_log::ProofOfDiscreteLog;
|
||||
use crate::bte::{Epoch, Params, Tau};
|
||||
use crate::bte::Params;
|
||||
use crate::error::DkgError;
|
||||
use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar};
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::GroupEncoding;
|
||||
use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use rand_core::RngCore;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq))]
|
||||
pub(crate) struct Node {
|
||||
pub(crate) tau: Tau,
|
||||
|
||||
// g1^rho
|
||||
pub(crate) a: G1Projective,
|
||||
|
||||
// g2^x
|
||||
pub(crate) b: G2Projective,
|
||||
|
||||
// f_i^rho, up to lambda_t elements
|
||||
pub(crate) ds: Vec<G2Projective>,
|
||||
|
||||
// fh_i^rho, always lambda_h elements
|
||||
pub(crate) dh: Vec<G2Projective>,
|
||||
|
||||
// h^rho
|
||||
pub(crate) e: G2Projective,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn new_root(
|
||||
a: G1Projective,
|
||||
b: G2Projective,
|
||||
ds: Vec<G2Projective>,
|
||||
dh: Vec<G2Projective>,
|
||||
e: G2Projective,
|
||||
) -> Self {
|
||||
Node {
|
||||
tau: Tau::new_root(),
|
||||
a,
|
||||
b,
|
||||
ds,
|
||||
dh,
|
||||
e,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_root(&self) -> bool {
|
||||
self.tau.0.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) {
|
||||
let delta = Scalar::random(&mut rng);
|
||||
self.a += G1Projective::generator() * delta;
|
||||
|
||||
// TODO: or do we have to do full tau evaluation here?
|
||||
self.b += self.tau.evaluate_partial_f(params) * delta;
|
||||
self.ds
|
||||
.iter_mut()
|
||||
.zip(params.fs.iter().skip(self.tau.height()))
|
||||
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
|
||||
self.dh
|
||||
.iter_mut()
|
||||
.zip(params.fh.iter())
|
||||
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
|
||||
|
||||
self.e += params.h * delta;
|
||||
}
|
||||
|
||||
// note: it's unsafe to use this method outside `try_update_to` as
|
||||
// we have guaranteed there that `self` is parent of the target
|
||||
// and that `self.tau != target_tau`
|
||||
/// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`,
|
||||
/// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material
|
||||
/// for its construction.
|
||||
fn derive_target_child_with_partials(
|
||||
&self,
|
||||
params: &Params,
|
||||
target_tau: Tau,
|
||||
partial_b: &G2Projective,
|
||||
partial_f: &G2Projective,
|
||||
mut rng: impl RngCore,
|
||||
) -> Self {
|
||||
debug_assert!(self.tau.is_parent_of(&target_tau));
|
||||
debug_assert_ne!(self.tau, target_tau);
|
||||
|
||||
let delta = Scalar::random(&mut rng);
|
||||
let a = self.a + G1Projective::generator() * delta;
|
||||
let b = partial_b + partial_f * delta;
|
||||
let ds = self
|
||||
.ds
|
||||
.iter()
|
||||
.zip(params.fs.iter())
|
||||
.skip(target_tau.height())
|
||||
.map(|(d_i, f_i)| d_i + f_i * delta)
|
||||
.collect();
|
||||
let dh = self
|
||||
.dh
|
||||
.iter()
|
||||
.zip(params.fh.iter())
|
||||
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
|
||||
.collect();
|
||||
let e = self.e + params.h * delta;
|
||||
|
||||
Node {
|
||||
tau: target_tau,
|
||||
a,
|
||||
b,
|
||||
ds,
|
||||
dh,
|
||||
e,
|
||||
}
|
||||
}
|
||||
|
||||
// note: it's unsafe to use this method outside `try_update_to` as
|
||||
// we have guaranteed there that `self` is parent of the target
|
||||
// and that `self.tau != target_tau`
|
||||
/// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`,
|
||||
/// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1`
|
||||
fn derive_right_nonfinal_child_of_with_partials(
|
||||
&self,
|
||||
params: &Params,
|
||||
most_direct_parent: Tau,
|
||||
partial_b: &G2Projective,
|
||||
partial_f: &G2Projective,
|
||||
mut rng: impl RngCore,
|
||||
) -> Self {
|
||||
let right_branch = most_direct_parent.right_child();
|
||||
|
||||
debug_assert!(self.tau.is_parent_of(&most_direct_parent));
|
||||
debug_assert!(self.tau.is_parent_of(&right_branch));
|
||||
debug_assert_ne!(self.tau, right_branch);
|
||||
|
||||
// n is height difference between self and the child
|
||||
let n = right_branch.height() - self.tau.height();
|
||||
|
||||
// i is the index of the last bit we just added
|
||||
let i = right_branch.height() - 1;
|
||||
|
||||
let delta = Scalar::random(&mut rng);
|
||||
let a = self.a + G1Projective::generator() * delta;
|
||||
let d0 = self.ds[n - 1];
|
||||
let b = partial_b + d0 + (partial_f + params.fs[i]) * delta;
|
||||
let ds = self
|
||||
.ds
|
||||
.iter()
|
||||
.skip(n)
|
||||
.zip(params.fs.iter().skip(right_branch.height()))
|
||||
.map(|(d_i, f_i)| d_i + f_i * delta)
|
||||
.collect();
|
||||
let dh = self
|
||||
.dh
|
||||
.iter()
|
||||
.zip(params.fh.iter())
|
||||
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
|
||||
.collect();
|
||||
|
||||
let e = self.e + params.h * delta;
|
||||
|
||||
Node {
|
||||
tau: right_branch,
|
||||
a,
|
||||
b,
|
||||
ds,
|
||||
dh,
|
||||
e,
|
||||
}
|
||||
}
|
||||
|
||||
// tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let g1_elements = 1;
|
||||
let g2_elements = self.ds.len() + self.dh.len() + 2;
|
||||
|
||||
let tau_bytes = self.tau.to_bytes();
|
||||
|
||||
// the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh
|
||||
let mut bytes =
|
||||
Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12);
|
||||
|
||||
bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes()));
|
||||
bytes.extend_from_slice(&tau_bytes);
|
||||
bytes.extend_from_slice(self.a.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.b.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes()));
|
||||
for d_i in &self.ds {
|
||||
bytes.extend_from_slice(d_i.to_bytes().as_ref());
|
||||
}
|
||||
bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes()));
|
||||
for dh_i in &self.dh {
|
||||
bytes.extend_from_slice(dh_i.to_bytes().as_ref());
|
||||
}
|
||||
bytes.extend_from_slice(self.e.to_bytes().as_ref());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
|
||||
// at the very least we require bytes for:
|
||||
// - tau_len ( 4 )
|
||||
// - tau ( could be 0 for root node )
|
||||
// - a ( 48 )
|
||||
// - b ( 96 )
|
||||
// - length indication of ds ( 4 )
|
||||
// - length indication of dh ( 4 )
|
||||
// - e ( 96 )
|
||||
if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Node",
|
||||
"insufficient number of bytes provided",
|
||||
));
|
||||
}
|
||||
|
||||
let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize;
|
||||
let mut i = 4;
|
||||
|
||||
let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?;
|
||||
i += tau_len;
|
||||
|
||||
// perform another length check to account for bytes consumed by tau
|
||||
if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Node",
|
||||
"insufficient number of bytes provided",
|
||||
));
|
||||
}
|
||||
|
||||
let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.a", "invalid curve point")
|
||||
})?;
|
||||
i += 48;
|
||||
|
||||
let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.b", "invalid curve point")
|
||||
})?;
|
||||
i += 96;
|
||||
|
||||
let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
|
||||
i += 4;
|
||||
|
||||
if bytes[i..].len() < ds_len * 96 + 4 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Node",
|
||||
"insufficient number of bytes provided (ds)",
|
||||
));
|
||||
}
|
||||
|
||||
let mut ds = Vec::with_capacity(ds_len);
|
||||
for j in 0..ds_len {
|
||||
let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure(
|
||||
format!("Node.ds_{}", j),
|
||||
"invalid curve point",
|
||||
)
|
||||
})?;
|
||||
|
||||
ds.push(d_i);
|
||||
i += 96;
|
||||
}
|
||||
|
||||
let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
|
||||
i += 4;
|
||||
|
||||
if bytes[i..].len() != (dh_len + 1) * 96 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Node",
|
||||
"insufficient number of bytes provided (dh)",
|
||||
));
|
||||
}
|
||||
|
||||
let mut dh = Vec::with_capacity(dh_len);
|
||||
for j in 0..dh_len {
|
||||
let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure(
|
||||
format!("Node.dh_{}", j),
|
||||
"invalid curve point",
|
||||
)
|
||||
})?;
|
||||
|
||||
dh.push(dh_i);
|
||||
i += 96;
|
||||
}
|
||||
|
||||
let e = deserialize_g2(&bytes[i..]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.h", "invalid curve point")
|
||||
})?;
|
||||
|
||||
Ok(Node {
|
||||
tau,
|
||||
a,
|
||||
b,
|
||||
ds,
|
||||
dh,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// produces public key and a decryption key for the root of the tree
|
||||
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
let g1 = G1Projective::generator();
|
||||
@@ -317,11 +27,10 @@ pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicK
|
||||
let a = g1 * rho;
|
||||
let b = g2 * x + params.f0 * rho;
|
||||
|
||||
let ds = params.fs.iter().map(|f_i| f_i * rho).collect();
|
||||
let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect();
|
||||
let e = params.h * rho;
|
||||
|
||||
let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e));
|
||||
let dk = DecryptionKey::new_root(a, b, dh, e);
|
||||
|
||||
let public_key = PublicKey(y);
|
||||
let key_with_proof = PublicKeyWithProof {
|
||||
@@ -351,6 +60,20 @@ pub struct PublicKeyWithProof {
|
||||
pub(crate) proof: ProofOfDiscreteLog,
|
||||
}
|
||||
|
||||
impl PemStorableKey for PublicKeyWithProof {
|
||||
type Error = DkgError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"DKG PUBLIC KEY WITH PROOF"
|
||||
}
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::try_from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKeyWithProof {
|
||||
pub fn verify(&self) -> bool {
|
||||
self.key.verify(&self.proof)
|
||||
@@ -414,231 +137,157 @@ impl PublicKeyWithProof {
|
||||
#[zeroize(drop)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct DecryptionKey {
|
||||
// note that the nodes are ordered from "right" to "left"
|
||||
pub(crate) nodes: Vec<Node>,
|
||||
// g1^rho
|
||||
pub(crate) a: G1Projective,
|
||||
|
||||
// g2^x * f0^rho
|
||||
pub(crate) b: G2Projective,
|
||||
|
||||
// fh_i^rho, always lambda_h elements
|
||||
pub(crate) dh: Vec<G2Projective>,
|
||||
|
||||
// h^rho
|
||||
pub(crate) e: G2Projective,
|
||||
}
|
||||
|
||||
impl PemStorableKey for DecryptionKey {
|
||||
type Error = DkgError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"DKG DECRYPTION KEY"
|
||||
}
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::try_from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl DecryptionKey {
|
||||
fn new_root(root_node: Node) -> Self {
|
||||
DecryptionKey {
|
||||
nodes: vec![root_node],
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> Result<&Node, DkgError> {
|
||||
// we must have at least a single node, otherwise we have a malformed key
|
||||
self.nodes.last().ok_or(DkgError::MalformedDecryptionKey)
|
||||
}
|
||||
|
||||
pub fn current_epoch(&self, params: &Params) -> Result<Option<Epoch>, DkgError> {
|
||||
let current_node = self.current()?;
|
||||
if current_node.is_root() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Epoch::try_from_tau(¤t_node.tau, params).map(Option::Some)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> {
|
||||
let tau = epoch.as_tau();
|
||||
self.nodes
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|node| node.tau.is_parent_of(&tau))
|
||||
.ok_or(DkgError::ExpiredKey)
|
||||
}
|
||||
|
||||
pub fn try_update_to_next_epoch(
|
||||
&mut self,
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
) -> Result<(), DkgError> {
|
||||
if self.nodes.is_empty() {
|
||||
return Err(DkgError::MalformedDecryptionKey);
|
||||
}
|
||||
|
||||
let mut target_epoch = Epoch::new(0);
|
||||
if self.nodes.len() == 1 && self.nodes[0].is_root() {
|
||||
return self.try_update_to(target_epoch, params, &mut rng);
|
||||
}
|
||||
|
||||
// unwrap is fine as we have asserted self.nodes is not empty
|
||||
self.nodes.pop().unwrap();
|
||||
|
||||
if let Some(tail) = self.nodes.last() {
|
||||
target_epoch = tail.tau.lowest_valid_epoch_child(params)?;
|
||||
} else {
|
||||
// essentially our key consisted of only a single node and it wasn't a root,
|
||||
// so either it was malformed or we somehow reached the final epoch and wanted to update
|
||||
// beyond that. Either way, update to l + 1 is impossible
|
||||
return Err(DkgError::MalformedDecryptionKey);
|
||||
}
|
||||
|
||||
self.try_update_to(target_epoch, params, &mut rng)
|
||||
}
|
||||
|
||||
/// Attempts to update `self` to the provided `epoch`. If the update is not possible,
|
||||
/// because the target was in the past or the key is malformed, an error is returned.
|
||||
///
|
||||
/// Note that this method mutates the key in place and if the original key was malformed,
|
||||
/// there are no guarantees about its internal state post-call.
|
||||
pub fn try_update_to(
|
||||
&mut self,
|
||||
target_epoch: Epoch,
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
) -> Result<(), DkgError> {
|
||||
if self.nodes.is_empty() {
|
||||
// somehow we have an empty decryption key
|
||||
return Err(DkgError::MalformedDecryptionKey);
|
||||
}
|
||||
|
||||
// makes it easier to work with since we will be generating non-leaf nodes
|
||||
let target_tau = target_epoch.as_tau();
|
||||
let current_tau = &self.current()?.tau;
|
||||
|
||||
if current_tau == &target_tau {
|
||||
// our key is already updated to the target
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if current_tau > &target_tau {
|
||||
// we cannot derive keys for past epochs
|
||||
return Err(DkgError::TargetEpochUpdateInThePast);
|
||||
}
|
||||
|
||||
// drop the nodes that are no longer required and get the most direct parent for the target epoch available
|
||||
let mut parent = loop {
|
||||
// if pop() fails the key is malformed since we checked that the target_epoch > current_epoch,
|
||||
// hence the update should have been possible
|
||||
let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?;
|
||||
if tail.tau.is_parent_of(&target_tau) {
|
||||
break tail;
|
||||
}
|
||||
};
|
||||
|
||||
// essentially the case of updating epoch n to n + 1, where n is even;
|
||||
// in that case the last two nodes are [..., epoch_{n+1}, epoch_n]
|
||||
// so we just have to reblind the n+1 node and we're done
|
||||
if parent.tau == target_tau {
|
||||
parent.reblind(params, &mut rng);
|
||||
self.nodes.push(parent);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// accumulators, note that the previous elements have already been included by the parent,
|
||||
// i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l}
|
||||
// new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n}
|
||||
// new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t)
|
||||
let mut new_b_accumulator = parent.b;
|
||||
let mut new_f_accumulator = parent.tau.evaluate_partial_f(params);
|
||||
|
||||
let parent_height = parent.tau.height();
|
||||
|
||||
// path from the parent to the child
|
||||
for (n, bit) in target_tau
|
||||
.0
|
||||
.iter()
|
||||
.by_vals()
|
||||
.skip(parent.tau.height())
|
||||
.enumerate()
|
||||
{
|
||||
// ith bit of the [child] epoch
|
||||
// note that n represents height difference between parent and the current bit
|
||||
let i = n + parent_height;
|
||||
|
||||
// if the bit is NOT set, push the right '1' subtree (for future keys)
|
||||
// so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`,
|
||||
// in the first loop iteration we're going to look at bit `0` and
|
||||
// derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1`
|
||||
// in the next loop iteration we're going to look at bit `1` and simply update the accumulators,
|
||||
// as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past
|
||||
// finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`,
|
||||
// i.e. the one that FOLLOWS the target node.
|
||||
if !bit {
|
||||
let direct_parent = target_tau.try_get_parent_at_height(i)?;
|
||||
|
||||
self.nodes
|
||||
.push(parent.derive_right_nonfinal_child_of_with_partials(
|
||||
params,
|
||||
direct_parent,
|
||||
&new_b_accumulator,
|
||||
&new_f_accumulator,
|
||||
&mut rng,
|
||||
));
|
||||
} else {
|
||||
// only update the accumulators when the bit is set, as d^0 == identity, so there's
|
||||
// no point in doing anything else;
|
||||
// note that we don't have to generate any new nodes when going into the right branch
|
||||
// of the tree as everything on the left would have been in the past, so we don't care about them
|
||||
new_b_accumulator += parent.ds[n]; // add d0
|
||||
new_f_accumulator += params.fs[i]; // f_i
|
||||
}
|
||||
}
|
||||
|
||||
self.nodes.push(parent.derive_target_child_with_partials(
|
||||
params,
|
||||
target_epoch.as_tau(),
|
||||
&new_b_accumulator,
|
||||
&new_f_accumulator,
|
||||
&mut rng,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
fn new_root(a: G1Projective, b: G2Projective, dh: Vec<G2Projective>, e: G2Projective) -> Self {
|
||||
DecryptionKey { a, b, dh, e }
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let num_nodes = self.nodes.len() as u32;
|
||||
let g1_elements = 1;
|
||||
let g2_elements = self.dh.len() + 2;
|
||||
|
||||
// unfortunately we're not going to know the expected capacity
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&num_nodes.to_be_bytes());
|
||||
// the extra 8 comes from the triple u32 we use for encoding lengths of ds and dh
|
||||
let mut bytes = Vec::with_capacity(g1_elements * 48 + g2_elements * 96 + 8);
|
||||
|
||||
for node in &self.nodes {
|
||||
let mut node_bytes = node.to_bytes();
|
||||
bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes()));
|
||||
bytes.append(&mut node_bytes)
|
||||
bytes.extend_from_slice(self.a.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(self.b.to_bytes().as_ref());
|
||||
bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes()));
|
||||
for dh_i in &self.dh {
|
||||
bytes.extend_from_slice(dh_i.to_bytes().as_ref());
|
||||
}
|
||||
bytes.extend_from_slice(self.e.to_bytes().as_ref());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
|
||||
// we have to be able to read the length of nodes
|
||||
if b.len() < 4 {
|
||||
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
|
||||
// at the very least we require bytes for:
|
||||
// - a ( 48 )
|
||||
// - b ( 96 )
|
||||
// - length indication of dh ( 4 )
|
||||
// - e ( 96 )
|
||||
if bytes.len() < 48 + 96 + 4 + 96 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"DecryptionKey",
|
||||
"Node",
|
||||
"insufficient number of bytes provided",
|
||||
));
|
||||
}
|
||||
let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
|
||||
let mut nodes = Vec::with_capacity(nodes_len);
|
||||
|
||||
let mut i = 4;
|
||||
for _ in 0..nodes_len {
|
||||
// check if we can actually read the length...
|
||||
if b[i..].len() < 4 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"DecryptionKey.Node",
|
||||
"insufficient number of bytes provided for BTE Node recovery",
|
||||
));
|
||||
}
|
||||
let mut i = 0;
|
||||
let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.a", "invalid curve point")
|
||||
})?;
|
||||
i += 48;
|
||||
|
||||
let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize;
|
||||
if b[i + 4..].len() < node_bytes {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"DecryptionKey.Node",
|
||||
"insufficient number of bytes provided for BTE Node recovery",
|
||||
));
|
||||
}
|
||||
i += 4;
|
||||
let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.b", "invalid curve point")
|
||||
})?;
|
||||
i += 96;
|
||||
|
||||
let node = Node::try_from_bytes(&b[i..i + node_bytes])?;
|
||||
nodes.push(node);
|
||||
i += node_bytes;
|
||||
let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
|
||||
i += 4;
|
||||
|
||||
if bytes[i..].len() != (dh_len + 1) * 96 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Node",
|
||||
"insufficient number of bytes provided (dh)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(DecryptionKey { nodes })
|
||||
let mut dh = Vec::with_capacity(dh_len);
|
||||
for j in 0..dh_len {
|
||||
let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure(
|
||||
format!("Node.dh_{}", j),
|
||||
"invalid curve point",
|
||||
)
|
||||
})?;
|
||||
|
||||
dh.push(dh_i);
|
||||
i += 96;
|
||||
}
|
||||
|
||||
let e = deserialize_g2(&bytes[i..]).ok_or_else(|| {
|
||||
DkgError::new_deserialization_failure("Node.h", "invalid curve point")
|
||||
})?;
|
||||
|
||||
Ok(Self { a, b, dh, e })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KeyPair {
|
||||
pub(crate) private_key: DecryptionKey,
|
||||
pub(crate) public_key: PublicKeyWithProof,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new(params: &Params, rng: impl RngCore) -> Self {
|
||||
let (dk, pk) = keygen(params, rng);
|
||||
Self {
|
||||
private_key: dk,
|
||||
public_key: pk,
|
||||
}
|
||||
}
|
||||
pub fn private_key(&self) -> &DecryptionKey {
|
||||
&self.private_key
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKeyWithProof {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result<Self, DkgError> {
|
||||
Ok(KeyPair {
|
||||
private_key: DecryptionKey::try_from_bytes(priv_bytes)?,
|
||||
public_key: PublicKeyWithProof::try_from_bytes(pub_bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKeyPair for KeyPair {
|
||||
type PrivatePemKey = DecryptionKey;
|
||||
type PublicPemKey = PublicKeyWithProof;
|
||||
|
||||
fn private_key(&self) -> &Self::PrivatePemKey {
|
||||
self.private_key()
|
||||
}
|
||||
|
||||
fn public_key(&self) -> &Self::PublicPemKey {
|
||||
self.public_key()
|
||||
}
|
||||
|
||||
fn from_keys(private_key: Self::PrivatePemKey, public_key: Self::PublicPemKey) -> Self {
|
||||
KeyPair {
|
||||
private_key,
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,163 +295,8 @@ impl DecryptionKey {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bte::setup;
|
||||
use bitvec::bitvec;
|
||||
use bitvec::order::Msb0;
|
||||
use rand_core::SeedableRng;
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn basic_coverage_nodes() {
|
||||
// it's some basic test I've been performing when writing the update function, but figured
|
||||
// might as well put it into a unit test. note that it doesn't check the entire structure,
|
||||
// but just the few last nodes of low height
|
||||
|
||||
let params = setup();
|
||||
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, _) = keygen(¶ms, &mut rng);
|
||||
|
||||
let root_node_copy = dk.nodes.clone();
|
||||
|
||||
// this is a root node
|
||||
assert_eq!(dk.nodes.len(), 1);
|
||||
assert!(dk.nodes[0].is_root());
|
||||
|
||||
// we have to have a node for right branch on each height (1, 01, 001, ... etc)
|
||||
// plus an additional one for the two left-most leaves (epochs "0" and "1")
|
||||
dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap();
|
||||
assert_eq!(dk.nodes.len(), 33);
|
||||
|
||||
let expected_last = Tau::new(0);
|
||||
// (and yes, I had to look up those names in a thesaurus)
|
||||
let expected_penultimate = Tau::new(1);
|
||||
// note that this value is 31bit long
|
||||
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1
|
||||
]);
|
||||
|
||||
let mut nodes_iter = dk.nodes.iter().rev();
|
||||
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
|
||||
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
|
||||
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
|
||||
|
||||
let mut epoch_zero_nodes = dk.nodes.clone();
|
||||
|
||||
// nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf
|
||||
dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap();
|
||||
assert_eq!(dk.nodes.len(), 32);
|
||||
epoch_zero_nodes.pop().unwrap();
|
||||
assert_eq!(
|
||||
epoch_zero_nodes
|
||||
.iter()
|
||||
.map(|node| node.tau.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
dk.nodes
|
||||
.iter()
|
||||
.map(|node| node.tau.clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
dk.try_update_to(Epoch::new(2), ¶ms, &mut rng).unwrap();
|
||||
dk.try_update_to(Epoch::new(3), ¶ms, &mut rng).unwrap();
|
||||
dk.try_update_to(Epoch::new(4), ¶ms, &mut rng).unwrap();
|
||||
|
||||
let expected_last = Tau::new(4);
|
||||
let expected_penultimate = Tau::new(5);
|
||||
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
|
||||
]);
|
||||
let expected_preantepenultimate = Tau(bitvec![u32, Msb0;
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||
]);
|
||||
assert_eq!(dk.nodes.len(), 32);
|
||||
let mut nodes_iter = dk.nodes.iter().rev();
|
||||
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
|
||||
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
|
||||
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
|
||||
assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau);
|
||||
|
||||
// the result should be the same of regardless if we update incrementally or go to the target immediately
|
||||
let mut new_root = DecryptionKey {
|
||||
nodes: root_node_copy,
|
||||
};
|
||||
new_root
|
||||
.try_update_to(Epoch::new(4), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
dk.nodes
|
||||
.iter()
|
||||
.map(|node| node.tau.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
new_root
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|node| node.tau.clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// getting expected nodes for those epochs is non-trivial for test purposes, but the last node
|
||||
// should ALWAYS be equal to the target epoch
|
||||
dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap();
|
||||
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42));
|
||||
dk.try_update_to(Epoch::new(123456), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456));
|
||||
dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435));
|
||||
|
||||
// trying to go to past epochs fails
|
||||
assert!(dk
|
||||
.try_update_to(Epoch::new(531), ¶ms, &mut rng)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn updating_to_next_epoch() {
|
||||
let params = setup();
|
||||
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, _) = keygen(¶ms, &mut rng);
|
||||
|
||||
// for root node current epoch is `None`
|
||||
assert_eq!(None, dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
// for root node it should result in epoch 0
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(Some(Epoch::new(0)), dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(Some(Epoch::new(1)), dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(Some(Epoch::new(2)), dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
// if we start from some non-root epoch, it should result in l + 1
|
||||
dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap();
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(Some(Epoch::new(43)), dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
dk.try_update_to(Epoch::new(12345), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(¶ms).unwrap());
|
||||
|
||||
dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap();
|
||||
assert_eq!(
|
||||
Some(Epoch::new(3292547436)),
|
||||
dk.current_epoch(¶ms).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_with_proof_roundtrip() {
|
||||
let params = setup();
|
||||
@@ -816,64 +310,4 @@ mod tests {
|
||||
|
||||
assert_eq!(pk, recovered)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn bte_node_roundtrip() {
|
||||
let params = setup();
|
||||
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, _) = keygen(¶ms, &mut rng);
|
||||
|
||||
let root_node = dk.nodes[0].clone();
|
||||
let bytes = root_node.to_bytes();
|
||||
let recovered = Node::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(root_node, recovered);
|
||||
|
||||
dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
for node in &dk.nodes {
|
||||
let bytes = node.to_bytes();
|
||||
let recovered = Node::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(node, &recovered);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn decryption_key_node_roundtrip() {
|
||||
let params = setup();
|
||||
|
||||
let dummy_seed = [1u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let (mut dk, _) = keygen(¶ms, &mut rng);
|
||||
|
||||
let bytes = dk.to_bytes();
|
||||
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(dk, recovered);
|
||||
|
||||
dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap();
|
||||
let bytes = dk.to_bytes();
|
||||
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(dk, recovered);
|
||||
|
||||
dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap();
|
||||
let bytes = dk.to_bytes();
|
||||
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(dk, recovered);
|
||||
|
||||
dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap();
|
||||
let bytes = dk.to_bytes();
|
||||
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(dk, recovered);
|
||||
|
||||
dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng)
|
||||
.unwrap();
|
||||
let bytes = dk.to_bytes();
|
||||
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(dk, recovered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::DkgError;
|
||||
use crate::utils::{hash_g2, RandomOracleBuilder};
|
||||
use crate::utils::hash_g2;
|
||||
use crate::{Chunk, Share};
|
||||
use bitvec::field::BitField;
|
||||
use bitvec::order::Msb0;
|
||||
use bitvec::vec::BitVec;
|
||||
use bitvec::view::BitView;
|
||||
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt};
|
||||
use bls12_381::{G1Affine, G2Affine, G2Prepared, G2Projective, Gt};
|
||||
use group::Curve;
|
||||
use lazy_static::lazy_static;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub mod encryption;
|
||||
pub mod keys;
|
||||
@@ -35,10 +29,6 @@ lazy_static! {
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
|
||||
const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP";
|
||||
|
||||
// this particular domain is not for curve hashing, but might as well also follow the same naming pattern
|
||||
const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION";
|
||||
|
||||
const MAX_EPOCHS_EXP: usize = 32;
|
||||
const HASH_SECURITY_PARAM: usize = 256;
|
||||
|
||||
// note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE
|
||||
@@ -49,253 +39,17 @@ pub const SCALAR_SIZE: usize = 32;
|
||||
/// In paper B; number of distinct chunks
|
||||
pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3);
|
||||
|
||||
pub(crate) type EpochStore = u32;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
// None empty bitvec implies this is a root node
|
||||
pub(crate) struct Tau(BitVec<EpochStore, Msb0>);
|
||||
|
||||
impl Tau {
|
||||
pub fn new_root() -> Self {
|
||||
Tau(BitVec::new())
|
||||
}
|
||||
|
||||
// TODO: perhaps this should be explicitly moved to some test module
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(epoch: EpochStore) -> Self {
|
||||
Tau(epoch.view_bits().to_bitvec())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn left_child(&self) -> Self {
|
||||
let mut child = self.0.clone();
|
||||
child.push(false);
|
||||
Tau(child)
|
||||
}
|
||||
|
||||
pub fn right_child(&self) -> Self {
|
||||
let mut child = self.0.clone();
|
||||
child.push(true);
|
||||
Tau(child)
|
||||
}
|
||||
|
||||
pub fn is_leaf(&self, params: &Params) -> bool {
|
||||
self.height() == params.lambda_t
|
||||
}
|
||||
|
||||
pub fn try_get_parent_at_height(&self, height: usize) -> Result<Self, DkgError> {
|
||||
if height > self.0.len() {
|
||||
return Err(DkgError::NotAValidParent);
|
||||
}
|
||||
|
||||
Ok(Tau(self.0[..height].to_bitvec()))
|
||||
}
|
||||
|
||||
// essentially is this tau prefixing the other
|
||||
pub fn is_parent_of(&self, other: &Tau) -> bool {
|
||||
if self.0.len() > other.0.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i, b) in self.0.iter().enumerate() {
|
||||
if b != other.0[i] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result<Epoch, DkgError> {
|
||||
if self.0.len() > params.lambda_t {
|
||||
// this node is already BELOW a valid leaf-epoch node. it can only happen
|
||||
// if either some invariant was broken or additional data was pushed to `tau`
|
||||
// in order compute some intermediate results, but in that case this method should have
|
||||
// never been called anyway. tl;dr: if this is called, the underlying key is malformed
|
||||
return Err(DkgError::NotAValidParent);
|
||||
}
|
||||
let mut child = self.0.clone();
|
||||
for _ in 0..(params.lambda_t - self.0.len()) {
|
||||
child.push(false)
|
||||
}
|
||||
|
||||
// the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here
|
||||
// (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway
|
||||
// but I'd prefer to just blow up since this would be a serious error
|
||||
Ok(Epoch::try_from_tau(&Tau(child), params).unwrap())
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn extend(
|
||||
&self,
|
||||
rr: &[G1Projective; NUM_CHUNKS],
|
||||
ss: &[G1Projective; NUM_CHUNKS],
|
||||
cc: &[[G1Projective; NUM_CHUNKS]],
|
||||
) -> Self {
|
||||
let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN);
|
||||
random_oracle_builder.update_with_g1_elements(rr.iter());
|
||||
random_oracle_builder.update_with_g1_elements(ss.iter());
|
||||
for ciphertext_chunks in cc {
|
||||
random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter());
|
||||
}
|
||||
|
||||
let tau_mem = self.0.as_raw_slice();
|
||||
assert_eq!(tau_mem.len(), 1, "tau length invariant was broken");
|
||||
random_oracle_builder.update(tau_mem[0].to_be_bytes());
|
||||
|
||||
let oracle_output = random_oracle_builder.finalize();
|
||||
debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM);
|
||||
|
||||
let mut extended_tau = self.clone();
|
||||
for byte in oracle_output {
|
||||
extended_tau
|
||||
.0
|
||||
.extend_from_bitslice(byte.view_bits::<Msb0>())
|
||||
}
|
||||
|
||||
extended_tau
|
||||
}
|
||||
|
||||
// considers all lambda_t + lambda_h bits
|
||||
fn evaluate_f(&self, params: &Params) -> G2Projective {
|
||||
self.0
|
||||
.iter()
|
||||
.by_vals()
|
||||
.zip(params.fs.iter().chain(params.fh.iter()))
|
||||
.filter(|(i, _)| *i)
|
||||
.map(|(_, f_i)| f_i)
|
||||
.fold(params.f0, |acc, f_i| acc + f_i)
|
||||
}
|
||||
|
||||
// only considers up to lambda_t bits
|
||||
fn evaluate_partial_f(&self, params: &Params) -> G2Projective {
|
||||
self.0
|
||||
.iter()
|
||||
.by_vals()
|
||||
.zip(params.fs.iter())
|
||||
.filter(|(i, _)| *i)
|
||||
.map(|(_, f_i)| f_i)
|
||||
.fold(params.f0, |acc, f_i| acc + f_i)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let len_bytes = (self.0.len() as u32).to_be_bytes();
|
||||
len_bytes
|
||||
.into_iter()
|
||||
.chain(self.0.chunks(8).map(BitField::load_be))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
|
||||
if b.len() < 4 {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Tau",
|
||||
"insufficient number of bytes provided",
|
||||
));
|
||||
}
|
||||
let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
|
||||
|
||||
// maximum theoretical length
|
||||
if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM {
|
||||
return Err(DkgError::new_deserialization_failure(
|
||||
"Tau",
|
||||
format!(
|
||||
"malformed length {} is greater than maximum {}",
|
||||
tau_len,
|
||||
MAX_EPOCHS_EXP + HASH_SECURITY_PARAM
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if tau_len == 0 {
|
||||
if b.len() != 4 {
|
||||
Err(DkgError::new_deserialization_failure(
|
||||
"Tau",
|
||||
"malformed bytes",
|
||||
))
|
||||
} else {
|
||||
Ok(Tau::new_root())
|
||||
}
|
||||
} else if b.len() == 4 {
|
||||
Err(DkgError::new_deserialization_failure(
|
||||
"Tau",
|
||||
"insufficient number of bytes provided",
|
||||
))
|
||||
} else {
|
||||
let mut inner = BitVec::repeat(false, tau_len);
|
||||
for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) {
|
||||
slot.store_be(byte);
|
||||
}
|
||||
|
||||
Ok(Tau(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Zeroize for Tau {
|
||||
fn zeroize(&mut self) {
|
||||
for v in self.0.as_raw_mut_slice() {
|
||||
v.zeroize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)]
|
||||
pub struct Epoch(EpochStore);
|
||||
|
||||
impl Epoch {
|
||||
pub fn new(value: EpochStore) -> Self {
|
||||
Epoch(value)
|
||||
}
|
||||
|
||||
pub(crate) fn as_tau(&self) -> Tau {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
pub(crate) fn as_extended_tau(
|
||||
&self,
|
||||
rr: &[G1Projective; NUM_CHUNKS],
|
||||
ss: &[G1Projective; NUM_CHUNKS],
|
||||
cc: &[[G1Projective; NUM_CHUNKS]],
|
||||
) -> Tau {
|
||||
self.as_tau().extend(rr, ss, cc)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result<Self, DkgError> {
|
||||
if !tau.is_leaf(params) {
|
||||
Err(DkgError::MalformedEpoch)
|
||||
} else {
|
||||
Ok(Epoch(tau.0.load_be()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Epoch> for Tau {
|
||||
fn from(epoch: Epoch) -> Self {
|
||||
Tau(epoch.0.view_bits().to_bitvec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EpochStore> for Epoch {
|
||||
fn from(epoch: EpochStore) -> Self {
|
||||
Epoch(epoch)
|
||||
}
|
||||
// considers all lambda_h bits
|
||||
pub fn evaluate_f(params: &Params) -> G2Projective {
|
||||
params.fh.iter().fold(params.f0, |acc, f_i| acc + f_i)
|
||||
}
|
||||
|
||||
pub struct Params {
|
||||
/// Maximum size of an epoch, in bits.
|
||||
pub lambda_t: usize,
|
||||
|
||||
/// Security parameter of our $H_{\Lamda_H}$ hash function
|
||||
pub lambda_h: usize,
|
||||
|
||||
// keeping f0 separate from the rest of the curve points makes it easier to work with tau
|
||||
f0: G2Projective,
|
||||
fs: Vec<G2Projective>, // f_1, f_2, .... f_{lambda_t} in the paper
|
||||
fh: Vec<G2Projective>, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper
|
||||
fh: Vec<G2Projective>, // f_{lambda_h}, f_{lambda_h+1}, .... f_{lambda_h} in the paper
|
||||
h: G2Projective,
|
||||
|
||||
/// Precomputed `h` used for the miller loop
|
||||
@@ -305,10 +59,6 @@ pub struct Params {
|
||||
pub fn setup() -> Params {
|
||||
let f0 = hash_g2(b"f0", SETUP_DOMAIN);
|
||||
|
||||
let fs = (1..=MAX_EPOCHS_EXP)
|
||||
.map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN))
|
||||
.collect();
|
||||
|
||||
let fh = (0..HASH_SECURITY_PARAM)
|
||||
.map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN))
|
||||
.collect();
|
||||
@@ -316,148 +66,10 @@ pub fn setup() -> Params {
|
||||
let h = hash_g2(b"h", SETUP_DOMAIN);
|
||||
|
||||
Params {
|
||||
lambda_t: MAX_EPOCHS_EXP,
|
||||
lambda_h: HASH_SECURITY_PARAM,
|
||||
f0,
|
||||
fs,
|
||||
fh,
|
||||
h,
|
||||
_h_prepared: G2Prepared::from(h.to_affine()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitvec::bitvec;
|
||||
use bitvec::order::Msb0;
|
||||
|
||||
#[test]
|
||||
fn creating_tau_from_epoch() {
|
||||
assert!(Tau::new_root().0.is_empty());
|
||||
|
||||
let zero = Tau::new(0);
|
||||
assert!(zero.0.iter().by_vals().all(|b| !b));
|
||||
|
||||
let one = Tau::new(1);
|
||||
let mut iter = one.0.iter().by_vals();
|
||||
// first 31 bits are 0, the last one is 1
|
||||
for _ in 0..31 {
|
||||
assert!(!iter.next().unwrap())
|
||||
}
|
||||
assert!(iter.next().unwrap());
|
||||
|
||||
// 101010 in binary
|
||||
let forty_two = Tau::new(42);
|
||||
// first 26 bits are not set
|
||||
let mut iter = forty_two.0.iter().by_vals();
|
||||
for _ in 0..26 {
|
||||
assert!(!iter.next().unwrap())
|
||||
}
|
||||
assert!(iter.next().unwrap());
|
||||
assert!(!iter.next().unwrap());
|
||||
assert!(iter.next().unwrap());
|
||||
assert!(!iter.next().unwrap());
|
||||
assert!(iter.next().unwrap());
|
||||
assert!(!iter.next().unwrap());
|
||||
|
||||
// value that requires an actual u32 (i.e. takes 4 bytes to represent)
|
||||
// 11000100_01000000_01001001_01101011 in binary
|
||||
let big_val = Tau::new(3292547435);
|
||||
let expected = bitvec![u32, Msb0;
|
||||
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
|
||||
0, 1, 1
|
||||
];
|
||||
assert_eq!(expected, big_val.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getting_parent_at_height() {
|
||||
let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]);
|
||||
|
||||
let expected_0 = Tau(BitVec::new());
|
||||
let expected_1 = Tau(bitvec![u32, Msb0; 1]);
|
||||
let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]);
|
||||
|
||||
assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap());
|
||||
assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap());
|
||||
assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap());
|
||||
assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap());
|
||||
assert!(tau.try_get_parent_at_height(8).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_tau_to_epoch() {
|
||||
let params = setup();
|
||||
|
||||
let tau0: Tau = Epoch::new(0).into();
|
||||
let tau1: Tau = Epoch::new(1).into();
|
||||
let tau42: Tau = Epoch::new(42).into();
|
||||
let tau_big: Tau = Epoch::new(3292547435).into();
|
||||
|
||||
assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, ¶ms).unwrap());
|
||||
assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, ¶ms).unwrap());
|
||||
assert_eq!(
|
||||
Epoch::new(42),
|
||||
Epoch::try_from_tau(&tau42, ¶ms).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Epoch::new(3292547435),
|
||||
Epoch::try_from_tau(&tau_big, ¶ms).unwrap()
|
||||
);
|
||||
|
||||
assert!(Epoch::try_from_tau(&Tau(BitVec::new()), ¶ms).is_err());
|
||||
assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), ¶ms).is_err());
|
||||
let _31bit_tau = Tau(bitvec![u32, Msb0;
|
||||
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
|
||||
0, 1
|
||||
]);
|
||||
assert!(Epoch::try_from_tau(&_31bit_tau, ¶ms).is_err());
|
||||
|
||||
let _33bit_tau = Tau(bitvec![u32, Msb0;
|
||||
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
|
||||
0, 1, 1, 0
|
||||
]);
|
||||
assert!(Epoch::try_from_tau(&_33bit_tau, ¶ms).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tau_roundtrip() {
|
||||
let good_taus = vec![
|
||||
Tau::new_root(),
|
||||
Tau::new(0),
|
||||
Tau::new(1),
|
||||
Tau::new(2),
|
||||
Tau::new(42),
|
||||
Tau::new(123456),
|
||||
Tau::new(3292547435),
|
||||
Tau::new(u32::MAX),
|
||||
];
|
||||
|
||||
for tau in good_taus {
|
||||
let bytes = tau.to_bytes();
|
||||
let recovered = Tau::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(tau, recovered);
|
||||
}
|
||||
|
||||
// more valid variants
|
||||
let mut another_tau = Tau::new(u32::MAX);
|
||||
another_tau.0.push(true);
|
||||
another_tau.0.push(false);
|
||||
another_tau.0.push(true);
|
||||
|
||||
let bytes = another_tau.to_bytes();
|
||||
let recovered = Tau::try_from_bytes(&bytes).unwrap();
|
||||
assert_eq!(another_tau, recovered);
|
||||
|
||||
// ensure there are no panics
|
||||
let big_length_bytes = [255, 255, 255, 255, 42];
|
||||
assert!(Tau::try_from_bytes(&big_length_bytes).is_err());
|
||||
|
||||
assert!(Tau::try_from_bytes(&[]).is_err());
|
||||
assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err());
|
||||
assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err());
|
||||
assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err());
|
||||
assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user