Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ff49ad948 | |||
| 76e21c229b | |||
| b68bab3cf9 | |||
| 5da1d89501 | |||
| d7090ecb5a | |||
| 1a1a2a0355 | |||
| 8bcc931d9b | |||
| 955ef7b871 | |||
| 9ee7ad4fa8 | |||
| 79aa1febbe | |||
| 32c4974c44 | |||
| 5dadd73dfd | |||
| 6ad5badef4 | |||
| 480ad18b2e | |||
| e7716ae852 |
@@ -1,136 +0,0 @@
|
||||
name: Continuous integration
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.os == 'windows-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [stable, beta, nightly]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
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
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --all
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.rust != 'nightly' }}
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
# COCONUT stuff
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
# BUILD
|
||||
- name: Build gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-gateway --features=coconut
|
||||
|
||||
- name: Build native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-client --features=coconut
|
||||
|
||||
- name: Build socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-socks5-client --features=coconut
|
||||
|
||||
- name: Build validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --bin nym-validator-api --features=coconut
|
||||
|
||||
# TEST
|
||||
- name: Test gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-gateway --features=coconut
|
||||
|
||||
- name: Test native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-client --features=coconut
|
||||
|
||||
- name: Test socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-socks5-client --features=coconut
|
||||
|
||||
- name: Test validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --bin nym-validator-api --features=coconut
|
||||
|
||||
# CLIPPY
|
||||
|
||||
- name: Run clippy on gateway with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-gateway --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on native client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-client --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on socks5 client with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-socks5-client --features=coconut -- -D warnings
|
||||
|
||||
- name: Run clippy on validator-api with coconut feature
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --bin nym-validator-api --features=coconut -- -D warnings
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Clippy check
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Mixnet Contract
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
mixnet-contract:
|
||||
# since it's going to be compiled into wasm, there's absolutely
|
||||
# no point in running CI on different OS-es
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [ stable, beta, nightly ]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml -- --check
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.rust != 'nightly' }}
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path contracts/mixnet/Cargo.toml -- -D warnings
|
||||
@@ -1,56 +0,0 @@
|
||||
name: CI for Network Explorer
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'explorer/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: explorer
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npm install
|
||||
continue-on-error: true
|
||||
- run: npm run test
|
||||
continue-on-error: true
|
||||
- run: npm run build
|
||||
continue-on-error: true
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "explorer/dist/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Keybase - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files/messages
|
||||
- name: Keybase - Send Notification
|
||||
env:
|
||||
NYM_PROJECT_NAME: "Network Explorer"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
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_NYMBOT_TEAM }}"
|
||||
KEYBASE_NYM_CHANNEL: "ci-network-explorer"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/messages/entry_point_notifications.sh
|
||||
@@ -1,77 +0,0 @@
|
||||
name: Webdriverio tests for nym wallet
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tauri-wallet/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tauri-wallet
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: wallet tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Tauri dependencies
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libgtksourceview-3.0-dev
|
||||
webkit2gtk-4.0
|
||||
libappindicator3-dev
|
||||
webkit2gtk-driver
|
||||
xvfb
|
||||
|
||||
- name: Install minimal stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Node v16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Install yarn for building application
|
||||
run: yarn install
|
||||
|
||||
- name: Build application
|
||||
run: yarn run webpack:build & yarn run tauri:build
|
||||
|
||||
- name: Check binary exists
|
||||
run: |
|
||||
cd target/release/
|
||||
(test -f nym-wallet && echo nym binary exists) || echo wallet does not exist
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
working-directory: tauri-wallet/webdriver
|
||||
|
||||
- name: Remove existing user datafile
|
||||
uses: JesseTG/rm@v1.0.2
|
||||
with:
|
||||
path: tauri-wallet/webdriver/common/data/user-data.json
|
||||
|
||||
- name: Create user data json file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "user-data.json"
|
||||
json: ${{ secrets.WALLET_USERDATA }}
|
||||
dir: 'tauri-wallet/webdriver/common/data/'
|
||||
|
||||
- name: Install tauri-driver
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: tauri-driver
|
||||
|
||||
- name: Launch tests
|
||||
run: xvfb-run yarn test:newuser
|
||||
working-directory: tauri-wallet/webdriver
|
||||
@@ -1,5 +1,5 @@
|
||||
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
|
||||
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.GITHUB_REF_SLUG }}.{{ env.NYM_CI_WWW_BASE }}/
|
||||
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
|
||||
> ✅ **SUCCESS**
|
||||
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
|
||||
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
name: Publish Nym Wallet
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- tauri-wallet-*
|
||||
branches:
|
||||
- feature/gh-action-build-tauri-wallet
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tauri-wallet
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-10.15, macos-11, windows-latest]
|
||||
outputs:
|
||||
PACKAGE_VERSION: ${{ steps.package-node-version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Tauri dependencies
|
||||
if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libgtksourceview-3.0-dev
|
||||
webkit2gtk-4.0
|
||||
libappindicator3-dev
|
||||
webkit2gtk-driver
|
||||
xvfb
|
||||
|
||||
- name: Install minimal stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Node v16
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Read node from package.json
|
||||
uses: culshaw/read-package-node-version-actions@v1
|
||||
with:
|
||||
path: "${{github.workspace}}/tauri-wallet"
|
||||
id: package-node-version
|
||||
|
||||
- name: Install yarn for building application
|
||||
run: yarn install
|
||||
|
||||
- name: Build application (bundle)
|
||||
run: yarn run webpack:build
|
||||
|
||||
- name: Build application (tauri)
|
||||
run: yarn run tauri:build
|
||||
|
||||
# - name: Fake build
|
||||
# run: |
|
||||
# mkdir release-assets
|
||||
# cp package.json release-assets/${{ matrix.os }}-${{ steps.package-node-version.outputs.version }}.zip
|
||||
|
||||
- name: Display built app target files
|
||||
run: ls target/release
|
||||
|
||||
- name: Display built app target bundle files
|
||||
run: ls -R target/release/bundle
|
||||
|
||||
- name: Compress (MacOS / Linux)
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
working-directory: tauri-wallet
|
||||
env:
|
||||
TARGET_FILE: ${{ matrix.os }}_v${{ steps.package-node-version.outputs.version }}.tar.gz
|
||||
run: |
|
||||
mkdir release-assets
|
||||
pushd target/release/bundle
|
||||
tar -cvzf ../../../release-assets/$TARGET_FILE .
|
||||
|
||||
- name: Compress (Windows)
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
uses: thedoctor0/zip-release@master
|
||||
with:
|
||||
type: 'zip'
|
||||
filename: '${{github.workspace}}/tauri-wallet/release-assets/${{ runner.os }}_v${{ steps.package-node-version.outputs.version }}.zip'
|
||||
path: "${{github.workspace}}/tauri-wallet/target/release/bundle"
|
||||
|
||||
- name: Upload release assets
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-assets-${{ matrix.os }}
|
||||
path: "${{github.workspace}}/tauri-wallet/release-assets/*"
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.build.outputs.PACKAGE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download assets
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: download-assets
|
||||
- name: Display downloaded assets
|
||||
run: ls -R download-assets
|
||||
- name: Re-arrange downloaded assets
|
||||
run: |
|
||||
mkdir release-assets
|
||||
find download-assets -mindepth 2 -type f -exec mv -t ${{github.workspace}}/release-assets -i '{}' +
|
||||
rm -rf download-assets
|
||||
- name: Hash files
|
||||
id: file_hashes
|
||||
# Put the multi-line string in an env var and create a new action env var by wrapping the multiline value
|
||||
run: |
|
||||
cd release-assets
|
||||
ASSET_HASHES=$(find . -type f -exec sha256sum {} \;)
|
||||
echo "ASSET_HASHES<<EOF" >> $GITHUB_ENV
|
||||
echo "$ASSET_HASHES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Display file hashes
|
||||
run: echo $ASSET_HASHES
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
# The pipe syntax needs to stay to escape the wildcard correctly
|
||||
artifacts: |
|
||||
release-assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: "nym-wallet-v${{env.PACKAGE_VERSION}}"
|
||||
commit: feature/gh-action-build-tauri-wallet
|
||||
draft: true
|
||||
name: "Nym Wallet v${{ env.PACKAGE_VERSION }}"
|
||||
body: |
|
||||
This is a build of the Nym Wallet from commit `${{ github.sha }}` [(browse commit)](https://github.com/nymtech/nym/tree/${{ github.sha }}).
|
||||
|
||||
Installers for each operating system have the following SHA256 hashes:
|
||||
|
||||
```
|
||||
${{ env.ASSET_HASHES }}
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Generate TS types
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
tauri-wallet-types:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libsoup2.4-dev librust-gdk-dev libwebkit2gtk-4.0-dev
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Generate TS
|
||||
run: cd tauri-wallet/src-tauri && cargo test
|
||||
- uses: EndBug/add-and-commit@v7.2.1 # https://github.com/marketplace/actions/add-commit
|
||||
with:
|
||||
add: '["tauri-wallet"]'
|
||||
message: '[ci skip] Generate TS types'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Wasm Client
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
wasm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown --features=coconut
|
||||
|
||||
# for some reason this does not seem to work correctly, leave it for later, building is good enough for now
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: test
|
||||
# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path clients/webassembly/Cargo.toml -- --check
|
||||
|
||||
# for some reason this does not seem to work correctly, leave it for later, building is good enough for now
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: --manifest-path clients/webassembly/Cargo.toml --target wasm32-unknown-unknown -- -D warnings
|
||||
@@ -11,7 +11,6 @@ target
|
||||
/.vscode/settings.json
|
||||
validator/.vscode
|
||||
sample-configs/validator-config.toml
|
||||
.vscode
|
||||
scripts/deploy_qa.sh
|
||||
scripts/run_gate.sh
|
||||
scripts/run_mix.sh
|
||||
|
||||
Generated
+20
-167
@@ -396,10 +396,7 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -471,15 +468,6 @@ dependencies = [
|
||||
"system-deps 3.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
|
||||
dependencies = [
|
||||
"rustc_version 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.70"
|
||||
@@ -643,11 +631,30 @@ dependencies = [
|
||||
name = "coconut-interface"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"coconut-rs",
|
||||
"getset",
|
||||
"nymcoconut",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coconut-rs"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/nymtech/coconut.git?branch=0.5.0#a1b72d51aa2a67b73b9f58d707030ae6dc70af7f"
|
||||
dependencies = [
|
||||
"bls12_381",
|
||||
"bs58",
|
||||
"digest 0.9.0",
|
||||
"ff",
|
||||
"getrandom 0.2.3",
|
||||
"group",
|
||||
"itertools",
|
||||
"rand 0.8.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.0.0"
|
||||
@@ -941,42 +948,6 @@ dependencies = [
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"cast",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"csv",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.1"
|
||||
@@ -1116,28 +1087,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-logs"
|
||||
version = "0.8.0"
|
||||
@@ -2322,12 +2271,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "3.5.5"
|
||||
@@ -3511,27 +3454,6 @@ dependencies = [
|
||||
"version-checker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bls12_381",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"doc-comment",
|
||||
"ff",
|
||||
"getrandom 0.2.3",
|
||||
"group",
|
||||
"itertools",
|
||||
"rand 0.8.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nymsphinx"
|
||||
version = "0.1.0"
|
||||
@@ -3696,12 +3618,6 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
@@ -4106,34 +4022,6 @@ version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmutil"
|
||||
version = "0.5.3"
|
||||
@@ -4661,12 +4549,6 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
@@ -4932,15 +4814,6 @@ dependencies = [
|
||||
"semver 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
@@ -5162,16 +5035,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.130"
|
||||
@@ -6420,16 +6283,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.12.0"
|
||||
|
||||
@@ -29,7 +29,6 @@ members = [
|
||||
"common/mixnode-common",
|
||||
"common/network-defaults",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nymcoconut",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use log::*;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::websocket;
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -24,26 +22,23 @@ use client_core::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::bandwidth::{
|
||||
prepare_for_spending, BandwidthVoucherAttributes, BANDWIDTH_VALUE, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::obtain_aggregate_verification_key;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::{
|
||||
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
|
||||
MixnetMessageSender,
|
||||
};
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::websocket;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::Credential;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key};
|
||||
|
||||
pub(crate) mod config;
|
||||
|
||||
@@ -181,17 +176,8 @@ impl NymClient {
|
||||
.await
|
||||
.expect("could not obtain aggregate verification key of validators");
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
|
||||
};
|
||||
|
||||
let bandwidth_credential = credentials::bandwidth::obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
&self.key_manager.identity_keypair().public_key().to_bytes(),
|
||||
&self.config.get_base().get_validator_api_endpoints(),
|
||||
)
|
||||
.await
|
||||
@@ -202,7 +188,6 @@ impl NymClient {
|
||||
prepare_for_spending(
|
||||
&self.key_manager.identity_keypair().public_key().to_bytes(),
|
||||
&bandwidth_credential,
|
||||
&bandwidth_credential_attributes,
|
||||
&verification_key,
|
||||
)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use url::Url;
|
||||
|
||||
use client_core::client::key_manager::KeyManager;
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters};
|
||||
use config::NymConfig;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::bandwidth::{
|
||||
prepare_for_spending, BandwidthVoucherAttributes, BANDWIDTH_VALUE, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::obtain_aggregate_verification_key;
|
||||
use crypto::asymmetric::{encryption, identity};
|
||||
use gateway_client::GatewayClient;
|
||||
use gateway_requests::registration::handshake::SharedKeys;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use topology::{filter::VersionFilterable, gateway};
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
use url::Url;
|
||||
|
||||
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
App::new("init")
|
||||
@@ -47,9 +36,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(Arg::with_name("validators")
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(Arg::with_name("disable-socket")
|
||||
.long("disable-socket")
|
||||
@@ -68,39 +57,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
)
|
||||
}
|
||||
|
||||
// this behaviour should definitely be changed, we shouldn't
|
||||
// need to get bandwidth credential for registration
|
||||
#[cfg(feature = "coconut")]
|
||||
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
|
||||
let verification_key = obtain_aggregate_verification_key(validators)
|
||||
.await
|
||||
.expect("could not obtain aggregate verification key of validators");
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
|
||||
};
|
||||
|
||||
let bandwidth_credential = credentials::bandwidth::obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
validators,
|
||||
)
|
||||
.await
|
||||
.expect("could not obtain bandwidth credential");
|
||||
|
||||
prepare_for_spending(
|
||||
raw_identity,
|
||||
&bandwidth_credential,
|
||||
&bandwidth_credential_attributes,
|
||||
&verification_key,
|
||||
)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
}
|
||||
|
||||
async fn register_with_gateway(
|
||||
gateway: &gateway::Node,
|
||||
our_identity: Arc<identity::KeyPair>,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use log::*;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
};
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -22,27 +23,21 @@ use client_core::client::topology_control::{
|
||||
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
|
||||
};
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::bandwidth::{
|
||||
prepare_for_spending, BandwidthVoucherAttributes, BANDWIDTH_VALUE, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::obtain_aggregate_verification_key;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::{
|
||||
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
|
||||
MixnetMessageSender,
|
||||
};
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::Credential;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key};
|
||||
|
||||
pub(crate) mod config;
|
||||
|
||||
@@ -169,17 +164,8 @@ impl NymClient {
|
||||
.await
|
||||
.expect("could not obtain aggregate verification key of validators");
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
|
||||
};
|
||||
|
||||
let bandwidth_credential = credentials::bandwidth::obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
&self.key_manager.identity_keypair().public_key().to_bytes(),
|
||||
&self.config.get_base().get_validator_api_endpoints(),
|
||||
)
|
||||
.await
|
||||
@@ -190,7 +176,6 @@ impl NymClient {
|
||||
prepare_for_spending(
|
||||
&self.key_manager.identity_keypair().public_key().to_bytes(),
|
||||
&bandwidth_credential,
|
||||
&bandwidth_credential_attributes,
|
||||
&verification_key,
|
||||
)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use rand::{prelude::SliceRandom, rngs::OsRng, thread_rng};
|
||||
use url::Url;
|
||||
|
||||
use client_core::client::key_manager::KeyManager;
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters};
|
||||
use config::NymConfig;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::bandwidth::{
|
||||
prepare_for_spending, BandwidthVoucherAttributes, BANDWIDTH_VALUE, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::obtain_aggregate_verification_key;
|
||||
use crypto::asymmetric::{encryption, identity};
|
||||
use gateway_client::GatewayClient;
|
||||
use gateway_requests::registration::handshake::SharedKeys;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use rand::{prelude::SliceRandom, rngs::OsRng, thread_rng};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use topology::{filter::VersionFilterable, gateway};
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
use url::Url;
|
||||
|
||||
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
App::new("init")
|
||||
@@ -51,9 +40,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(Arg::with_name("validators")
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(Arg::with_name("port")
|
||||
.short("p")
|
||||
@@ -68,39 +57,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
)
|
||||
}
|
||||
|
||||
// this behaviour should definitely be changed, we shouldn't
|
||||
// need to get bandwidth credential for registration
|
||||
#[cfg(feature = "coconut")]
|
||||
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
|
||||
let verification_key = obtain_aggregate_verification_key(validators)
|
||||
.await
|
||||
.expect("could not obtain aggregate verification key of validators");
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar("BandwidthVoucher"),
|
||||
};
|
||||
|
||||
let bandwidth_credential = credentials::bandwidth::obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
validators,
|
||||
)
|
||||
.await
|
||||
.expect("could not obtain bandwidth credential");
|
||||
|
||||
prepare_for_spending(
|
||||
raw_identity,
|
||||
&bandwidth_credential,
|
||||
&bandwidth_credential_attributes,
|
||||
&verification_key,
|
||||
)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
}
|
||||
|
||||
async fn register_with_gateway(
|
||||
gateway: &gateway::Node,
|
||||
our_identity: Arc<identity::KeyPair>,
|
||||
|
||||
@@ -3,24 +3,21 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
use coconut_interface::{
|
||||
self, hash_to_scalar, Attribute, Credential, Parameters, Signature, Theta, VerificationKey,
|
||||
};
|
||||
use credentials::{obtain_aggregate_signature, obtain_aggregate_verification_key};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
struct State {
|
||||
signatures: Vec<Signature>,
|
||||
n_attributes: u32,
|
||||
params: Parameters,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
voucher_value: Attribute,
|
||||
voucher_info: Attribute,
|
||||
public_attributes_bytes: Vec<Vec<u8>>,
|
||||
public_attributes: Vec<Attribute>,
|
||||
private_attributes: Vec<Attribute>,
|
||||
aggregated_verification_key: Option<VerificationKey>,
|
||||
}
|
||||
|
||||
@@ -40,10 +37,9 @@ impl State {
|
||||
signatures: Vec::new(),
|
||||
n_attributes,
|
||||
params,
|
||||
serial_number: private_attributes[0],
|
||||
binding_number: private_attributes[1],
|
||||
voucher_value: public_attributes[0],
|
||||
voucher_info: public_attributes[1],
|
||||
public_attributes_bytes,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
aggregated_verification_key: None,
|
||||
}
|
||||
}
|
||||
@@ -67,8 +63,8 @@ async fn randomise_credential(
|
||||
) -> Result<Vec<Signature>, String> {
|
||||
let mut state = state.write().await;
|
||||
let signature = state.signatures.remove(idx);
|
||||
let (new_signature, _) = signature.randomise(&state.params);
|
||||
state.signatures.insert(idx, new_signature);
|
||||
let new = signature.randomise(&state.params);
|
||||
state.signatures.insert(idx, new);
|
||||
Ok(state.signatures.clone())
|
||||
}
|
||||
|
||||
@@ -121,15 +117,14 @@ async fn prove_credential(
|
||||
let state = state.read().await;
|
||||
|
||||
if let Some(signature) = state.signatures.get(idx) {
|
||||
match coconut_interface::prove_bandwidth_credential(
|
||||
match coconut_interface::prove_credential(
|
||||
&state.params,
|
||||
&verification_key,
|
||||
signature,
|
||||
state.serial_number,
|
||||
state.binding_number,
|
||||
&state.private_attributes,
|
||||
) {
|
||||
Ok(theta) => Ok(theta),
|
||||
Err(e) => Err(format!("{:?}", e)),
|
||||
Err(e) => Err(format!("{}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Got invalid Signature idx".to_string())
|
||||
@@ -149,15 +144,10 @@ async fn verify_credential(
|
||||
|
||||
let state = state.read().await;
|
||||
|
||||
let public_attributes_bytes = vec![
|
||||
state.voucher_value.to_bytes().to_vec(),
|
||||
state.voucher_info.to_bytes().to_vec(),
|
||||
];
|
||||
|
||||
let credential = Credential::new(
|
||||
state.n_attributes,
|
||||
theta,
|
||||
public_attributes_bytes,
|
||||
state.public_attributes_bytes.clone(),
|
||||
state
|
||||
.signatures
|
||||
.get(idx)
|
||||
@@ -174,13 +164,11 @@ async fn get_credential(
|
||||
) -> Result<Vec<Signature>, String> {
|
||||
let guard = state.read().await;
|
||||
let parsed_urls = parse_url_validators(&validator_urls)?;
|
||||
let public_attributes = vec![guard.voucher_value, guard.voucher_info];
|
||||
let private_attributes = vec![guard.serial_number, guard.binding_number];
|
||||
|
||||
let signature = obtain_aggregate_signature(
|
||||
&guard.params,
|
||||
&public_attributes,
|
||||
&private_attributes,
|
||||
&guard.public_attributes,
|
||||
&guard.private_attributes,
|
||||
&parsed_urls,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use rand::rngs::OsRng;
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::Credential;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{hash_to_scalar, Parameters};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::bandwidth::{BandwidthVoucherAttributes, BANDWIDTH_VALUE, TOTAL_ATTRIBUTES};
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key};
|
||||
use crypto::asymmetric::{encryption, identity};
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::GatewayClient;
|
||||
use nymsphinx::acknowledgements::AckKey;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::preparer::MessagePreparer;
|
||||
use rand::rngs::OsRng;
|
||||
use received_processor::ReceivedMessagesProcessor;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use topology::{gateway, nym_topology_from_bonds, NymTopology};
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::Credential;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::{bandwidth::prepare_for_spending, obtain_aggregate_verification_key};
|
||||
|
||||
pub(crate) mod received_processor;
|
||||
|
||||
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
|
||||
@@ -114,31 +109,15 @@ impl NymClient {
|
||||
.await
|
||||
.expect("could not obtain aggregate verification key of validators");
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
|
||||
};
|
||||
|
||||
let bandwidth_credential = credentials::bandwidth::obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
validators,
|
||||
)
|
||||
.await
|
||||
.expect("could not obtain bandwidth credential");
|
||||
let bandwidth_credential =
|
||||
credentials::bandwidth::obtain_signature(identity_bytes, validators)
|
||||
.await
|
||||
.expect("could not obtain bandwidth credential");
|
||||
// the above would presumably be loaded from a file
|
||||
|
||||
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
|
||||
prepare_for_spending(
|
||||
identity_bytes,
|
||||
&bandwidth_credential,
|
||||
&bandwidth_credential_attributes,
|
||||
&verification_key,
|
||||
)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
prepare_for_spending(identity_bytes, &bandwidth_credential, &verification_key)
|
||||
.expect("could not prepare out bandwidth credential for spending")
|
||||
}
|
||||
|
||||
// Right now it's impossible to have async exported functions to take `&self` rather than self
|
||||
@@ -296,7 +275,7 @@ impl NymClient {
|
||||
let validator_client = validator_client::ApiClient::new(self.validator_server.clone());
|
||||
|
||||
let mixnodes = match validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => panic!("{:?}", err),
|
||||
Err(err) => panic!("{}", err),
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
|
||||
@@ -139,6 +139,11 @@ impl GatewayClient {
|
||||
self.gateway_identity
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn remaining_bandwidth(&self) -> i64 {
|
||||
self.bandwidth_remaining
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
|
||||
|
||||
@@ -8,4 +8,4 @@ description = "Crutch library until there is proper SerDe support for coconut st
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
getset = "0.1.1"
|
||||
|
||||
nymcoconut = {path = "../nymcoconut" }
|
||||
coconut-rs = { git = "https://github.com/nymtech/coconut.git", branch = "0.5.0" }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use getset::{CopyGetters, Getters};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use nymcoconut::*;
|
||||
pub use coconut_rs::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Getters, CopyGetters, Clone)]
|
||||
pub struct Credential {
|
||||
@@ -42,7 +42,7 @@ impl Credential {
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<Attribute>>();
|
||||
nymcoconut::verify_credential(¶ms, verification_key, &self.theta, &public_attributes)
|
||||
coconut_rs::verify_credential(¶ms, verification_key, &self.theta, &public_attributes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ pub struct BlindSignRequestBody {
|
||||
#[getset(get = "pub")]
|
||||
blind_sign_request: BlindSignRequest,
|
||||
#[getset(get = "pub")]
|
||||
public_key: nymcoconut::PublicKey,
|
||||
public_key: coconut_rs::PublicKey,
|
||||
public_attributes: Vec<String>,
|
||||
#[getset(get = "pub")]
|
||||
total_params: u32,
|
||||
@@ -92,13 +92,13 @@ pub struct BlindSignRequestBody {
|
||||
|
||||
impl BlindSignRequestBody {
|
||||
pub fn new(
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_key: &nymcoconut::PublicKey,
|
||||
blind_sign_request: BlindSignRequest,
|
||||
public_key: &coconut_rs::PublicKey,
|
||||
public_attributes: &[Attribute],
|
||||
total_params: u32,
|
||||
) -> BlindSignRequestBody {
|
||||
BlindSignRequestBody {
|
||||
blind_sign_request: blind_sign_request.clone(),
|
||||
blind_sign_request,
|
||||
public_key: public_key.clone(),
|
||||
public_attributes: public_attributes
|
||||
.iter()
|
||||
|
||||
@@ -8,74 +8,40 @@
|
||||
|
||||
use url::Url;
|
||||
|
||||
use coconut_interface::{
|
||||
Credential, Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
|
||||
};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::utils::{obtain_aggregate_signature, prepare_credential_for_spending};
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters, Signature, VerificationKey};
|
||||
|
||||
pub const BANDWIDTH_VALUE: u64 = 10 * 1024 * 1024 * 1024; // 10 GB
|
||||
const BANDWIDTH_VALUE: u64 = 10 * 1024 * 1024 * 1024; // 10 GB
|
||||
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 2;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 2;
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 1;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 1;
|
||||
pub const TOTAL_ATTRIBUTES: u32 = PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES;
|
||||
|
||||
pub const SERIAL_NUMBER_LEN: usize = 47;
|
||||
pub const BINDING_NUMBER_LEN: usize = 47;
|
||||
pub const VOUCHER_INFO_LEN: usize = 47;
|
||||
|
||||
pub struct BandwidthVoucherAttributes {
|
||||
// a random secret value generated by the client used for double-spending detection
|
||||
pub serial_number: PrivateAttribute,
|
||||
// a random secret value generated by the client used to bind multiple credentials together
|
||||
pub binding_number: PrivateAttribute,
|
||||
// the value (e.g., bandwidth) encoded in this voucher
|
||||
pub voucher_value: PublicAttribute,
|
||||
// a field with public information, e.g., type of voucher, epoch etc.
|
||||
pub voucher_info: PublicAttribute,
|
||||
}
|
||||
|
||||
impl BandwidthVoucherAttributes {
|
||||
pub fn get_public_attributes(&self) -> Vec<PublicAttribute> {
|
||||
vec![self.voucher_value, self.voucher_info]
|
||||
}
|
||||
|
||||
pub fn get_private_attributes(&self) -> Vec<PrivateAttribute> {
|
||||
vec![self.serial_number, self.binding_number]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this definitely has to be moved somewhere else. It's just a temporary solution
|
||||
pub async fn obtain_signature(
|
||||
params: &Parameters,
|
||||
attributes: &BandwidthVoucherAttributes,
|
||||
validators: &[Url],
|
||||
) -> Result<Signature, Error> {
|
||||
let public_attributes = attributes.get_public_attributes();
|
||||
let private_attributes = attributes.get_private_attributes();
|
||||
pub async fn obtain_signature(raw_identity: &[u8], validators: &[Url]) -> Result<Signature, Error> {
|
||||
let public_attributes = vec![hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes())];
|
||||
let private_attributes = vec![hash_to_scalar(raw_identity)];
|
||||
|
||||
obtain_aggregate_signature(params, &public_attributes, &private_attributes, validators).await
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
|
||||
|
||||
obtain_aggregate_signature(¶ms, &public_attributes, &private_attributes, validators).await
|
||||
}
|
||||
|
||||
pub fn prepare_for_spending(
|
||||
raw_identity: &[u8],
|
||||
signature: &Signature,
|
||||
attributes: &BandwidthVoucherAttributes,
|
||||
verification_key: &VerificationKey,
|
||||
) -> Result<Credential, Error> {
|
||||
let public_attributes = vec![
|
||||
raw_identity.to_vec(),
|
||||
BANDWIDTH_VALUE.to_be_bytes().to_vec(),
|
||||
];
|
||||
let public_attributes = vec![BANDWIDTH_VALUE.to_be_bytes().to_vec()];
|
||||
let private_attributes = vec![raw_identity.to_vec()];
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
|
||||
|
||||
prepare_credential_for_spending(
|
||||
¶ms,
|
||||
public_attributes,
|
||||
attributes.serial_number,
|
||||
attributes.binding_number,
|
||||
private_attributes,
|
||||
signature,
|
||||
verification_key,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,4 @@ pub mod bandwidth;
|
||||
pub mod error;
|
||||
mod utils;
|
||||
|
||||
pub use utils::{
|
||||
blind_sign_partial_credential, create_aggregate_verification_key, get_verification_keys,
|
||||
obtain_aggregate_signature, obtain_aggregate_verification_key,
|
||||
};
|
||||
pub use utils::{obtain_aggregate_signature, obtain_aggregate_verification_key};
|
||||
|
||||
+20
-127
@@ -1,17 +1,14 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use coconut_interface::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, prepare_blind_sign,
|
||||
prove_bandwidth_credential, Attribute, BlindSignRequest, BlindSignRequestBody,
|
||||
BlindedSignature, Credential, ElGamalKeyPair, Parameters, Signature, SignatureShare,
|
||||
VerificationKey,
|
||||
aggregate_signature_shares, aggregate_verification_keys, hash_to_scalar, prepare_blind_sign,
|
||||
prove_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
|
||||
SignatureShare, VerificationKey,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::bandwidth::PRIVATE_ATTRIBUTES;
|
||||
use crate::error::Error;
|
||||
|
||||
/// Contacts all provided validators and then aggregate their verification keys.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -35,43 +32,6 @@ use crate::error::Error;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_verification_keys(validators: &[Url]) -> Result<Vec<VerificationKey>, Error> {
|
||||
if validators.is_empty() {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
|
||||
let mut shares = Vec::with_capacity(validators.len());
|
||||
|
||||
let mut client = validator_client::ApiClient::new(validators[0].clone());
|
||||
let response = client.get_coconut_verification_key().await?;
|
||||
|
||||
shares.push(response.key);
|
||||
|
||||
for validator_url in validators.iter().enumerate().skip(1) {
|
||||
client.change_validator_api(validator_url.1.clone());
|
||||
let response = client.get_coconut_verification_key().await?;
|
||||
shares.push(response.key);
|
||||
}
|
||||
|
||||
Ok(shares)
|
||||
}
|
||||
|
||||
pub fn create_aggregate_verification_key(
|
||||
verification_keys: &Vec<VerificationKey>,
|
||||
) -> Result<VerificationKey, Error> {
|
||||
if verification_keys.is_empty() {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
|
||||
// creates a vec of [1, 2, .. n] where n is length of verification_keys, e.g. [1,2,3] for 3 keys
|
||||
let indices: Vec<u64> = (1u64..(verification_keys.len() + 1) as u64).collect();
|
||||
|
||||
Ok(aggregate_verification_keys(
|
||||
&verification_keys,
|
||||
Some(&indices.as_slice()),
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn obtain_aggregate_verification_key(
|
||||
validators: &[Url],
|
||||
) -> Result<VerificationKey, Error> {
|
||||
@@ -98,45 +58,22 @@ pub async fn obtain_aggregate_verification_key(
|
||||
Ok(aggregate_verification_keys(&shares, Some(&indices))?)
|
||||
}
|
||||
|
||||
pub async fn blind_sign_partial_credential(
|
||||
validator_url: &Url,
|
||||
elgamal_keypair: &ElGamalKeyPair,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
total_params: u32,
|
||||
) -> Result<BlindedSignature, Error> {
|
||||
let client = validator_client::ApiClient::new(validator_url.clone());
|
||||
|
||||
let blind_sign_request_body = BlindSignRequestBody::new(
|
||||
&blind_sign_request,
|
||||
elgamal_keypair.public_key(),
|
||||
public_attributes,
|
||||
total_params,
|
||||
);
|
||||
|
||||
Ok(client
|
||||
.blind_sign(&blind_sign_request_body)
|
||||
.await?
|
||||
.blinded_signature)
|
||||
}
|
||||
|
||||
async fn obtain_partial_credential(
|
||||
params: &Parameters,
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[Attribute],
|
||||
client: &validator_client::ApiClient,
|
||||
validator_vk: &VerificationKey,
|
||||
) -> Result<Signature, Error> {
|
||||
let elgamal_keypair = coconut_interface::elgamal_keygen(params);
|
||||
let blind_sign_request = prepare_blind_sign(
|
||||
params,
|
||||
&elgamal_keypair,
|
||||
elgamal_keypair.public_key(),
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
)?;
|
||||
|
||||
let blind_sign_request_body = BlindSignRequestBody::new(
|
||||
&blind_sign_request,
|
||||
blind_sign_request,
|
||||
elgamal_keypair.public_key(),
|
||||
public_attributes,
|
||||
(public_attributes.len() + private_attributes.len()) as u32,
|
||||
@@ -146,16 +83,7 @@ async fn obtain_partial_credential(
|
||||
.blind_sign(&blind_sign_request_body)
|
||||
.await?
|
||||
.blinded_signature;
|
||||
Ok(blinded_signature
|
||||
.unblind(
|
||||
params,
|
||||
elgamal_keypair.private_key(),
|
||||
validator_vk,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
)
|
||||
.unwrap())
|
||||
Ok(blinded_signature.unblind(elgamal_keypair.private_key()))
|
||||
}
|
||||
|
||||
pub async fn obtain_aggregate_signature(
|
||||
@@ -169,75 +97,40 @@ pub async fn obtain_aggregate_signature(
|
||||
}
|
||||
|
||||
let mut shares = Vec::with_capacity(validators.len());
|
||||
let mut validators_partial_vks: Vec<VerificationKey> = Vec::with_capacity(validators.len());
|
||||
|
||||
let mut client = validator_client::ApiClient::new(validators[0].clone());
|
||||
let validator_partial_vk = client.get_coconut_verification_key().await?;
|
||||
validators_partial_vks.push(validator_partial_vk.key.clone());
|
||||
|
||||
let first = obtain_partial_credential(
|
||||
params,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
&client,
|
||||
&validator_partial_vk.key,
|
||||
)
|
||||
.await?;
|
||||
let first =
|
||||
obtain_partial_credential(params, public_attributes, private_attributes, &client).await?;
|
||||
shares.push(SignatureShare::new(first, 0));
|
||||
|
||||
for (id, validator_url) in validators.iter().enumerate().skip(1) {
|
||||
client.change_validator_api(validator_url.clone());
|
||||
let validator_partial_vk = client.get_coconut_verification_key().await?;
|
||||
validators_partial_vks.push(validator_partial_vk.key.clone());
|
||||
let signature = obtain_partial_credential(
|
||||
params,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
&client,
|
||||
&validator_partial_vk.key,
|
||||
)
|
||||
.await?;
|
||||
let signature =
|
||||
obtain_partial_credential(params, public_attributes, private_attributes, &client)
|
||||
.await?;
|
||||
let share = SignatureShare::new(signature, id as u64);
|
||||
shares.push(share)
|
||||
}
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(private_attributes);
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
|
||||
let mut indices: Vec<u64> = Vec::with_capacity(validators_partial_vks.len());
|
||||
for i in 1..validators_partial_vks.len() {
|
||||
indices.push(i as u64);
|
||||
}
|
||||
let verification_key =
|
||||
aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref())).unwrap();
|
||||
Ok(aggregate_signature_shares(
|
||||
params,
|
||||
&verification_key,
|
||||
&attributes,
|
||||
&shares,
|
||||
)?)
|
||||
Ok(aggregate_signature_shares(&shares)?)
|
||||
}
|
||||
|
||||
// TODO: better type flow
|
||||
pub fn prepare_credential_for_spending(
|
||||
params: &Parameters,
|
||||
public_attributes: Vec<Vec<u8>>,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
private_attributes: Vec<Vec<u8>>,
|
||||
signature: &Signature,
|
||||
verification_key: &VerificationKey,
|
||||
) -> Result<Credential, Error> {
|
||||
let theta = prove_bandwidth_credential(
|
||||
params,
|
||||
verification_key,
|
||||
signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)?;
|
||||
let private_attributes = private_attributes
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<Attribute>>();
|
||||
let theta = prove_credential(params, verification_key, signature, &private_attributes)?;
|
||||
|
||||
Ok(Credential::new(
|
||||
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
|
||||
(public_attributes.len() + private_attributes.len()) as u32,
|
||||
theta,
|
||||
public_attributes,
|
||||
signature,
|
||||
|
||||
@@ -23,7 +23,17 @@ pub struct MixNode {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize_repr, PartialEq, PartialOrd, Deserialize_repr, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize_repr,
|
||||
Deserialize_repr,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Layer {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
[package]
|
||||
name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>", "Ania Piotrowska <ania@nymtech.net>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
itertools = "0.10"
|
||||
digest = "0.9"
|
||||
rand = "0.8"
|
||||
thiserror = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
bs58 = "0.4.0"
|
||||
sha2 = "0.9"
|
||||
|
||||
[dependencies.ff]
|
||||
version = "0.10"
|
||||
default-features = false
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.10"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version="0.3", features=["html_reports"] }
|
||||
doc-comment = "0.3"
|
||||
|
||||
[dev-dependencies.bincode]
|
||||
version = "1"
|
||||
|
||||
#[[bench]]
|
||||
#name = "benchmarks"
|
||||
#harness = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version="0.2", features=["js"] }
|
||||
@@ -1,333 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::ops::{Deref, Mul};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use group::Curve;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar};
|
||||
|
||||
/// Type alias for the ephemeral key generated during ElGamal encryption
|
||||
pub type EphemeralKey = Scalar;
|
||||
|
||||
/// Two G1 points representing ElGamal ciphertext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
impl TryFrom<&[u8]> for Ciphertext {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Ciphertext must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
|
||||
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
|
||||
|
||||
let c1 = try_deserialize_g1_projective(
|
||||
c1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()),
|
||||
)?;
|
||||
let c2 = try_deserialize_g1_projective(
|
||||
c2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Ciphertext(c1, c2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ciphertext {
|
||||
pub fn c1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn c2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
Ciphertext::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct PrivateKey(pub(crate) Scalar);
|
||||
|
||||
impl PrivateKey {
|
||||
/// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve
|
||||
/// that represents original h^m.
|
||||
pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective {
|
||||
let (c1, c2) = &(ciphertext.0, ciphertext.1);
|
||||
|
||||
// (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d
|
||||
c2 - c1 * self.0
|
||||
}
|
||||
|
||||
pub fn public_key(&self, params: &Parameters) -> PublicKey {
|
||||
PublicKey(params.gen1() * self.0)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 32]) -> Result<PrivateKey> {
|
||||
try_deserialize_scalar(
|
||||
bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize ElGamal private key - it was not in the canonical form"
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
.map(PrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PrivateKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
PrivateKey::from_bytes(slice.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PrivateKey {}
|
||||
|
||||
// TODO: perhaps be more explicit and apart from gamma also store generator and group order?
|
||||
/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct PublicKey(G1Projective);
|
||||
|
||||
impl PublicKey {
|
||||
/// Encrypt encrypts the given message in the form of h^m,
|
||||
/// where h is a point on the G1 curve using the given public key.
|
||||
/// The random k is returned alongside the encryption
|
||||
/// as it is required by the Coconut Scheme to create proofs of knowledge.
|
||||
pub fn encrypt(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
h: &G1Projective,
|
||||
msg: &Scalar,
|
||||
) -> (Ciphertext, EphemeralKey) {
|
||||
let k = params.random_scalar();
|
||||
// c1 = g1^k
|
||||
let c1 = params.gen1() * k;
|
||||
// c2 = gamma^k * h^m
|
||||
let c2 = self.0 * k + h * msg;
|
||||
|
||||
(Ciphertext(c1, c2), k)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
self.to_byte_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
|
||||
Ok(PublicKey::try_from(bytes.to_vec().as_slice()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PublicKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.0.to_affine().to_compressed().into()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Ok(PublicKey::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for PublicKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(slice: &[u8]) -> Result<PublicKey> {
|
||||
try_deserialize_g1_projective(
|
||||
slice.try_into().unwrap(),
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed ElGamal public key".to_string(),
|
||||
),
|
||||
)
|
||||
.map(PublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PublicKey {}
|
||||
|
||||
impl Deref for PublicKey {
|
||||
type Target = G1Projective;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Mul<&'b Scalar> for &'a PublicKey {
|
||||
type Output = G1Projective;
|
||||
|
||||
fn mul(self, rhs: &'b Scalar) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
/// A convenient wrapper for both keys of the ElGamal keypair
|
||||
pub struct ElGamalKeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl ElGamalKeyPair {
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters]
|
||||
pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
|
||||
let private_key = params.random_scalar();
|
||||
let gamma = params.gen1() * private_key;
|
||||
|
||||
ElGamalKeyPair {
|
||||
private_key: PrivateKey(private_key),
|
||||
public_key: PublicKey(gamma),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keygen() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let expected = params.gen1() * keypair.private_key.0;
|
||||
let gamma = keypair.public_key.0;
|
||||
assert_eq!(
|
||||
expected, gamma,
|
||||
"Public key, gamma, should be equal to g1^d, where d is the private key"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
|
||||
let expected_c1 = params.gen1() * ephemeral_key;
|
||||
assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k");
|
||||
|
||||
let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m;
|
||||
assert_eq!(
|
||||
expected_c2, ciphertext.1,
|
||||
"c2 should be equal to gamma^k * h^m"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, _) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
let dec = keypair.private_key.decrypt(&ciphertext);
|
||||
|
||||
let expected = h * m;
|
||||
assert_eq!(
|
||||
expected, dec,
|
||||
"after ElGamal decryption, original h^m should be obtained"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let private_key = PrivateKey(params.random_scalar());
|
||||
let bytes = private_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal scalar's bytes
|
||||
assert_eq!(private_key.0.to_bytes(), bytes);
|
||||
assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let public_key = PublicKey(params.gen1() * r);
|
||||
let bytes = public_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes
|
||||
assert_eq!(public_key.0.to_affine().to_compressed(), bytes);
|
||||
assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ciphertext_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = ciphertext.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
ciphertext.0.to_affine().to_compressed(),
|
||||
ciphertext.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// A `Result` alias where the `Err` case is `coconut_rs::Error`.
|
||||
pub type Result<T> = std::result::Result<T, CoconutError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CoconutError {
|
||||
#[error("Setup error: {0}")]
|
||||
Setup(String),
|
||||
|
||||
#[error("encountered error during keygen")]
|
||||
Keygen,
|
||||
|
||||
#[error("Issuance related error: {0}")]
|
||||
Issuance(String),
|
||||
|
||||
#[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)]
|
||||
IssuanceMaxAttributes { max: usize, requested: usize },
|
||||
|
||||
#[error("Interpolation error: {0}")]
|
||||
Interpolation(String),
|
||||
|
||||
#[error("Aggregation error: {0}")]
|
||||
Aggregation(String),
|
||||
|
||||
#[error("Unblind error: {0}")]
|
||||
Unblind(String),
|
||||
|
||||
#[error("Verification error: {0}")]
|
||||
Verification(String),
|
||||
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
#[error(
|
||||
"Deserailization error, expected at least {} bytes, got {}",
|
||||
min,
|
||||
actual
|
||||
)]
|
||||
DeserializationMinLength { min: usize, actual: usize },
|
||||
|
||||
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")]
|
||||
DeserializationInvalidLength {
|
||||
actual: usize,
|
||||
target: usize,
|
||||
modulus_target: usize,
|
||||
modulus: usize,
|
||||
object: String,
|
||||
},
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
use crate::{BlindSignRequest, BlindedSignature, Bytable, Theta};
|
||||
|
||||
macro_rules! impl_clone {
|
||||
($struct:ident) => {
|
||||
impl Clone for $struct {
|
||||
fn clone(&self) -> Self {
|
||||
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_clone!(BlindSignRequest);
|
||||
impl_clone!(BlindedSignature);
|
||||
impl_clone!(Theta);
|
||||
@@ -1,2 +0,0 @@
|
||||
mod clone;
|
||||
mod serde;
|
||||
@@ -1,56 +0,0 @@
|
||||
use crate::elgamal::PrivateKey;
|
||||
use crate::scheme::SecretKey;
|
||||
use crate::{
|
||||
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, Theta, VerificationKey,
|
||||
};
|
||||
use serde::de::Unexpected;
|
||||
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! impl_serde {
|
||||
($struct:ident, $visitor:ident) => {
|
||||
pub struct $visitor {}
|
||||
|
||||
impl Serialize for $struct {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_bs58())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for $visitor {
|
||||
type Value = $struct;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "A base58 encoded struct")
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||
match $struct::try_from_bs58(s) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $struct {
|
||||
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str($visitor {})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_serde!(SecretKey, V1);
|
||||
impl_serde!(VerificationKey, V2);
|
||||
impl_serde!(PublicKey, V3);
|
||||
impl_serde!(PrivateKey, V4);
|
||||
impl_serde!(BlindSignRequest, V5);
|
||||
impl_serde!(BlindedSignature, V6);
|
||||
impl_serde!(Signature, V7);
|
||||
impl_serde!(Theta, V8);
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::Scalar;
|
||||
|
||||
pub use elgamal::elgamal_keygen;
|
||||
pub use elgamal::ElGamalKeyPair;
|
||||
pub use elgamal::PublicKey;
|
||||
pub use error::CoconutError;
|
||||
pub use scheme::aggregation::aggregate_signature_shares;
|
||||
pub use scheme::aggregation::aggregate_verification_keys;
|
||||
pub use scheme::issuance::blind_sign;
|
||||
pub use scheme::issuance::prepare_blind_sign;
|
||||
pub use scheme::issuance::BlindSignRequest;
|
||||
pub use scheme::keygen::ttp_keygen;
|
||||
pub use scheme::keygen::KeyPair;
|
||||
pub use scheme::keygen::VerificationKey;
|
||||
pub use scheme::setup::setup;
|
||||
pub use scheme::setup::Parameters;
|
||||
pub use scheme::verification::prove_bandwidth_credential;
|
||||
pub use scheme::verification::verify_credential;
|
||||
pub use scheme::verification::Theta;
|
||||
pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
pub use scheme::SignatureShare;
|
||||
pub use traits::Base58;
|
||||
pub use utils::hash_to_scalar;
|
||||
|
||||
use crate::traits::Bytable;
|
||||
|
||||
pub mod elgamal;
|
||||
mod error;
|
||||
mod impls;
|
||||
pub mod proofs;
|
||||
pub mod scheme;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod traits;
|
||||
mod utils;
|
||||
|
||||
pub type Attribute = Scalar;
|
||||
pub type PrivateAttribute = Attribute;
|
||||
pub type PublicAttribute = Attribute;
|
||||
|
||||
impl Bytable for Attribute {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
|
||||
Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Attribute {}
|
||||
@@ -1,836 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// TODO: look at https://crates.io/crates/merlin to perhaps use it instead?
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use digest::generic_array::typenum::Unsigned;
|
||||
use digest::Digest;
|
||||
use group::GroupEncoding;
|
||||
use itertools::izip;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::elgamal::Ciphertext;
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::utils::{hash_g1, try_deserialize_scalar, try_deserialize_scalar_vec};
|
||||
use crate::{elgamal, Attribute, ElGamalKeyPair};
|
||||
|
||||
// as per the reference python implementation
|
||||
type ChallengeDigest = Sha256;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ProofCmCs {
|
||||
challenge: Scalar,
|
||||
response_opening: Scalar,
|
||||
response_private_elgamal_key: Scalar,
|
||||
response_keys: Vec<Scalar>,
|
||||
response_attributes: Vec<Scalar>,
|
||||
}
|
||||
|
||||
// note: this is slightly different from the reference python implementation
|
||||
// as we omit the unnecessary string conversion. Instead we concatenate byte
|
||||
// representations together and hash that.
|
||||
// note2: G1 and G2 elements are using their compressed representations
|
||||
// and as per the bls12-381 library all elements are using big-endian form
|
||||
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
|
||||
fn compute_challenge<D, I, B>(iter: I) -> Scalar
|
||||
where
|
||||
D: Digest,
|
||||
I: Iterator<Item = B>,
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
let mut h = D::new();
|
||||
for point_representation in iter {
|
||||
h.update(point_representation);
|
||||
}
|
||||
let digest = h.finalize();
|
||||
|
||||
// TODO: I don't like the 0 padding here (though it's what we've been using before,
|
||||
// but we never had a security audit anyway...)
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
Scalar::from_bytes_wide(&bytes)
|
||||
}
|
||||
|
||||
fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
|
||||
witness - challenge * secret
|
||||
}
|
||||
|
||||
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
|
||||
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
|
||||
where
|
||||
S: Borrow<Scalar>,
|
||||
{
|
||||
debug_assert_eq!(witnesses.len(), secrets.len());
|
||||
|
||||
witnesses
|
||||
.iter()
|
||||
.zip(secrets.iter())
|
||||
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProofCmCs {
|
||||
/// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment
|
||||
/// using the Fiat-Shamir heuristic.
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
elgamal_keypair: &ElGamalKeyPair,
|
||||
ephemeral_keys: &[elgamal::EphemeralKey],
|
||||
commitment: &G1Projective,
|
||||
commitment_opening: &Scalar,
|
||||
private_attributes: &[Attribute],
|
||||
priv_attributes_ciphertexts: &[Ciphertext],
|
||||
) -> Self {
|
||||
// note: this is only called from `prepare_blind_sign` that already checks
|
||||
// whether private attributes are non-empty and whether we don't have too many
|
||||
// attributes in total to sign.
|
||||
// we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len()
|
||||
|
||||
// witness creation
|
||||
|
||||
let witness_commitment_opening = params.random_scalar();
|
||||
let witness_private_elgamal_key = params.random_scalar();
|
||||
let witness_keys = params.n_random_scalars(ephemeral_keys.len());
|
||||
let witness_attributes = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
// recompute h
|
||||
let h = hash_g1(commitment.to_bytes());
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let g1 = params.gen1();
|
||||
|
||||
// compute commitments
|
||||
let commitment_private_key_elgamal = g1 * witness_private_elgamal_key;
|
||||
|
||||
// Aw[i] = (wk[i] * g1)
|
||||
let commitment_keys1_bytes = witness_keys
|
||||
.iter()
|
||||
.map(|wk_i| g1 * wk_i)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bw[i] = (wm[i] * h) + (wk[i] * gamma)
|
||||
let commitment_keys2_bytes = witness_keys
|
||||
.iter()
|
||||
.zip(witness_attributes.iter())
|
||||
.map(|(wk_i, wm_i)| elgamal_keypair.public_key() * wk_i + h * wm_i)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// zkp commitment for the attributes commitment cm
|
||||
// Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i])
|
||||
let commitment_attributes = g1 * witness_commitment_opening
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(wm_i, hs_i)| hs_i * wm_i)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let ciphertexts_bytes = priv_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|c| c.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// compute challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
elgamal_keypair.public_key().to_bytes().as_ref(),
|
||||
))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
commitment_private_key_elgamal.to_bytes().as_ref(),
|
||||
))
|
||||
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
|
||||
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
|
||||
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
|
||||
);
|
||||
|
||||
// Responses
|
||||
let response_opening =
|
||||
produce_response(&witness_commitment_opening, &challenge, commitment_opening);
|
||||
let response_private_elgamal_key = produce_response(
|
||||
&witness_private_elgamal_key,
|
||||
&challenge,
|
||||
&elgamal_keypair.private_key().0,
|
||||
);
|
||||
let response_keys = produce_responses(&witness_keys, &challenge, ephemeral_keys);
|
||||
let response_attributes = produce_responses(
|
||||
&witness_attributes,
|
||||
&challenge,
|
||||
&private_attributes.iter().collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_private_elgamal_key,
|
||||
response_keys,
|
||||
response_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
pub_key: &elgamal::PublicKey,
|
||||
commitment: &G1Projective,
|
||||
attributes_ciphertexts: &[elgamal::Ciphertext],
|
||||
) -> bool {
|
||||
if self.response_keys.len() != attributes_ciphertexts.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// recompute h
|
||||
let h = hash_g1(commitment.to_bytes());
|
||||
let g1 = params.gen1();
|
||||
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// recompute witnesses commitments
|
||||
let commitment_private_key_elgamal =
|
||||
pub_key * &self.challenge + g1 * self.response_private_elgamal_key;
|
||||
|
||||
// Aw[i] = (c * c1[i]) + (rk[i] * g1)
|
||||
let commitment_keys1_bytes = attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c1())
|
||||
.zip(self.response_keys.iter())
|
||||
.map(|(c1, res_k)| c1 * self.challenge + g1 * res_k)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bw[i] = (c * c2[i]) + (rk[i] * gamma) + (rm[i] * h)
|
||||
let commitment_keys2_bytes = izip!(
|
||||
attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c2()),
|
||||
self.response_keys.iter(),
|
||||
self.response_attributes.iter()
|
||||
)
|
||||
.map(|(c2, res_key, res_attr)| c2 * self.challenge + pub_key * res_key + h * res_attr)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n])
|
||||
let commitment_attributes = commitment * self.challenge
|
||||
+ g1 * self.response_opening
|
||||
+ self
|
||||
.response_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(res_attr, hs)| hs * res_attr)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let ciphertexts_bytes = attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|c| c.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// re-compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(pub_key.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
commitment_private_key_elgamal.to_bytes().as_ref(),
|
||||
))
|
||||
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
|
||||
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
|
||||
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || rr || rk.len() || rk || rm.len() || rm
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let keys_len = self.response_keys.len() as u64;
|
||||
let attributes_len = self.response_attributes.len() as u64;
|
||||
|
||||
let mut bytes = Vec::with_capacity(16 + (keys_len + attributes_len + 3) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_opening.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_private_elgamal_key.to_bytes());
|
||||
bytes.extend_from_slice(&keys_len.to_le_bytes());
|
||||
|
||||
for rk in &self.response_keys {
|
||||
bytes.extend_from_slice(&rk.to_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&attributes_len.to_le_bytes());
|
||||
|
||||
for rm in &self.response_attributes {
|
||||
bytes.extend_from_slice(&rm.to_bytes());
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 4 + 16 || (bytes.len() - 16) % 32 != 0 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with bytes of invalid length".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
let response_private_elgamal_key_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
let response_opening = try_deserialize_scalar(
|
||||
&response_opening_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize the response to the random".to_string(),
|
||||
),
|
||||
)?;
|
||||
let response_private_elgamal_key = try_deserialize_scalar(
|
||||
&response_private_elgamal_key_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize the response to the private ElGamal key".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let rk_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
|
||||
idx += 8;
|
||||
if bytes[idx..].len() < rk_len as usize * 32 + 8 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let rk_end = idx + rk_len as usize * 32;
|
||||
let response_keys = try_deserialize_scalar_vec(
|
||||
rk_len,
|
||||
&bytes[idx..rk_end],
|
||||
CoconutError::Deserialization("Failed to deserialize keys response".to_string()),
|
||||
)?;
|
||||
|
||||
let rm_len = u64::from_le_bytes(bytes[rk_end..rk_end + 8].try_into().unwrap());
|
||||
let response_attributes = try_deserialize_scalar_vec(
|
||||
rm_len,
|
||||
&bytes[rk_end + 8..],
|
||||
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_private_elgamal_key,
|
||||
response_keys,
|
||||
response_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ProofKappa {
|
||||
challenge: Scalar,
|
||||
response_attributes: Vec<Scalar>,
|
||||
response_blinder: Scalar,
|
||||
}
|
||||
|
||||
impl ProofKappa {
|
||||
pub fn construct(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
blinding_factor: &Scalar,
|
||||
blinded_message: &G2Projective,
|
||||
private_attributes: &[Attribute],
|
||||
verifier_id: &[u8; 32],
|
||||
timestamp: &[u8; 32],
|
||||
) -> Self {
|
||||
// create the witnesses
|
||||
let witness_blinder = params.random_scalar();
|
||||
let witness_attributes = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// witnesses commitments
|
||||
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
|
||||
let commitment_kappa = params.gen2() * witness_blinder
|
||||
+ verification_key.alpha
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(wm_i, beta_i)| beta_i * wm_i)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(blinded_message.to_bytes().as_ref())) //kappa
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verifier_id.as_ref()))
|
||||
.chain(std::iter::once(timestamp.as_ref())),
|
||||
);
|
||||
|
||||
// responses
|
||||
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
|
||||
let response_attributes =
|
||||
produce_responses(&witness_attributes, &challenge, private_attributes);
|
||||
|
||||
ProofKappa {
|
||||
challenge,
|
||||
response_attributes,
|
||||
response_blinder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_attributes_len(&self) -> usize {
|
||||
self.response_attributes.len()
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
kappa: &G2Projective,
|
||||
verifier_id: &[u8; 32],
|
||||
timestamp: &[u8; 32],
|
||||
) -> bool {
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// re-compute witnesses commitments
|
||||
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
|
||||
let commitment_kappa = kappa * self.challenge
|
||||
+ params.gen2() * self.response_blinder
|
||||
+ verification_key.alpha * (Scalar::one() - self.challenge)
|
||||
+ self
|
||||
.response_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verifier_id.as_ref()))
|
||||
.chain(std::iter::once(timestamp.as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let attributes_len = self.response_attributes.len() as u64;
|
||||
let mut bytes = Vec::with_capacity(8 + (attributes_len + 2) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_blinder.to_bytes());
|
||||
|
||||
bytes.extend_from_slice(&attributes_len.to_le_bytes());
|
||||
|
||||
for rm in &self.response_attributes {
|
||||
bytes.extend_from_slice(&rm.to_bytes());
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with bytes of invalid length".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
let challenge_bytes = bytes[..32].try_into().unwrap();
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
let blinder_bytes = bytes[32..64].try_into().unwrap();
|
||||
let response_blinder = try_deserialize_scalar(
|
||||
&blinder_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
|
||||
)?;
|
||||
|
||||
let rm_len = u64::from_le_bytes(bytes[64..64 + 8].try_into().unwrap());
|
||||
let response_attributes = try_deserialize_scalar_vec(
|
||||
rm_len,
|
||||
&bytes[64 + 8..],
|
||||
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofKappa {
|
||||
challenge,
|
||||
response_attributes,
|
||||
response_blinder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ProofKappaNu {
|
||||
// c
|
||||
challenge: Scalar,
|
||||
|
||||
// responses
|
||||
response_serial_number: Scalar,
|
||||
response_binding_number: Scalar,
|
||||
response_blinder: Scalar,
|
||||
}
|
||||
|
||||
impl ProofKappaNu {
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
blinding_factor: &Scalar,
|
||||
blinded_message: &G2Projective,
|
||||
blinded_serial_number: &G2Projective,
|
||||
) -> Self {
|
||||
// create the witnesses
|
||||
let witness_blinder = params.random_scalar();
|
||||
let witness_serial_number = params.random_scalar();
|
||||
let witness_binding_number = params.random_scalar();
|
||||
let witness_attributes = vec![witness_serial_number, witness_binding_number];
|
||||
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// witnesses commitments
|
||||
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
|
||||
let commitment_kappa = params.gen2() * witness_blinder
|
||||
+ verification_key.alpha
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(wm_i, beta_i)| beta_i * wm_i)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = params.gen2() * witness_serial_number;
|
||||
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(blinded_message.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(blinded_serial_number.to_bytes().as_ref())) //kappa
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
// responses
|
||||
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
|
||||
let response_serial_number =
|
||||
produce_response(&witness_serial_number, &challenge, serial_number);
|
||||
let response_binding_number =
|
||||
produce_response(&witness_binding_number, &challenge, binding_number);
|
||||
|
||||
ProofKappaNu {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn private_attributes_len(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
kappa: &G2Projective,
|
||||
zeta: &G2Projective,
|
||||
) -> bool {
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let response_attributes = vec![self.response_serial_number, self.response_binding_number];
|
||||
// re-compute witnesses commitments
|
||||
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
|
||||
let commitment_kappa = kappa * self.challenge
|
||||
+ params.gen2() * self.response_blinder
|
||||
+ verification_key.alpha * (Scalar::one() - self.challenge)
|
||||
+ response_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number;
|
||||
|
||||
// compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zeta.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || rm.len() || rm || rt
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
//let attributes_len = self.response_attributes.len() as u64;
|
||||
let attributes_len = 2;
|
||||
let mut bytes = Vec::with_capacity((attributes_len + 1) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_serial_number.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_binding_number.to_bytes());
|
||||
|
||||
bytes.extend_from_slice(&self.response_blinder.to_bytes());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 3 || (bytes.len()) % 32 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len(),
|
||||
modulus: 32,
|
||||
object: "kappa and zeta".to_string(),
|
||||
target: 32 * 3 + 8,
|
||||
});
|
||||
}
|
||||
|
||||
let challenge_bytes = bytes[..32].try_into().unwrap();
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
// let rm_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
||||
if bytes[32..].len() != (2 + 1) as usize * 32 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize proof of kappa and zeta with insufficient number of bytes provided, expected {} got {}.", (2 + 1) as usize * 32, bytes[32..].len())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
|
||||
let response_serial_number = try_deserialize_scalar(
|
||||
serial_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
|
||||
)?;
|
||||
|
||||
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
|
||||
let response_binding_number = try_deserialize_scalar(
|
||||
binding_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
|
||||
)?;
|
||||
|
||||
let blinder_bytes = bytes[96..].try_into().unwrap();
|
||||
let response_blinder = try_deserialize_scalar(
|
||||
&blinder_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofKappaNu {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// proof builder:
|
||||
// - commitment
|
||||
// - challenge
|
||||
// - responses
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use group::Group;
|
||||
use rand::thread_rng;
|
||||
|
||||
use crate::scheme::issuance::{compute_attribute_encryption, compute_commitment_hash};
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
use crate::scheme::verification::{compute_kappa, compute_zeta};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn proof_cm_cs_bytes_roundtrip() {
|
||||
let mut rng = thread_rng();
|
||||
let mut params = setup(1).unwrap();
|
||||
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let cm = G1Projective::random(&mut rng);
|
||||
let r = params.random_scalar();
|
||||
|
||||
let commitment_hash = compute_commitment_hash(cm);
|
||||
let (attributes_ciphertexts, _): (Vec<_>, Vec<_>) = compute_attribute_encryption(
|
||||
¶ms,
|
||||
private_attributes.as_ref(),
|
||||
elgamal_keypair.public_key(),
|
||||
commitment_hash,
|
||||
);
|
||||
let ephemeral_keys = params.n_random_scalars(1);
|
||||
|
||||
// 0 public 1 private
|
||||
let pi_s = ProofCmCs::construct(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&cm,
|
||||
&r,
|
||||
&private_attributes,
|
||||
&*attributes_ciphertexts,
|
||||
);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
|
||||
// 2 private
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let ephemeral_keys = params.n_random_scalars(2);
|
||||
|
||||
let pi_s = ProofCmCs::construct(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&cm,
|
||||
&r,
|
||||
&private_attributes,
|
||||
&*attributes_ciphertexts,
|
||||
);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_kappa_nu_bytes_roundtrip() {
|
||||
let mut params = setup(1).unwrap();
|
||||
|
||||
let keypair = keygen(&mut params);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let r = params.random_scalar();
|
||||
let kappa = compute_kappa(¶ms, &keypair.verification_key(), &private_attributes, r);
|
||||
let zeta = compute_zeta(¶ms, serial_number);
|
||||
|
||||
// 0 public 2 private
|
||||
let pi_v = ProofKappaNu::construct(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let bytes = pi_v.to_bytes();
|
||||
assert_eq!(ProofKappaNu::from_bytes(&bytes).unwrap(), pi_v);
|
||||
|
||||
// 2 public 2 private
|
||||
let mut params = setup(4).unwrap();
|
||||
let keypair = keygen(&mut params);
|
||||
|
||||
let pi_v = ProofKappaNu::construct(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let bytes = pi_v.to_bytes();
|
||||
assert_eq!(ProofKappaNu::from_bytes(&bytes).unwrap(), pi_v);
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
|
||||
use bls12_381::{G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
|
||||
use crate::utils::perform_lagrangian_interpolation_at_origin;
|
||||
use crate::{Attribute, Parameters};
|
||||
|
||||
pub(crate) trait Aggregatable: Sized {
|
||||
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
|
||||
|
||||
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
|
||||
// if aggregation is a threshold one, all indices should be unique
|
||||
indices.iter().unique_by(|&index| index).count() == indices.len()
|
||||
}
|
||||
}
|
||||
|
||||
// includes `VerificationKey`
|
||||
impl<T> Aggregatable for T
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> T: Sum<&'a T>,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
|
||||
if aggregatable.is_empty() {
|
||||
return Err(CoconutError::Aggregation("Empty set of values".to_string()));
|
||||
}
|
||||
|
||||
if let Some(indices) = indices {
|
||||
if !Self::check_unique_indices(indices) {
|
||||
return Err(CoconutError::Aggregation("Non-unique indices".to_string()));
|
||||
}
|
||||
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
|
||||
} else {
|
||||
// non-threshold
|
||||
Ok(aggregatable.iter().sum())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Aggregatable for PartialSignature {
|
||||
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
|
||||
let h = sigs
|
||||
.get(0)
|
||||
.ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))?
|
||||
.sig1();
|
||||
|
||||
// TODO: is it possible to avoid this allocation?
|
||||
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
|
||||
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
|
||||
|
||||
Ok(Signature(*h, aggr_sigma))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures all provided verification keys were generated to verify the same number of attributes.
|
||||
fn check_same_key_size(keys: &[VerificationKey]) -> bool {
|
||||
keys.iter().map(|vk| vk.beta.len()).all_equal()
|
||||
}
|
||||
|
||||
pub fn aggregate_verification_keys(
|
||||
keys: &[VerificationKey],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<VerificationKey> {
|
||||
if !check_same_key_size(keys) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification keys are of different sizes".to_string(),
|
||||
));
|
||||
}
|
||||
Aggregatable::aggregate(keys, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
// aggregate the signature
|
||||
|
||||
let signature = match Aggregatable::aggregate(signatures, indices) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
// Verify the signature
|
||||
let alpha = verification_key.alpha;
|
||||
|
||||
let tmp = attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
if !check_bilinear_pairing(
|
||||
&signature.0.to_affine(),
|
||||
&G2Prepared::from((alpha + tmp).to_affine()),
|
||||
&signature.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification of the aggregated signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
shares: &[SignatureShare],
|
||||
) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures(
|
||||
params,
|
||||
verification_key,
|
||||
attributes,
|
||||
&signatures,
|
||||
Some(&indices),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bls12_381::G1Projective;
|
||||
use group::Group;
|
||||
|
||||
use crate::scheme::issuance::sign;
|
||||
use crate::scheme::keygen::ttp_keygen;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::verify;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_works_for_any_subset_of_keys() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
|
||||
assert_eq!(aggr_vk1, aggr_vk2);
|
||||
|
||||
// TODO: should those two actually work or not?
|
||||
// aggregating threshold+1
|
||||
let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_vk1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_all, aggr_vk1);
|
||||
|
||||
// not taking enough points (threshold was 3)
|
||||
let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_vk1);
|
||||
|
||||
// taking wrong index
|
||||
let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap();
|
||||
assert_ne!(aggr_vk1, aggr_bad);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_empty_set_of_keys() {
|
||||
let keys: Vec<VerificationKey> = vec![];
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let keys = vec![VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[])).is_err());
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_keys_of_different_size() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_works_for_any_subset_of_signatures() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
|
||||
let sigs = sks
|
||||
.iter()
|
||||
.map(|sk| sign(&mut params, sk, &attributes).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// aggregating (any) threshold works
|
||||
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_sig1 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[..3],
|
||||
Some(&[1, 2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
let aggr_sig2 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_sig2);
|
||||
|
||||
// verify credential for good measure
|
||||
assert!(verify(¶ms, &aggr_vk_1, &attributes, &aggr_sig1));
|
||||
assert!(verify(¶ms, &aggr_vk_2, &attributes, &aggr_sig2));
|
||||
|
||||
// aggregating threshold+1 works
|
||||
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
let aggr_more = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_more,
|
||||
&attributes,
|
||||
&sigs[1..],
|
||||
Some(&[2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
let aggr_all = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&sigs,
|
||||
Some(&[1, 2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_all, aggr_sig1);
|
||||
|
||||
// not taking enough points (threshold was 3) should fail
|
||||
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_not_enough = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_not_enough,
|
||||
&attributes,
|
||||
&sigs[..2],
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_sig1);
|
||||
|
||||
// taking wrong index should fail
|
||||
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_bad,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[42, 123, 100]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
fn random_signature() -> Signature {
|
||||
let mut rng = rand::thread_rng();
|
||||
Signature(
|
||||
G1Projective::random(&mut rng),
|
||||
G1Projective::random(&mut rng),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
|
||||
let signatures: Vec<Signature> = vec![];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, None).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let signatures = vec![random_signature()];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, Some(&[]))
|
||||
.is_err()
|
||||
);
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let signatures = vec![random_signature(), random_signature()];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 1]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// TODO: test for aggregating non-threshold keys
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Affine, G1Projective, Scalar};
|
||||
use group::{Curve, GroupEncoding};
|
||||
|
||||
use crate::elgamal::{Ciphertext, EphemeralKey};
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofCmCs;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::BlindedSignature;
|
||||
use crate::scheme::SecretKey;
|
||||
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
|
||||
#[cfg(test)]
|
||||
use crate::Signature;
|
||||
use crate::{elgamal, Attribute, ElGamalKeyPair};
|
||||
// TODO: possibly completely remove those two functions.
|
||||
// They only exist to have a simpler and smaller code snippets to test
|
||||
// basic functionalities.
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{hash_g1, try_deserialize_g1_projective};
|
||||
|
||||
// TODO NAMING: double check this one
|
||||
// Lambda
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct BlindSignRequest {
|
||||
// cm
|
||||
commitment: G1Projective,
|
||||
// h
|
||||
commitment_hash: G1Projective,
|
||||
// c
|
||||
private_attributes_ciphertexts: Vec<elgamal::Ciphertext>,
|
||||
// pi_s
|
||||
pi_s: ProofCmCs,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
if bytes.len() < 48 + 48 + 8 + 96 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: 48 + 48 + 8 + 9,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut j = 0;
|
||||
let commitment_bytes_len = 48;
|
||||
let commitment_hash_bytes_len = 48;
|
||||
|
||||
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
|
||||
let commitment = try_deserialize_g1_projective(
|
||||
&cm_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_bytes_len;
|
||||
|
||||
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
|
||||
let commitment_hash = try_deserialize_g1_projective(
|
||||
&cm_hash_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment hash".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_hash_bytes_len;
|
||||
|
||||
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
|
||||
j += 8;
|
||||
if bytes[j..].len() < c_len as usize * 96 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: c_len as usize * 96,
|
||||
actual: bytes[56..].len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut private_attributes_ciphertexts = Vec::with_capacity(c_len as usize);
|
||||
for i in 0..c_len as usize {
|
||||
let start = j + i * 96;
|
||||
let end = start + 96;
|
||||
private_attributes_ciphertexts.push(Ciphertext::try_from(&bytes[start..end])?)
|
||||
}
|
||||
|
||||
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 96..])?;
|
||||
|
||||
Ok(BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_ciphertexts,
|
||||
pi_s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for BlindSignRequest {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
let cm_bytes = self.commitment.to_affine().to_compressed();
|
||||
let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed();
|
||||
let c_len = self.private_attributes_ciphertexts.len() as u64;
|
||||
let proof_bytes = self.pi_s.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 96 + proof_bytes.len());
|
||||
|
||||
bytes.extend_from_slice(&cm_bytes);
|
||||
bytes.extend_from_slice(&cm_hash_bytes);
|
||||
bytes.extend_from_slice(&c_len.to_le_bytes());
|
||||
for c in &self.private_attributes_ciphertexts {
|
||||
bytes.extend_from_slice(&c.to_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
BlindSignRequest::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindSignRequest {}
|
||||
|
||||
impl BlindSignRequest {
|
||||
fn verify_proof(&self, params: &Parameters, pub_key: &elgamal::PublicKey) -> bool {
|
||||
self.pi_s.verify(
|
||||
params,
|
||||
pub_key,
|
||||
&self.commitment,
|
||||
&self.private_attributes_ciphertexts,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_commitment_hash(&self) -> G1Projective {
|
||||
self.commitment_hash
|
||||
}
|
||||
|
||||
// TODO: perhaps also include pi_s.len()?
|
||||
// to be determined once we implement serde to make sure its 1:1 compatible
|
||||
// with bincode
|
||||
// cm || c.len() || c || pi_s
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
BlindSignRequest::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_private_attributes_commitment(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
hs: &[G1Affine],
|
||||
) -> (Scalar, G1Projective) {
|
||||
let commitment_opening = params.random_scalar();
|
||||
|
||||
// Produces h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
// where m0, m1, ...., mn are private attributes
|
||||
let attr_cm = private_attributes
|
||||
.iter()
|
||||
.zip(hs)
|
||||
.map(|(&m, h)| h * m)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
// Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
let commitment = params.gen1() * commitment_opening + attr_cm;
|
||||
(commitment_opening, commitment)
|
||||
}
|
||||
|
||||
pub fn compute_commitment_hash(commitment: G1Projective) -> G1Projective {
|
||||
hash_g1(commitment.to_bytes())
|
||||
}
|
||||
|
||||
pub fn compute_attribute_encryption(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
pub_key: &elgamal::PublicKey,
|
||||
commitment_hash: G1Projective,
|
||||
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
|
||||
private_attributes
|
||||
.iter()
|
||||
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Builds cryptographic material required for blind sign.
|
||||
pub fn prepare_blind_sign(
|
||||
params: &Parameters,
|
||||
elgamal_keypair: &ElGamalKeyPair,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<BlindSignRequest> {
|
||||
if private_attributes.is_empty() {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Tried to prepare blind sign request for an empty set of private attributes"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = params.gen_hs();
|
||||
if private_attributes.len() + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: private_attributes.len() + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let (commitment_opening, commitment) =
|
||||
compute_private_attributes_commitment(params, private_attributes, hs);
|
||||
|
||||
// Compute the challenge as the commitment hash
|
||||
let commitment_hash = compute_commitment_hash(commitment);
|
||||
// build ElGamal encryption
|
||||
let (private_attributes_ciphertexts, ephemeral_keys): (Vec<_>, Vec<_>) =
|
||||
compute_attribute_encryption(
|
||||
params,
|
||||
private_attributes,
|
||||
elgamal_keypair.public_key(),
|
||||
commitment_hash,
|
||||
);
|
||||
|
||||
let pi_s = ProofCmCs::construct(
|
||||
params,
|
||||
elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&commitment,
|
||||
&commitment_opening,
|
||||
private_attributes,
|
||||
&*private_attributes_ciphertexts,
|
||||
);
|
||||
|
||||
Ok(BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_ciphertexts,
|
||||
pi_s,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn blind_sign(
|
||||
params: &Parameters,
|
||||
signing_secret_key: &SecretKey,
|
||||
prover_pub_key: &elgamal::PublicKey,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<BlindedSignature> {
|
||||
let num_private = blind_sign_request.private_attributes_ciphertexts.len();
|
||||
let hs = params.gen_hs();
|
||||
|
||||
if num_private + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: num_private + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the commitment hash
|
||||
let h = hash_g1(blind_sign_request.commitment.to_bytes());
|
||||
if !(h == blind_sign_request.commitment_hash) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the commitment hash".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Verify the ZK proof
|
||||
if !blind_sign_request.verify_proof(params, prover_pub_key) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the proof of knowledge".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// in python implementation there are n^2 G1 multiplications, let's do it with a single one instead.
|
||||
// i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes)
|
||||
// rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately
|
||||
let signed_public = h * public_attributes
|
||||
.iter()
|
||||
.zip(signing_secret_key.ys.iter().skip(num_private))
|
||||
.map(|(attr, yi)| attr * yi)
|
||||
.sum::<Scalar>();
|
||||
|
||||
// c1[0] ^ y[0] * ... * c1[m] ^ y[m]
|
||||
let sig_1 = blind_sign_request
|
||||
.private_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c1())
|
||||
.zip(signing_secret_key.ys.iter())
|
||||
.map(|(c1, yi)| c1 * yi)
|
||||
.sum();
|
||||
|
||||
// h ^ x + c2[0] ^ y[0] + ... c2[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
|
||||
let sig_2 = blind_sign_request
|
||||
.private_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c2())
|
||||
.zip(signing_secret_key.ys.iter())
|
||||
.map(|(c2, yi)| c2 * yi)
|
||||
.chain(std::iter::once(h * signing_secret_key.x))
|
||||
.chain(std::iter::once(signed_public))
|
||||
.sum();
|
||||
|
||||
Ok(BlindedSignature(h, elgamal::Ciphertext(sig_1, sig_2)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn sign(
|
||||
params: &mut Parameters,
|
||||
secret_key: &SecretKey,
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<Signature> {
|
||||
if public_attributes.len() > secret_key.ys.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: secret_key.ys.len(),
|
||||
requested: public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: why in the python implementation this hash onto the curve is present
|
||||
// while it's not used in the paper? the paper uses random exponent instead.
|
||||
// (the python implementation hashes string representation of all attributes onto the curve,
|
||||
// but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary
|
||||
// transformation. If I'm wrong, please correct me.)
|
||||
let attributes_sum = public_attributes.iter().sum::<Scalar>();
|
||||
let h = hash_g1((params.gen1() * attributes_sum).to_bytes());
|
||||
|
||||
// x + m0 * y0 + m1 * y1 + ... mn * yn
|
||||
let exponent = secret_key.x
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(secret_key.ys.iter())
|
||||
.map(|(m_i, y_i)| m_i * y_i)
|
||||
.sum::<Scalar>();
|
||||
|
||||
let sig2 = h * exponent;
|
||||
Ok(Signature(h, sig2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn blind_sign_request_bytes_roundtrip() {
|
||||
let mut params = Parameters::new(1).unwrap();
|
||||
let public_attributes = params.n_random_scalars(0);
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
println!("{:?}", bytes.len());
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,550 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use core::iter::Sum;
|
||||
use core::ops::{Add, Mul};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::aggregation::aggregate_verification_keys;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
use crate::traits::Bytable;
|
||||
use crate::utils::{
|
||||
try_deserialize_g2_projective, try_deserialize_scalar, try_deserialize_scalar_vec, Polynomial,
|
||||
};
|
||||
use crate::Base58;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct SecretKey {
|
||||
pub(crate) x: Scalar,
|
||||
pub(crate) ys: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SecretKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
|
||||
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8,
|
||||
target: 32 * 2 + 8,
|
||||
modulus: 32,
|
||||
object: "secret key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
|
||||
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
||||
let actual_ys_len = (bytes.len() - 40) / 32;
|
||||
|
||||
if ys_len as usize != actual_ys_len {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Tried to deserialize secret key with inconsistent ys len (expected {}, got {})",
|
||||
ys_len, actual_ys_len
|
||||
)));
|
||||
}
|
||||
|
||||
let x = try_deserialize_scalar(
|
||||
&x_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()),
|
||||
)?;
|
||||
let ys = try_deserialize_scalar_vec(
|
||||
ys_len,
|
||||
&bytes[40..],
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(SecretKey { x, ys })
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
/// Derive verification key using this secret key.
|
||||
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
|
||||
let g2 = params.gen2();
|
||||
VerificationKey {
|
||||
alpha: g2 * self.x,
|
||||
beta: self.ys.iter().map(|y| g2 * y).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// x || ys.len() || ys
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let ys_len = self.ys.len() as u64;
|
||||
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.x.to_bytes());
|
||||
bytes.extend_from_slice(&ys_len.to_le_bytes());
|
||||
for y in self.ys.iter() {
|
||||
bytes.extend_from_slice(&y.to_bytes())
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey> {
|
||||
SecretKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for SecretKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
SecretKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for SecretKey {}
|
||||
|
||||
// TODO: perhaps change points to affine representation
|
||||
// to make verification slightly more efficient?
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct VerificationKey {
|
||||
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
|
||||
pub alpha: G2Projective,
|
||||
pub beta: Vec<G2Projective>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for VerificationKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
if bytes.len() < 96 * 2 + 8 || (bytes.len() - 8) % 96 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8,
|
||||
target: 96 * 2 + 8,
|
||||
modulus: 96,
|
||||
object: "secret key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
|
||||
let beta_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
|
||||
let actual_beta_len = (bytes.len() - 104) / 96;
|
||||
|
||||
if beta_len as usize != actual_beta_len {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize verification key with inconsistent beta len (expected {}, got {})",
|
||||
beta_len, actual_beta_len
|
||||
)));
|
||||
}
|
||||
|
||||
let alpha = try_deserialize_g2_projective(
|
||||
&alpha_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (alpha)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut beta = Vec::with_capacity(actual_beta_len);
|
||||
for i in 0..actual_beta_len {
|
||||
let start = 104 + i * 96;
|
||||
let end = start + 96;
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g2_projective(
|
||||
&beta_i_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (beta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
beta.push(beta_i)
|
||||
}
|
||||
|
||||
Ok(VerificationKey { alpha, beta })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Add<&'b VerificationKey> for VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: &'b VerificationKey) -> VerificationKey {
|
||||
// If you're trying to add two keys together that were created
|
||||
// for different number of attributes, just panic as it's a
|
||||
// nonsense operation.
|
||||
assert_eq!(
|
||||
self.beta.len(),
|
||||
rhs.beta.len(),
|
||||
"trying to add verification keys generated for different number of attributes"
|
||||
);
|
||||
|
||||
VerificationKey {
|
||||
alpha: self.alpha + rhs.alpha,
|
||||
beta: self
|
||||
.beta
|
||||
.iter()
|
||||
.zip(rhs.beta.iter())
|
||||
.map(|(self_beta, rhs_beta)| self_beta + rhs_beta)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Mul<Scalar> for &'a VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: Scalar) -> Self::Output {
|
||||
VerificationKey {
|
||||
alpha: self.alpha * rhs,
|
||||
beta: self.beta.iter().map(|b_i| b_i * rhs).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sum<T> for VerificationKey
|
||||
where
|
||||
T: Borrow<VerificationKey>,
|
||||
{
|
||||
#[inline]
|
||||
fn sum<I>(iter: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = T>,
|
||||
{
|
||||
let mut peekable = iter.peekable();
|
||||
let head_attributes = match peekable.peek() {
|
||||
Some(head) => head.borrow().beta.len(),
|
||||
None => {
|
||||
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
|
||||
// of VerificationKey. So should it panic here or just return some nonsense value?
|
||||
return VerificationKey::identity(0);
|
||||
}
|
||||
};
|
||||
|
||||
peekable.fold(VerificationKey::identity(head_attributes), |acc, item| {
|
||||
acc + item.borrow()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VerificationKey {
|
||||
/// Create a (kinda) identity verification key using specified
|
||||
/// number of 'beta' elements
|
||||
pub(crate) fn identity(beta_size: usize) -> Self {
|
||||
VerificationKey {
|
||||
alpha: G2Projective::identity(),
|
||||
beta: vec![G2Projective::identity(); beta_size],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
|
||||
aggregate_verification_keys(sigs, indices)
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> &G2Projective {
|
||||
&self.alpha
|
||||
}
|
||||
|
||||
pub fn beta(&self) -> &Vec<G2Projective> {
|
||||
&self.beta
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let beta_len = self.beta.len() as u64;
|
||||
let mut bytes = Vec::with_capacity(8 + (beta_len + 1) as usize * 96);
|
||||
|
||||
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
|
||||
bytes.extend_from_slice(&beta_len.to_le_bytes());
|
||||
for beta in self.beta.iter() {
|
||||
bytes.extend_from_slice(&beta.to_affine().to_compressed())
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
VerificationKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for VerificationKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
VerificationKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for VerificationKey {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct KeyPair {
|
||||
secret_key: SecretKey,
|
||||
verification_key: VerificationKey,
|
||||
|
||||
/// Optional index value specifying polynomial point used during threshold key generation.
|
||||
pub index: Option<SignerIndex>,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
const MARKER_BYTES: &'static [u8] = b"coconutkeypair";
|
||||
|
||||
pub fn secret_key(&self) -> SecretKey {
|
||||
self.secret_key.clone()
|
||||
}
|
||||
|
||||
pub fn verification_key(&self) -> VerificationKey {
|
||||
self.verification_key.clone()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from_byte_slice(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for KeyPair {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
let mut byts = vec![];
|
||||
let secret_key_bytes = self.secret_key.to_bytes();
|
||||
let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes();
|
||||
let verification_key_bytes = self.verification_key.to_bytes();
|
||||
let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes();
|
||||
byts.extend_from_slice(Self::MARKER_BYTES);
|
||||
byts.extend_from_slice(&secret_key_len);
|
||||
byts.extend_from_slice(&secret_key_bytes);
|
||||
byts.extend_from_slice(&verification_key_len);
|
||||
byts.extend_from_slice(&verification_key_bytes);
|
||||
if let Some(index) = self.index {
|
||||
byts.extend_from_slice(&index.to_le_bytes())
|
||||
}
|
||||
byts
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for KeyPair {}
|
||||
|
||||
impl TryFrom<&[u8]> for KeyPair {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<KeyPair> {
|
||||
let header_len = Self::MARKER_BYTES.len();
|
||||
|
||||
// we must be able to at the very least read the length of secret key which is past the header
|
||||
// and is 8 bytes long
|
||||
if bytes.len() < header_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: header_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let secret_key_len =
|
||||
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
|
||||
let secret_key_start = header_len + 8;
|
||||
|
||||
let secret_key =
|
||||
SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?;
|
||||
|
||||
// we must be able to read the length of verification key
|
||||
if bytes.len() < secret_key_start + secret_key_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: secret_key_start + secret_key_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let verification_key_len = u64::from_le_bytes(
|
||||
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
) as usize;
|
||||
let verification_key_start = secret_key_start + secret_key_len + 8;
|
||||
|
||||
let verification_key = VerificationKey::try_from(
|
||||
&bytes[verification_key_start..verification_key_start + verification_key_len],
|
||||
)?;
|
||||
let consumed_bytes = verification_key_start + verification_key_len;
|
||||
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
|
||||
Some(u64::from_le_bytes(
|
||||
bytes[consumed_bytes..consumed_bytes + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
|
||||
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
|
||||
/// that are independent of each other.
|
||||
#[cfg(test)]
|
||||
pub fn keygen(params: &Parameters) -> KeyPair {
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
let x = params.random_scalar();
|
||||
let ys = params.n_random_scalars(attributes);
|
||||
|
||||
let secret_key = SecretKey { x, ys };
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
|
||||
/// such that they support threshold aggregation by `threshold` number of parties.
|
||||
/// It is expected that this procedure is executed by a Trusted Third Party.
|
||||
pub fn ttp_keygen(
|
||||
params: &Parameters,
|
||||
threshold: u64,
|
||||
num_authorities: u64,
|
||||
) -> Result<Vec<KeyPair>> {
|
||||
if threshold == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to generate threshold keys with a 0 threshold value".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if threshold > num_authorities {
|
||||
return Err(
|
||||
CoconutError::Setup(
|
||||
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
// generate polynomials
|
||||
let v = Polynomial::new_random(params, threshold - 1);
|
||||
let ws = (0..attributes)
|
||||
.map(|_| Polynomial::new_random(params, threshold - 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: potentially if we had some known authority identifier we could use that instead
|
||||
// of the increasing (1,2,3,...) sequence
|
||||
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
|
||||
|
||||
// generate polynomial shares
|
||||
let x = polynomial_indices
|
||||
.iter()
|
||||
.map(|&id| v.evaluate(&Scalar::from(id)));
|
||||
let ys = polynomial_indices.iter().map(|&id| {
|
||||
ws.iter()
|
||||
.map(|w| w.evaluate(&Scalar::from(id)))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// finally set the keys
|
||||
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys });
|
||||
|
||||
let keypairs = secret_keys
|
||||
.zip(polynomial_indices.iter())
|
||||
.map(|(secret_key, index)| {
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: Some(*index),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keypair_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params1);
|
||||
let keypair5 = keygen(&mut params5);
|
||||
|
||||
let bytes1 = keypair1.to_bytes();
|
||||
let bytes5 = keypair5.to_bytes();
|
||||
|
||||
assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1);
|
||||
assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_key_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params1);
|
||||
let keypair5 = keygen(&mut params5);
|
||||
|
||||
let bytes1 = keypair1.secret_key.to_bytes();
|
||||
let bytes5 = keypair5.secret_key.to_bytes();
|
||||
|
||||
assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key);
|
||||
assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_key_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = &keygen(&mut params1);
|
||||
let keypair5 = &keygen(&mut params5);
|
||||
|
||||
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
|
||||
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
|
||||
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes1.as_slice()).unwrap(),
|
||||
keypair1.verification_key
|
||||
);
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes5.as_slice()).unwrap(),
|
||||
keypair5.verification_key
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,600 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// TODO: implement https://crates.io/crates/signature traits?
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
|
||||
pub use keygen::{SecretKey, VerificationKey};
|
||||
|
||||
use crate::elgamal::Ciphertext;
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g1_projective;
|
||||
use crate::{elgamal, Attribute};
|
||||
|
||||
pub mod aggregation;
|
||||
pub mod issuance;
|
||||
pub mod keygen;
|
||||
pub mod setup;
|
||||
pub mod verification;
|
||||
|
||||
pub type SignerIndex = u64;
|
||||
|
||||
// (h, s)
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Signature(pub G1Projective, pub G1Projective);
|
||||
|
||||
pub type PartialSignature = Signature;
|
||||
|
||||
impl TryFrom<&[u8]> for Signature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Signature> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Signature must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let sig1 = try_deserialize_g1_projective(
|
||||
sig1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()),
|
||||
)?;
|
||||
|
||||
let sig2 = try_deserialize_g1_projective(
|
||||
sig2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Signature(sig1, sig2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub(crate) fn sig1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub(crate) fn sig2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
|
||||
let r = params.random_scalar();
|
||||
let r_prime = params.random_scalar();
|
||||
let h_prime = self.0 * r_prime;
|
||||
let s_prime = (self.1 * r_prime) + (h_prime * r);
|
||||
(Signature(h_prime, s_prime), r)
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
|
||||
Signature::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Signature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Signature::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Signature {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct BlindedSignature(G1Projective, elgamal::Ciphertext);
|
||||
|
||||
impl Bytable for BlindedSignature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Self::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindedSignature {}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindedSignature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
if bytes.len() != 144 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"BlindedSignature must be exactly 144 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
|
||||
let h = try_deserialize_g1_projective(
|
||||
h_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed h".to_string()),
|
||||
)?;
|
||||
let c_tilde = Ciphertext::try_from(&bytes[48..])?;
|
||||
|
||||
Ok(BlindedSignature(h, c_tilde))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlindedSignature {
|
||||
pub fn unblind(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
private_key: &elgamal::PrivateKey,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
) -> Result<Signature> {
|
||||
// parse the signature
|
||||
let h = &self.0;
|
||||
let c = &self.1;
|
||||
let sig2 = private_key.decrypt(c);
|
||||
|
||||
// Verify the commitment hash
|
||||
if !(commitment_hash == h) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of commitment hash from signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let alpha = partial_verification_key.alpha;
|
||||
|
||||
let tmp = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(partial_verification_key.beta.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// Verify the signature share
|
||||
if !check_bilinear_pairing(
|
||||
&h.to_affine(),
|
||||
&G2Prepared::from((alpha + tmp).to_affine()),
|
||||
&sig2.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of signature share failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Signature(self.0, sig2))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 144] {
|
||||
let mut bytes = [0u8; 144];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
BlindedSignature::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// perhaps this should take signature by reference? we'll see how it goes
|
||||
pub struct SignatureShare {
|
||||
signature: Signature,
|
||||
index: SignerIndex,
|
||||
}
|
||||
|
||||
impl SignatureShare {
|
||||
pub fn new(signature: Signature, index: SignerIndex) -> Self {
|
||||
SignatureShare { signature, index }
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn index(&self) -> SignerIndex {
|
||||
self.index
|
||||
}
|
||||
|
||||
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
|
||||
// aggregate_signature_shares(shares)
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::aggregation::{aggregate_signatures, aggregate_verification_keys};
|
||||
use crate::scheme::issuance::{blind_sign, prepare_blind_sign, sign};
|
||||
use crate::scheme::keygen::{keygen, ttp_keygen};
|
||||
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_private_attributes() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
|
||||
let lambda =
|
||||
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(
|
||||
&mut params,
|
||||
&keypair2.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta1,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_attributes() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig2,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes() {
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(
|
||||
&mut params,
|
||||
&keypair2.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta1,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
|
||||
let keypairs = ttp_keygen(&mut params, 2, 3).unwrap();
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sigs = keypairs
|
||||
.iter()
|
||||
.map(|keypair| {
|
||||
blind_sign(
|
||||
&mut params,
|
||||
&keypair.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
// taking different subset of keys and credentials
|
||||
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = signature.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
signature.0.to_affine().to_compressed(),
|
||||
signature.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blinded_signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let t = params.random_scalar();
|
||||
let blinded_sig = BlindedSignature(
|
||||
params.gen1() * t,
|
||||
Ciphertext(params.gen1() * r, params.gen1() * s),
|
||||
);
|
||||
let bytes = blinded_sig.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
blinded_sig.0.to_affine().to_compressed(),
|
||||
blinded_sig.1 .0.to_affine().to_compressed(),
|
||||
blinded_sig.1 .1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::utils::hash_g1;
|
||||
use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar};
|
||||
use ff::Field;
|
||||
use group::Curve;
|
||||
use rand::thread_rng;
|
||||
/// System-wide parameters used for the protocol
|
||||
pub struct Parameters {
|
||||
/// Generator of the G1 group
|
||||
g1: G1Affine,
|
||||
|
||||
/// Additional generators of the G1 group
|
||||
hs: Vec<G1Affine>,
|
||||
|
||||
/// Generator of the G2 group
|
||||
g2: G2Affine,
|
||||
|
||||
/// Precomputed G2 generator used for the miller loop
|
||||
_g2_prepared_miller: G2Prepared,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
pub fn new(num_attributes: u32) -> Result<Parameters> {
|
||||
if num_attributes == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to setup the scheme for 0 attributes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = (1..=num_attributes)
|
||||
.map(|i| hash_g1(format!("h{}", i)).to_affine())
|
||||
.collect();
|
||||
|
||||
Ok(Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs,
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn gen1(&self) -> &G1Affine {
|
||||
&self.g1
|
||||
}
|
||||
|
||||
pub fn gen2(&self) -> &G2Affine {
|
||||
&self.g2
|
||||
}
|
||||
|
||||
pub fn prepared_miller_g2(&self) -> &G2Prepared {
|
||||
&self._g2_prepared_miller
|
||||
}
|
||||
|
||||
pub fn gen_hs(&self) -> &[G1Affine] {
|
||||
&self.hs
|
||||
}
|
||||
|
||||
pub fn random_scalar(&self) -> Scalar {
|
||||
// lazily-initialized thread-local random number generator, seeded by the system
|
||||
let mut rng = thread_rng();
|
||||
Scalar::random(&mut rng)
|
||||
}
|
||||
|
||||
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
|
||||
(0..n).map(|_| self.random_scalar()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(num_attributes: u32) -> Result<Parameters> {
|
||||
Parameters::new(num_attributes)
|
||||
}
|
||||
|
||||
// for ease of use in tests requiring params
|
||||
// TODO: not sure if this will have to go away when tests require some specific number of generators
|
||||
#[cfg(test)]
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs: Vec::new(),
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::ops::Neg;
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
|
||||
use group::{Curve, Group};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::{ProofKappa, ProofKappaNu};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::Signature;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g2_projective;
|
||||
use crate::Attribute;
|
||||
|
||||
// TODO NAMING: this whole thing
|
||||
// Theta
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ThetaCovid {
|
||||
// blinded_message (kappa)
|
||||
pub blinded_message: G2Projective,
|
||||
// sigma
|
||||
pub credential: Signature,
|
||||
// pi_v
|
||||
pub pi_v: ProofKappa,
|
||||
}
|
||||
|
||||
impl ThetaCovid {
|
||||
pub fn verify_proof(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
verifier_id: &[u8; 32],
|
||||
timestamp: &[u8; 32],
|
||||
) -> bool {
|
||||
self.pi_v.verify(
|
||||
params,
|
||||
verification_key,
|
||||
&self.blinded_message,
|
||||
verifier_id,
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
// kappa || credential || proof
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
|
||||
let credential_bytes = self.credential.to_bytes();
|
||||
let proof_bytes = self.pi_v.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(192 + proof_bytes.len());
|
||||
bytes.extend_from_slice(&blinded_message_bytes);
|
||||
bytes.extend_from_slice(&credential_bytes);
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn to_bytes_tuple(&self) -> ([u8; 96], [u8; 96], Vec<u8>) {
|
||||
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
|
||||
let credential_bytes = self.credential.to_bytes();
|
||||
let proof_bytes = self.pi_v.to_bytes();
|
||||
|
||||
(blinded_message_bytes, credential_bytes, proof_bytes)
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<ThetaCovid> {
|
||||
if bytes.len() < 192 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 192, got {}", bytes.len()),
|
||||
));
|
||||
}
|
||||
|
||||
let blinded_message_bytes = bytes[..96].try_into().unwrap();
|
||||
let blinded_message = try_deserialize_g2_projective(
|
||||
&blinded_message_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize kappa".to_string()),
|
||||
)?;
|
||||
|
||||
let credential = Signature::try_from(&bytes[96..192])?;
|
||||
|
||||
let pi_v = ProofKappa::from_bytes(&bytes[192..])?;
|
||||
|
||||
Ok(ThetaCovid {
|
||||
blinded_message,
|
||||
credential,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Theta {
|
||||
// blinded_message (kappa)
|
||||
pub blinded_message: G2Projective,
|
||||
// blinded serial number (zeta)
|
||||
pub blinded_serial_number: G2Projective,
|
||||
// sigma
|
||||
pub credential: Signature,
|
||||
// pi_v
|
||||
pub pi_v: ProofKappaNu,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Theta {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Theta> {
|
||||
if bytes.len() < 288 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()),
|
||||
));
|
||||
}
|
||||
|
||||
let blinded_message_bytes = bytes[..96].try_into().unwrap();
|
||||
let blinded_message = try_deserialize_g2_projective(
|
||||
&blinded_message_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize kappa".to_string()),
|
||||
)?;
|
||||
|
||||
let blinded_serial_number_bytes = bytes[96..192].try_into().unwrap();
|
||||
let blinded_serial_number = try_deserialize_g2_projective(
|
||||
&blinded_serial_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize zeta".to_string()),
|
||||
)?;
|
||||
let credential = Signature::try_from(&bytes[192..288])?;
|
||||
|
||||
let pi_v = ProofKappaNu::from_bytes(&bytes[288..])?;
|
||||
|
||||
Ok(Theta {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
credential,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Theta {
|
||||
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
|
||||
self.pi_v.verify(
|
||||
params,
|
||||
verification_key,
|
||||
&self.blinded_message,
|
||||
&self.blinded_serial_number,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: perhaps also include pi_v.len()?
|
||||
// to be determined once we implement serde to make sure its 1:1 compatible
|
||||
// with bincode
|
||||
// kappa || nu || credential || pi_v
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
|
||||
let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed();
|
||||
let credential_bytes = self.credential.to_bytes();
|
||||
let proof_bytes = self.pi_v.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(288 + proof_bytes.len());
|
||||
bytes.extend_from_slice(&blinded_message_bytes);
|
||||
bytes.extend_from_slice(&blinded_serial_number_bytes);
|
||||
bytes.extend_from_slice(&credential_bytes);
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Theta> {
|
||||
Theta::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Theta {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Theta::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Theta {}
|
||||
|
||||
pub fn compute_kappa(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
blinding_factor: Scalar,
|
||||
) -> G2Projective {
|
||||
params.gen2() * blinding_factor
|
||||
+ verification_key.alpha
|
||||
+ private_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>()
|
||||
}
|
||||
|
||||
pub fn compute_zeta(params: &Parameters, serial_number: Attribute) -> G2Projective {
|
||||
params.gen2() * serial_number
|
||||
}
|
||||
|
||||
pub fn prove_covid_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
signature: &Signature,
|
||||
private_attributes: &[Attribute],
|
||||
verifier_id: &[u8; 32],
|
||||
timestamp: &[u8; 32],
|
||||
) -> Result<ThetaCovid> {
|
||||
if verification_key.beta.len() < params.gen_hs().len() {
|
||||
return Err(
|
||||
CoconutError::Verification(
|
||||
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
|
||||
verification_key.beta.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Randomize the signature
|
||||
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
|
||||
|
||||
// blinded_message : kappa in the paper.
|
||||
// Value kappa is needed since we want to show a signature sigma'.
|
||||
// In order to verify sigma' we need both the verification key vk and the message m.
|
||||
// However, we do not want to reveal m to whomever we are showing the signature.
|
||||
// Thus, we need kappa which allows us to verify sigma'. In particular,
|
||||
// kappa is computed on m as input, but thanks to the use or random value r,
|
||||
// it does not reveal any information about m.
|
||||
let blinded_message = compute_kappa(
|
||||
params,
|
||||
verification_key,
|
||||
&private_attributes,
|
||||
sign_blinding_factor,
|
||||
);
|
||||
|
||||
let pi_v = ProofKappa::construct(
|
||||
params,
|
||||
verification_key,
|
||||
&sign_blinding_factor,
|
||||
&blinded_message,
|
||||
&private_attributes,
|
||||
verifier_id,
|
||||
timestamp,
|
||||
);
|
||||
|
||||
Ok(ThetaCovid {
|
||||
blinded_message,
|
||||
credential: signature_prime,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prove_bandwidth_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
signature: &Signature,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
) -> Result<Theta> {
|
||||
if verification_key.beta.len() < 2 {
|
||||
return Err(
|
||||
CoconutError::Verification(
|
||||
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
|
||||
verification_key.beta.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Randomize the signature
|
||||
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
|
||||
|
||||
// blinded_message : kappa in the paper.
|
||||
// Value kappa is needed since we want to show a signature sigma'.
|
||||
// In order to verify sigma' we need both the verification key vk and the message m.
|
||||
// However, we do not want to reveal m to whomever we are showing the signature.
|
||||
// Thus, we need kappa which allows us to verify sigma'. In particular,
|
||||
// kappa is computed on m as input, but thanks to the use or random value r,
|
||||
// it does not reveal any information about m.
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let blinded_message = compute_kappa(
|
||||
params,
|
||||
verification_key,
|
||||
&private_attributes,
|
||||
sign_blinding_factor,
|
||||
);
|
||||
|
||||
// zeta is a commitment to the serial number (i.e., a public value associated with the serial number)
|
||||
let blinded_serial_number = compute_zeta(params, serial_number);
|
||||
|
||||
let pi_v = ProofKappaNu::construct(
|
||||
params,
|
||||
verification_key,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&sign_blinding_factor,
|
||||
&blinded_message,
|
||||
&blinded_serial_number,
|
||||
);
|
||||
|
||||
Ok(Theta {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
credential: signature_prime,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether e(P, Q) * e(-R, S) == id
|
||||
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
|
||||
// checking e(P, Q) * e(-R, S) == id
|
||||
// is equivalent to checking e(P, Q) == e(R, S)
|
||||
// but requires only a single final exponentiation rather than two of them
|
||||
// and therefore, as seen via benchmarks.rs, is almost 50% faster
|
||||
// (1.47ms vs 2.45ms, tested on R9 5900X)
|
||||
|
||||
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
|
||||
multi_miller.final_exponentiation().is_identity().into()
|
||||
}
|
||||
|
||||
pub fn verify_covid_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
theta: &ThetaCovid,
|
||||
public_attributes: &[Attribute],
|
||||
verifier_id: &[u8; 32],
|
||||
timestamp: &[u8; 32],
|
||||
) -> bool {
|
||||
if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !theta.verify_proof(params, verification_key, verifier_id, timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let kappa = if public_attributes.is_empty() {
|
||||
theta.blinded_message
|
||||
} else {
|
||||
let signed_public_attributes = public_attributes
|
||||
.iter()
|
||||
.zip(
|
||||
verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.skip(theta.pi_v.private_attributes_len()),
|
||||
)
|
||||
.map(|(pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
theta.blinded_message + signed_public_attributes
|
||||
};
|
||||
|
||||
check_bilinear_pairing(
|
||||
&theta.credential.0.to_affine(),
|
||||
&G2Prepared::from(kappa.to_affine()),
|
||||
&(theta.credential.1).to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(theta.credential.0.is_identity())
|
||||
}
|
||||
|
||||
pub fn verify_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
theta: &Theta,
|
||||
public_attributes: &[Attribute],
|
||||
) -> bool {
|
||||
if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !theta.verify_proof(params, verification_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let kappa = if public_attributes.is_empty() {
|
||||
theta.blinded_message
|
||||
} else {
|
||||
let signed_public_attributes = public_attributes
|
||||
.iter()
|
||||
.zip(
|
||||
verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.skip(theta.pi_v.private_attributes_len()),
|
||||
)
|
||||
.map(|(pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
theta.blinded_message + signed_public_attributes
|
||||
};
|
||||
|
||||
check_bilinear_pairing(
|
||||
&theta.credential.0.to_affine(),
|
||||
&G2Prepared::from(kappa.to_affine()),
|
||||
&(theta.credential.1).to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(theta.credential.0.is_identity())
|
||||
}
|
||||
|
||||
// Used in tests only
|
||||
#[cfg(test)]
|
||||
pub fn verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
public_attributes: &[Attribute],
|
||||
sig: &Signature,
|
||||
) -> bool {
|
||||
let kappa = (verification_key.alpha
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(m_i, b_i)| b_i * m_i)
|
||||
.sum::<G2Projective>())
|
||||
.to_affine();
|
||||
|
||||
check_bilinear_pairing(
|
||||
&sig.0.to_affine(),
|
||||
&G2Prepared::from(kappa),
|
||||
&sig.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(sig.0.is_identity())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn theta_bytes_roundtrip() {
|
||||
let mut params = setup(2).unwrap();
|
||||
|
||||
let keypair = keygen(&mut params);
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = theta.to_bytes();
|
||||
assert_eq!(Theta::try_from(bytes.as_slice()).unwrap(), theta);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use crate::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
|
||||
hash_to_scalar, prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen,
|
||||
verify_credential, CoconutError, Signature, SignatureShare, VerificationKey,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn main() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let elgamal_keypair = elgamal_keygen(¶ms);
|
||||
|
||||
// generate commitment and encryption
|
||||
let blind_sign_request = prepare_blind_sign(
|
||||
¶ms,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)?;
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?;
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?;
|
||||
|
||||
// generate blinded signatures
|
||||
let mut blinded_signatures = Vec::new();
|
||||
|
||||
for keypair in coconut_keypairs {
|
||||
let blinded_signature = blind_sign(
|
||||
¶ms,
|
||||
&keypair.secret_key(),
|
||||
&elgamal_keypair.public_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)?;
|
||||
blinded_signatures.push(blinded_signature)
|
||||
}
|
||||
|
||||
// Unblind
|
||||
|
||||
let unblinded_signatures: Vec<Signature> = blinded_signatures
|
||||
.into_iter()
|
||||
.zip(verification_keys.iter())
|
||||
.map(|(signature, verification_key)| {
|
||||
signature
|
||||
.unblind(
|
||||
¶ms,
|
||||
&elgamal_keypair.private_key(),
|
||||
&verification_key,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregate signatures
|
||||
|
||||
let signature_shares: Vec<SignatureShare> = unblinded_signatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
|
||||
.collect();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
// Randomize credentials and generate any cryptographic material to verify them
|
||||
let signature =
|
||||
aggregate_signature_shares(¶ms, &verification_key, &attributes, &signature_shares)?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)?;
|
||||
|
||||
// Verify credentials
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use crate::scheme::verification::{prove_covid_credential, verify_covid_credential, ThetaCovid};
|
||||
use crate::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
|
||||
hash_to_scalar, prepare_blind_sign, setup, ttp_keygen, CoconutError, Signature, SignatureShare,
|
||||
VerificationKey,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn main() -> Result<(), CoconutError> {
|
||||
let params = setup(15)?;
|
||||
|
||||
// validators keys
|
||||
let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?;
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect();
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?;
|
||||
|
||||
// user's ElGamal keypair
|
||||
let elgamal_keypair = elgamal_keygen(¶ms);
|
||||
|
||||
// attributes to consider
|
||||
let patient_id = hash_to_scalar(String::from("NHS678777").as_bytes());
|
||||
let full_name = hash_to_scalar(String::from("JaneDoe").as_bytes());
|
||||
let vaccine_medication_product_id = hash_to_scalar(String::from("EU/1/20/1528").as_bytes());
|
||||
let country_of_vaccination = hash_to_scalar(String::from("UK").as_bytes());
|
||||
let issuer = hash_to_scalar(String::from("NHS").as_bytes());
|
||||
let dob = hash_to_scalar(String::from("2021-11-05").as_bytes());
|
||||
|
||||
let public_attributes = vec![
|
||||
patient_id,
|
||||
full_name,
|
||||
vaccine_medication_product_id,
|
||||
country_of_vaccination,
|
||||
issuer,
|
||||
dob,
|
||||
];
|
||||
let user_secret = params.random_scalar();
|
||||
let private_attributes = vec![user_secret];
|
||||
|
||||
// ISSUANCE PROTOCOL
|
||||
let blind_sign_request = prepare_blind_sign(
|
||||
¶ms,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)?;
|
||||
|
||||
// generate blinded signatures
|
||||
let mut blinded_signatures = Vec::new();
|
||||
|
||||
let is_vaccinated = hash_to_scalar(String::from("TRUE").as_bytes());
|
||||
let is_over_18 = hash_to_scalar(String::from("TRUE").as_bytes());
|
||||
let is_over_21 = hash_to_scalar(String::from("TRUE").as_bytes());
|
||||
|
||||
// These are the attributes on which the validator issues a signature
|
||||
let public_attributes = [
|
||||
patient_id,
|
||||
full_name,
|
||||
vaccine_medication_product_id,
|
||||
country_of_vaccination,
|
||||
issuer,
|
||||
dob,
|
||||
is_vaccinated,
|
||||
is_over_18,
|
||||
is_over_21,
|
||||
];
|
||||
|
||||
for keypair in coconut_keypairs {
|
||||
let blinded_signature = blind_sign(
|
||||
¶ms,
|
||||
&keypair.secret_key(),
|
||||
&elgamal_keypair.public_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)?;
|
||||
blinded_signatures.push(blinded_signature)
|
||||
}
|
||||
|
||||
let unblinded_signatures: Vec<Signature> = blinded_signatures
|
||||
.into_iter()
|
||||
.zip(verification_keys.iter())
|
||||
.map(|(signature, verification_key)| {
|
||||
signature
|
||||
.unblind(
|
||||
¶ms,
|
||||
&elgamal_keypair.private_key(),
|
||||
&verification_key,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let signature_shares: Vec<SignatureShare> = unblinded_signatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
|
||||
.collect();
|
||||
|
||||
let mut attributes = Vec::with_capacity(1 + 9);
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
// Randomize credentials and generate any cryptographic material to verify them
|
||||
let signature =
|
||||
aggregate_signature_shares(¶ms, &verification_key, &attributes, &signature_shares)?;
|
||||
|
||||
// SHOW PROTOCOL
|
||||
let verifier_id = [11u8; 32];
|
||||
let timestamp = [12u8; 32];
|
||||
|
||||
let show_private_attributes = vec![
|
||||
user_secret,
|
||||
patient_id,
|
||||
full_name,
|
||||
vaccine_medication_product_id,
|
||||
country_of_vaccination,
|
||||
issuer,
|
||||
dob,
|
||||
];
|
||||
|
||||
// Prove covid credential
|
||||
let theta_covid = prove_covid_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&signature,
|
||||
&show_private_attributes,
|
||||
&verifier_id,
|
||||
×tamp,
|
||||
)?;
|
||||
|
||||
let theta_covid_bytes = theta_covid.to_bytes();
|
||||
println!("Length of theta in bytes: {:?}", theta_covid_bytes.len());
|
||||
|
||||
let theta_covid_from_bytes = ThetaCovid::from_bytes(&*theta_covid_bytes).unwrap();
|
||||
|
||||
// Verify covid credentials
|
||||
let disclosed_attributes = vec![is_vaccinated, is_over_18, is_over_21];
|
||||
assert!(verify_covid_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&theta_covid_from_bytes,
|
||||
disclosed_attributes.as_ref(),
|
||||
&verifier_id,
|
||||
×tamp,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
mod e2e;
|
||||
mod e2e_covid;
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::CoconutError;
|
||||
|
||||
pub trait Bytable
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn to_byte_vec(&self) -> Vec<u8>;
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError>;
|
||||
}
|
||||
|
||||
pub trait Base58
|
||||
where
|
||||
Self: Bytable,
|
||||
{
|
||||
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
|
||||
Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap())
|
||||
}
|
||||
fn to_bs58(&self) -> String {
|
||||
bs58::encode(self.to_byte_vec()).into_string()
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
|
||||
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
|
||||
pub struct Polynomial {
|
||||
coefficients: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl Polynomial {
|
||||
// for polynomial of degree n, we generate n+1 values
|
||||
// (for example for degree 1, like y = x + 2, we need [2,1])
|
||||
pub fn new_random(params: &Parameters, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: params.n_random_scalars((degree + 1) as usize),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the polynomial at point x.
|
||||
pub fn evaluate(&self, x: &Scalar) -> Scalar {
|
||||
if self.coefficients.is_empty() {
|
||||
Scalar::zero()
|
||||
// if x is zero then we can ignore most of the expensive computation and
|
||||
// just return the last term of the polynomial
|
||||
} else if x.is_zero() {
|
||||
// we checked that coefficients are not empty so unwrap here is fine
|
||||
*self.coefficients.first().unwrap()
|
||||
} else {
|
||||
self.coefficients
|
||||
.iter()
|
||||
.enumerate()
|
||||
// coefficient[n] * x ^ n
|
||||
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
|
||||
let x = Scalar::zero();
|
||||
|
||||
points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, point_i)| {
|
||||
let mut numerator = Scalar::one();
|
||||
let mut denominator = Scalar::one();
|
||||
let xi = Scalar::from(*point_i);
|
||||
|
||||
for (j, point_j) in points.iter().enumerate() {
|
||||
if j != i {
|
||||
let xj = Scalar::from(*point_j);
|
||||
|
||||
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
|
||||
numerator *= x - xj;
|
||||
|
||||
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
|
||||
denominator *= xi - xj;
|
||||
}
|
||||
}
|
||||
// numerator / denominator
|
||||
numerator * denominator.invert().unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
|
||||
/// It can be used for Scalars, G1 and G2 points.
|
||||
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
|
||||
points: &[SignerIndex],
|
||||
values: &[T],
|
||||
) -> Result<T>
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
if points.is_empty() || values.is_empty() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if points.len() != values.len() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let coefficients = generate_lagrangian_coefficients_at_origin(points);
|
||||
|
||||
Ok(coefficients
|
||||
.into_iter()
|
||||
.zip(values.iter())
|
||||
.map(|(coeff, val)| val * coeff)
|
||||
.sum())
|
||||
}
|
||||
|
||||
// A temporary way of hashing particular message into G1.
|
||||
// Implementation idea was taken from `threshold_crypto`:
|
||||
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
|
||||
// Eventually it should get replaced by, most likely, the osswu map
|
||||
// method once ideally it's implemented inside the pairing crate.
|
||||
|
||||
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
|
||||
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
|
||||
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
|
||||
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
|
||||
|
||||
pub(crate) fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
|
||||
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
|
||||
}
|
||||
|
||||
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
|
||||
let mut output = vec![Scalar::zero()];
|
||||
|
||||
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
|
||||
msg.as_ref(),
|
||||
SCALAR_HASH_DOMAIN,
|
||||
&mut output,
|
||||
);
|
||||
output[0]
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_scalar_vec(
|
||||
expected_len: u64,
|
||||
bytes: &[u8],
|
||||
err: CoconutError,
|
||||
) -> Result<Vec<Scalar>> {
|
||||
if bytes.len() != expected_len as usize * 32 {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(expected_len as usize);
|
||||
for i in 0..expected_len as usize {
|
||||
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
|
||||
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
|
||||
None => return Err(err),
|
||||
Some(scalar) => scalar,
|
||||
};
|
||||
out.push(s)
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
|
||||
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_g1_projective(
|
||||
bytes: &[u8; 48],
|
||||
err: CoconutError,
|
||||
) -> Result<G1Projective> {
|
||||
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G1Projective::from)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_g2_projective(
|
||||
bytes: &[u8; 96],
|
||||
err: CoconutError,
|
||||
) -> Result<G2Projective> {
|
||||
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G2Projective::from)
|
||||
}
|
||||
|
||||
// use core::fmt;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::de::Visitor;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
//
|
||||
// // #[cfg(feature = "serde")]
|
||||
// #[serde(remote = "Scalar")]
|
||||
// pub(crate) struct ScalarDef(pub Scalar);
|
||||
//
|
||||
// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
//
|
||||
// impl Serialize for ScalarDef {
|
||||
// fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer,
|
||||
// {
|
||||
// use serde::ser::SerializeTuple;
|
||||
// let mut tup = serializer.serialize_tuple(32)?;
|
||||
// for byte in self.0.to_bytes().iter() {
|
||||
// tup.serialize_element(byte)?;
|
||||
// }
|
||||
// tup.end()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl<'de> Deserialize<'de> for ScalarDef {
|
||||
// fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
||||
// where
|
||||
// D: Deserializer<'de>,
|
||||
// {
|
||||
// struct ScalarVisitor;
|
||||
//
|
||||
// impl<'de> Visitor<'de> for ScalarVisitor {
|
||||
// type Value = ScalarDef;
|
||||
//
|
||||
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
// formatter.write_str("a 32-byte canonical bls12_381 scalar")
|
||||
// }
|
||||
//
|
||||
// fn visit_seq<A>(self, mut seq: A) -> core::result::Result<ScalarDef, A::Error>
|
||||
// where
|
||||
// A: serde::de::SeqAccess<'de>,
|
||||
// {
|
||||
// let mut bytes = [0u8; 32];
|
||||
// for i in 0..32 {
|
||||
// bytes[i] = seq
|
||||
// .next_element()?
|
||||
// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?;
|
||||
// }
|
||||
//
|
||||
// let res = Scalar::from_bytes(&bytes);
|
||||
// if res.is_some().into() {
|
||||
// Ok(ScalarDef(res.unwrap()))
|
||||
// } else {
|
||||
// Err(serde::de::Error::custom(
|
||||
// &"scalar was not canonically encoded",
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// deserializer.deserialize_tuple(32, ScalarVisitor)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G1ProjectiveSerdeHelper(Scalar);
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G2ProjectiveSerdeHelper(Scalar);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::RngCore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn polynomial_evaluation() {
|
||||
// y = 42 (it should be 42 regardless of x)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(42)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
|
||||
|
||||
// y = x + 10, at x = 2 (exp: 12)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(10), Scalar::from(1)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
|
||||
|
||||
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![
|
||||
(-Scalar::from(3)),
|
||||
Scalar::from(2),
|
||||
(-Scalar::from(5)),
|
||||
Scalar::zero(),
|
||||
Scalar::from(1),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
|
||||
|
||||
// empty polynomial
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![],
|
||||
};
|
||||
|
||||
// should always be 0
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn performing_lagrangian_scalar_interpolation_at_origin() {
|
||||
// x^2 + 3
|
||||
// x, f(x):
|
||||
// 1, 4,
|
||||
// 2, 7,
|
||||
// 3, 12,
|
||||
let points = vec![1, 2, 3];
|
||||
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(3),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// x^3 + 3x^2 - 5x + 11
|
||||
// x, f(x):
|
||||
// 1, 10
|
||||
// 2, 21
|
||||
// 3, 50
|
||||
// 4, 103
|
||||
let points = vec![1, 2, 3, 4];
|
||||
let values = vec![
|
||||
Scalar::from(10),
|
||||
Scalar::from(21),
|
||||
Scalar::from(50),
|
||||
Scalar::from(103),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(11),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// more points than it is required
|
||||
// x^2 + x + 10
|
||||
// x, f(x)
|
||||
// 1, 12
|
||||
// 2, 16
|
||||
// 3, 22
|
||||
// 4, 30
|
||||
// 5, 40
|
||||
let points = vec![1, 2, 3, 4, 5];
|
||||
let values = vec![
|
||||
Scalar::from(12),
|
||||
Scalar::from(16),
|
||||
Scalar::from(22),
|
||||
Scalar::from(30),
|
||||
Scalar::from(40),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(10),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_g1_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_g1(msg1), hash_g1(msg1));
|
||||
assert_eq!(hash_g1(msg2), hash_g1(msg2));
|
||||
assert_ne!(hash_g1(msg1), hash_g1(msg2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_scalar_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
|
||||
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
|
||||
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/env", "@babel/react"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# See http://editorconfig.org/
|
||||
# EditorConfig helps developers define and maintain consistent coding styles
|
||||
# between different editors and IDEs. The EditorConfig project consists of a
|
||||
# file format for defining coding styles and a collection of text editor plugins
|
||||
# that enable editors to read the file format and adhere to defined styles.
|
||||
# EditorConfig files are easily readable and they work nicely with version
|
||||
# control systems.
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
|
||||
"extends": ["plugin:react/recommended", "airbnb", "prettier", "plugin:jest/recommended", "plugin:jest/style"],
|
||||
"rules": {
|
||||
"jest/prefer-strict-equal": "error",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"prettier/prettier": "error",
|
||||
"import/prefer-default-export": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-filename-extension": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"**/*.test.[jt]s",
|
||||
"**/*.spec.[jt]s",
|
||||
"**/*.test.[jt]sx",
|
||||
"**/*.spec.[jt]sx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never",
|
||||
"tsx": "never",
|
||||
"js": "never",
|
||||
"jsx": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.+(ts|tsx)",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint/eslint-plugin"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"no-use-before-define": [0],
|
||||
"@typescript-eslint/no-use-before-define": [1],
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"quotes": "off",
|
||||
"@typescript-eslint/quotes": [
|
||||
2,
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"root-import": {
|
||||
"rootPathPrefix": "@",
|
||||
"rootPathSuffix": "src",
|
||||
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
.DS_Store
|
||||
detailAPI.md
|
||||
@@ -0,0 +1 @@
|
||||
14
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Nym Network Explorer
|
||||
|
||||
The network explorer lets you explore the Nym network.
|
||||
|
||||
## Getting started
|
||||
|
||||
You will need:
|
||||
|
||||
- NodeJS (use `nvm install` to automatically install the correct version)
|
||||
- `npm`
|
||||
|
||||
From the `explorer` directory of the `nym` monorepo, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
You can then open a browser to http://localhost:3000 and start development.
|
||||
|
||||
## Development
|
||||
|
||||
Documentation for developers [can be found here](./docs).
|
||||
|
||||
## Deployment
|
||||
|
||||
Build the UI with:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The output will be in the `dist` directory. Serve this with `nginx` or `httpd`.
|
||||
|
||||
Make sure you have built the [explorer-api](./explorer-api) and are running it as a service proxied through
|
||||
`nginx` or `httpd` so that both the UI and API are running on the same host.
|
||||
|
||||
## License
|
||||
|
||||
Please see the [project README](./README.md) and the [LICENSES directory](../LICENSES) for license details for all Nym software.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you would like to contribute to the Network Explorer send us a PR or
|
||||
[raise an issue on GitHub](https://github.com/nymtech/nym/issues) and tag them with `network-explorer`.
|
||||
|
||||
## Development
|
||||
|
||||
Please see [development docs](./docs) here for more information on the structure and design of this app.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Nym Network Explorer - Development Docs
|
||||
|
||||
## Getting started
|
||||
|
||||
You will need:
|
||||
|
||||
- NodeJS
|
||||
- `nvm`
|
||||
|
||||
We use the following:
|
||||
|
||||
- Typescript
|
||||
- `eslint`
|
||||
- `webpack`
|
||||
- `jest`
|
||||
- `react-material-ui`
|
||||
- `react` 17
|
||||
|
||||
## Development mode
|
||||
|
||||
Run the following:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
A development server with hot reloading will be running on http://localhost:3000.
|
||||
|
||||
## Linting
|
||||
|
||||
`eslint` and `prettier` are configured.
|
||||
|
||||
You can lint the code by running:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
> **Note:** this will only show linting errors and will not fix them
|
||||
|
||||
To fix all linting errors automatically run:
|
||||
|
||||
```
|
||||
npm run lint:fix
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
Generated
+30149
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "@nym/network-explorer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@cosmjs/math": "^0.26.2",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/icons-material": "^5.0.0",
|
||||
"@mui/material": "^5.0.1",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@mui/system": "^5.0.1",
|
||||
"@mui/x-data-grid": "^5.0.0-beta.2",
|
||||
"@nymproject/nym-validator-client": "^0.18.0",
|
||||
"d3-scale": "^4.0.0",
|
||||
"date-fns": "^2.24.0",
|
||||
"i18n-iso-countries": "^6.8.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-google-charts": "^3.0.15",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-tooltip": "^4.2.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.3",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/d3-fetch": "^3.0.1",
|
||||
"@types/d3-geo": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.1",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^17.0.34",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-simple-maps": "^1.0.6",
|
||||
"@types/react-tooltip": "^4.2.4",
|
||||
"@types/topojson-client": "^3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-airbnb": "18.2.1",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-import-resolver-root-import": "1.0.4",
|
||||
"eslint-plugin-import": "2.24.2",
|
||||
"eslint-plugin-jest": "^24.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-react": "7.25.1",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"imports-loader": "^3.0.0",
|
||||
"jest": "^27.1.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "2.3.2",
|
||||
"react-refresh": "^0.10.0",
|
||||
"react-refresh-typescript": "^2.0.2",
|
||||
"style-loader": "^3.2.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.4.2",
|
||||
"webpack": "^5.52.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.1.0",
|
||||
"webpack-favicons": "^1.0.7",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"yaml-loader": "^0.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --progress --port 3000",
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
"build:serve": "npx serve dist",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tsc": "tsc",
|
||||
"tsc:watch": "tsc --watch",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { Nav } from './components/Nav';
|
||||
import { Routes } from './routes/index';
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<Nav>
|
||||
<Routes />
|
||||
</Nav>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
// master APIs
|
||||
export const MASTER_URL = 'https://testnet-milhon-explorer.nymtech.net';
|
||||
export const MASTER_VALIDATOR_URL =
|
||||
'https://testnet-milhon-validator1.nymtech.net';
|
||||
export const BIG_DIPPER = 'https://testnet-milhon-blocks.nymtech.net';
|
||||
|
||||
// specific API routes
|
||||
export const MIXNODE_PING = `${MASTER_URL}/api/ping`;
|
||||
export const MIXNODES_API = `${MASTER_URL}/api/mix-node`;
|
||||
export const GATEWAYS_API = `${MASTER_VALIDATOR_URL}/api/v1/gateways`;
|
||||
export const VALIDATORS_API = `${MASTER_VALIDATOR_URL}/validators`;
|
||||
export const BLOCK_API = `${MASTER_VALIDATOR_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${MASTER_URL}/api/countries`;
|
||||
export const UPTIME_STORY_API = `${MASTER_VALIDATOR_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR =
|
||||
"We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
// socials
|
||||
export const TELEGRAM_LINK = 'https://t.me/nymchan';
|
||||
export const TWITTER_LINK = 'https://twitter.com/nymproject';
|
||||
export const GITHUB_LINK = 'https://github.com/nymtech';
|
||||
export const NYM_WEBSITE = 'https://nymtech.net';
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
GATEWAYS_API,
|
||||
MIXNODES_API,
|
||||
VALIDATORS_API,
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
MIXNODE_PING,
|
||||
UPTIME_STORY_API,
|
||||
} from './constants';
|
||||
|
||||
import {
|
||||
MixNodeResponse,
|
||||
GatewayResponse,
|
||||
ValidatorsResponse,
|
||||
CountryDataResponse,
|
||||
MixNodeResponseItem,
|
||||
DelegationsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
function getFromCache(key: string) {
|
||||
const ts = Number(localStorage.getItem('ts'));
|
||||
const hasExpired = Date.now() - ts > 200000;
|
||||
const curr = localStorage.getItem(key);
|
||||
if (curr && !hasExpired) {
|
||||
return JSON.parse(curr);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function storeInCache(key: string, data: any) {
|
||||
localStorage.setItem(key, data);
|
||||
}
|
||||
export class Api {
|
||||
static fetchMixnodes = async (): Promise<MixNodeResponse> => {
|
||||
const cachedMixnodes = getFromCache('mixnodes');
|
||||
if (cachedMixnodes) {
|
||||
return cachedMixnodes;
|
||||
}
|
||||
const res = await fetch(MIXNODES_API);
|
||||
const json = await res.json();
|
||||
storeInCache('mixnodes', JSON.stringify(json));
|
||||
storeInCache('ts', Date.now());
|
||||
return json;
|
||||
};
|
||||
|
||||
static fetchMixnodeByID = async (
|
||||
id: string,
|
||||
): Promise<MixNodeResponseItem | undefined> => {
|
||||
const allMixnodes: MixNodeResponse = await Api.fetchMixnodes();
|
||||
const matchedByID = allMixnodes.filter(
|
||||
(eachRecord) => eachRecord.mix_node.identity_key === id,
|
||||
);
|
||||
return (matchedByID.length && matchedByID[0]) || undefined;
|
||||
};
|
||||
|
||||
static fetchGateways = async (): Promise<GatewayResponse> => {
|
||||
const res = await fetch(GATEWAYS_API);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
return json.result;
|
||||
};
|
||||
|
||||
static fetchBlock = async (): Promise<number> => {
|
||||
const res = await fetch(BLOCK_API);
|
||||
const json = await res.json();
|
||||
const { height } = json.result.block.header;
|
||||
return height;
|
||||
};
|
||||
|
||||
static fetchCountryData = async (): Promise<CountryDataResponse> => {
|
||||
const result: CountryDataResponse = {};
|
||||
const res = await fetch(COUNTRY_DATA_API);
|
||||
const json = await res.json();
|
||||
Object.keys(json).forEach((ISO3) => {
|
||||
result[ISO3] = { ISO3, nodes: json[ISO3] };
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
static fetchDelegationsById = async (
|
||||
id: string,
|
||||
): Promise<DelegationsResponse> =>
|
||||
(await fetch(`${MIXNODES_API}/${id}/delegations`)).json();
|
||||
|
||||
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
|
||||
(await fetch(`${MIXNODES_API}/${id}/stats`)).json();
|
||||
|
||||
static fetchStatusById = async (id: string): Promise<StatusResponse> =>
|
||||
(await fetch(`${MIXNODE_PING}/${id}`)).json();
|
||||
|
||||
static fetchUptimeStoryById = async (
|
||||
id: string,
|
||||
): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
||||
import * as React from 'react';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Alert, CircularProgress, useMediaQuery, Box } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { ExpandMore } from '@mui/icons-material';
|
||||
|
||||
export const BondBreakdownTable: React.FC = () => {
|
||||
const { mixnodeDetailInfo, delegations } = useMainContext();
|
||||
const [allContentLoaded, setAllContentLoaded] =
|
||||
React.useState<boolean>(false);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [showDelegations, toggleShowDelegations] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const [bonds, setBonds] = React.useState({
|
||||
delegations: '0',
|
||||
pledges: '0',
|
||||
bondsTotal: '0',
|
||||
hasLoaded: false,
|
||||
});
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mixnodeDetailInfo && mixnodeDetailInfo.data?.length) {
|
||||
const thisMixnode = mixnodeDetailInfo?.data[0];
|
||||
|
||||
// delegations
|
||||
const decimalisedDelegations = printableCoin({
|
||||
amount: thisMixnode.total_delegation.amount.toString(),
|
||||
denom: thisMixnode.total_delegation.denom,
|
||||
});
|
||||
|
||||
// pledges
|
||||
const decimalisedPledges = printableCoin({
|
||||
amount: thisMixnode.bond_amount.amount.toString(),
|
||||
denom: thisMixnode.bond_amount.denom,
|
||||
});
|
||||
|
||||
// bonds total (del + pledges)
|
||||
const pledgesSum = Number(thisMixnode.bond_amount.amount);
|
||||
const delegationsSum = Number(thisMixnode.total_delegation.amount);
|
||||
const bondsTotal = printableCoin({
|
||||
amount: (delegationsSum + pledgesSum).toString(),
|
||||
denom: 'upunk',
|
||||
});
|
||||
|
||||
setBonds({
|
||||
delegations: decimalisedDelegations,
|
||||
pledges: decimalisedPledges,
|
||||
bondsTotal,
|
||||
hasLoaded: true,
|
||||
});
|
||||
}
|
||||
}, [mixnodeDetailInfo]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const hasError = Boolean(mixnodeDetailInfo?.error || delegations?.error);
|
||||
const hasAllMixnodeInfo = Boolean(
|
||||
mixnodeDetailInfo?.data !== undefined &&
|
||||
mixnodeDetailInfo?.data[0].mix_node,
|
||||
);
|
||||
const hasAllDelegationsInfo = Boolean(
|
||||
delegations?.data !== undefined && delegations?.data,
|
||||
);
|
||||
const hasAllData = Boolean(
|
||||
!hasError && hasAllMixnodeInfo && hasAllDelegationsInfo,
|
||||
);
|
||||
setShowError(hasError);
|
||||
setAllContentLoaded(hasAllData);
|
||||
}, [mixnodeDetailInfo, delegations]);
|
||||
|
||||
const expandDelegations = () => {
|
||||
if (delegations?.data && delegations.data.length > 0) {
|
||||
toggleShowDelegations(!showDelegations);
|
||||
}
|
||||
};
|
||||
const calcBondPercentage = (num: number) => {
|
||||
if (mixnodeDetailInfo?.data !== undefined && mixnodeDetailInfo?.data[0]) {
|
||||
const rawDeligationAmount = Number(
|
||||
mixnodeDetailInfo.data[0].total_delegation.amount,
|
||||
);
|
||||
const rawPledgeAmount = Number(
|
||||
mixnodeDetailInfo.data[0].bond_amount.amount,
|
||||
);
|
||||
const rawTotalBondsAmount = rawDeligationAmount + rawPledgeAmount;
|
||||
return ((num * 100) / rawTotalBondsAmount).toFixed(1);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (mixnodeDetailInfo?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (showError) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
We are unable to retrieve a Mixnode with that ID. Please try later or
|
||||
Contact Us.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showError && allContentLoaded) {
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
|
||||
<TableBody>
|
||||
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
width: '150px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Bond total
|
||||
</TableCell>
|
||||
<TableCell align="left">{bonds.bondsTotal}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Pledge total</TableCell>
|
||||
<TableCell align="left">{bonds.pledges}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell onClick={expandDelegations} align="left">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Delegation total {'\u00A0'}
|
||||
{delegations?.data && delegations?.data?.length > 0 && (
|
||||
<ExpandMore />
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="left">{bonds.delegations}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{showDelegations && (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 400,
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ fontWeight: 'bold', background: '#242C3D' }}
|
||||
align="left"
|
||||
>
|
||||
Delegators
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ fontWeight: 'bold', background: '#242C3D' }}
|
||||
align="left"
|
||||
>
|
||||
Stake
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
background: '#242C3D',
|
||||
width: '200px',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Share from bond
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{delegations?.data?.map(
|
||||
({ owner, amount: { amount, denom } }) => (
|
||||
<TableRow key={owner}>
|
||||
<TableCell
|
||||
sx={matches ? { width: 190 } : null}
|
||||
align="left"
|
||||
>
|
||||
{owner}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{printableCoin({ amount: amount.toString(), denom })}
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
{calcBondPercentage(amount)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
export const ComponentError: React.FC<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }}
|
||||
variant="body1"
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Card, CardHeader, CardContent, Typography } from '@mui/material';
|
||||
import React, { ReactEventHandler } from 'react';
|
||||
|
||||
type ContentCardProps = {
|
||||
title?: string | React.ReactNode;
|
||||
subtitle?: string;
|
||||
Icon?: React.ReactNode;
|
||||
Action?: React.ReactNode;
|
||||
errorMsg?: string;
|
||||
onClick?: ReactEventHandler;
|
||||
};
|
||||
|
||||
export const ContentCard: React.FC<ContentCardProps> = ({
|
||||
title,
|
||||
Icon,
|
||||
Action,
|
||||
subtitle,
|
||||
errorMsg,
|
||||
children,
|
||||
onClick,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
{title && (
|
||||
<CardHeader
|
||||
title={title || ''}
|
||||
avatar={Icon}
|
||||
action={Action}
|
||||
subheader={subtitle}
|
||||
/>
|
||||
)}
|
||||
{children && <CardContent>{children}</CardContent>}
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{errorMsg}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
ContentCard.defaultProps = {
|
||||
title: undefined,
|
||||
subtitle: undefined,
|
||||
Icon: null,
|
||||
Action: null,
|
||||
errorMsg: undefined,
|
||||
onClick: () => null,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
|
||||
export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
|
||||
headingTitle,
|
||||
}) => {
|
||||
const [filter, toggleFilter] = React.useState<boolean>(false);
|
||||
|
||||
const handleClick = () => {
|
||||
toggleFilter(!filter);
|
||||
};
|
||||
return (
|
||||
<Box alignItems="center" display="flex" onClick={handleClick}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{headingTitle}
|
||||
</Typography>
|
||||
{filter ? <ExpandMore /> : <ExpandLess />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Socials } from './Socials';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
mt: 3,
|
||||
pt: 3,
|
||||
pb: 3,
|
||||
}}
|
||||
>
|
||||
{isMobile && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Socials isFooter />
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
textAlign: isMobile ? 'center' : 'end',
|
||||
color: theme.palette.nym.text.footer,
|
||||
}}
|
||||
>
|
||||
© 2021 Nym Technologies SA, all rights reserved
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,397 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronLeft, ExpandLess, ExpandMore, Menu } from '@mui/icons-material';
|
||||
import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles';
|
||||
import MuiLink from '@mui/material/Link';
|
||||
import Box from '@mui/material/Box';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import MuiDrawer from '@mui/material/Drawer';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import List from '@mui/material/List';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
|
||||
import { BIG_DIPPER, NYM_WEBSITE } from 'src/api/constants';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
import { OverviewSVG } from '../icons/OverviewSVG';
|
||||
import { NetworkComponentsSVG } from '../icons/NetworksSVG';
|
||||
import { NodemapSVG } from '../icons/NodemapSVG';
|
||||
import { Socials } from './Socials';
|
||||
import { Footer } from './Footer';
|
||||
import { DarkLightSwitchDesktop, DarkLightSwitchMobile } from './Switch';
|
||||
|
||||
const drawerWidth = 300;
|
||||
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
});
|
||||
|
||||
const closedMixin = (theme: Theme): CSSObject => ({
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||
});
|
||||
|
||||
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
height: 64,
|
||||
}));
|
||||
|
||||
const Drawer = styled(MuiDrawer, {
|
||||
shouldForwardProp: (prop) => prop !== 'open',
|
||||
})(({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme),
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme),
|
||||
}),
|
||||
}));
|
||||
|
||||
type navOptionType = {
|
||||
id: number;
|
||||
isActive?: boolean;
|
||||
url: string;
|
||||
title: string;
|
||||
Icon?: React.ReactNode;
|
||||
nested?: navOptionType[];
|
||||
isExpandedChild?: boolean;
|
||||
};
|
||||
|
||||
const originalNavOptions: navOptionType[] = [
|
||||
{
|
||||
id: 0,
|
||||
isActive: false,
|
||||
url: '/overview',
|
||||
title: 'Overview',
|
||||
Icon: <OverviewSVG />,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
isActive: false,
|
||||
url: '/network-components',
|
||||
title: 'Network Components',
|
||||
Icon: <NetworkComponentsSVG />,
|
||||
nested: [
|
||||
{
|
||||
id: 3,
|
||||
url: '/network-components/mixnodes',
|
||||
title: 'Mixnodes',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: '/network-components/gateways',
|
||||
title: 'Gateways',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: `${BIG_DIPPER}/validators`,
|
||||
title: 'Validators',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
isActive: false,
|
||||
url: '/nodemap',
|
||||
title: 'Nodemap',
|
||||
Icon: <NodemapSVG />,
|
||||
},
|
||||
];
|
||||
|
||||
type ExpandableButtonType = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
isActive?: boolean;
|
||||
Icon?: React.ReactNode;
|
||||
nested?: navOptionType[];
|
||||
isChild?: boolean;
|
||||
openDrawer: () => void;
|
||||
closeDrawer: () => void;
|
||||
drawIsOpen: boolean;
|
||||
setToActive: (num: number) => void;
|
||||
};
|
||||
|
||||
const ExpandableButton: React.FC<ExpandableButtonType> = ({
|
||||
id,
|
||||
url,
|
||||
setToActive,
|
||||
isActive,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
drawIsOpen,
|
||||
Icon,
|
||||
title,
|
||||
nested,
|
||||
isChild,
|
||||
}) => {
|
||||
const [dynamicStyle, setDynamicStyle] = React.useState({});
|
||||
const [nestedOptions, toggleNestedOptions] = React.useState(false);
|
||||
const [isExternal, setIsExternal] = React.useState<boolean>(false);
|
||||
const { palette } = useTheme();
|
||||
|
||||
const handleClick = () => {
|
||||
openDrawer();
|
||||
if (title === 'Network Components' && nested) {
|
||||
toggleNestedOptions(!nestedOptions);
|
||||
}
|
||||
setToActive(id);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (url) {
|
||||
setIsExternal(url.includes('http'));
|
||||
}
|
||||
if (nested) {
|
||||
setDynamicStyle({
|
||||
background: '#242C3D',
|
||||
borderRight: `3px solid ${palette.nym.highlight}`,
|
||||
});
|
||||
}
|
||||
if (isChild) {
|
||||
setDynamicStyle({
|
||||
background: '#3C4558',
|
||||
fontWeight: 800,
|
||||
});
|
||||
}
|
||||
if (!nested && !isChild) {
|
||||
setDynamicStyle({
|
||||
background: '#242C3D',
|
||||
borderRight: `3px solid ${palette.nym.highlight}`,
|
||||
});
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!drawIsOpen && nestedOptions) {
|
||||
toggleNestedOptions(false);
|
||||
}
|
||||
}, [drawIsOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
component={!nested ? Link : 'div'}
|
||||
to={isExternal ? { pathname: url } : url}
|
||||
target={isExternal ? '_blank' : ''}
|
||||
sx={
|
||||
isActive
|
||||
? dynamicStyle
|
||||
: { background: '#111826', borderRight: 'none' }
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
background: isChild ? '#3C4558' : 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={title}
|
||||
sx={{
|
||||
color: palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
fontWeight: isActive ? 800 : 300,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{nested && nestedOptions && <ExpandLess />}
|
||||
{nested && !nestedOptions && <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{nestedOptions &&
|
||||
nested?.map((each) => (
|
||||
<ExpandableButton
|
||||
id={each.id}
|
||||
url={each.url}
|
||||
key={each.title}
|
||||
title={each.title}
|
||||
openDrawer={openDrawer}
|
||||
drawIsOpen={drawIsOpen}
|
||||
closeDrawer={closeDrawer}
|
||||
setToActive={setToActive}
|
||||
isChild
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandableButton.defaultProps = {
|
||||
Icon: null,
|
||||
nested: undefined,
|
||||
isChild: false,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export const Nav: React.FC = ({ children }) => {
|
||||
const [navOptionsState, updateNavOptionsState] =
|
||||
React.useState(originalNavOptions);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const setToActive = (id: number) => {
|
||||
const updated = navOptionsState.map((option) => ({
|
||||
...option,
|
||||
isActive: option.id === id,
|
||||
}));
|
||||
updateNavOptionsState(updated);
|
||||
};
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
disableGutters
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: 205,
|
||||
}}
|
||||
>
|
||||
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
|
||||
<NymLogoSVG />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
sx={{
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
fontSize: '18px',
|
||||
}}
|
||||
>
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to="/overview"
|
||||
underline="none"
|
||||
color="inherit"
|
||||
>
|
||||
Network Explorer
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
mr: 2,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
pr: 0,
|
||||
pl: 2,
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Socials />
|
||||
<DarkLightSwitchDesktop defaultChecked />
|
||||
</Box>
|
||||
)}
|
||||
{isMobile && <DarkLightSwitchMobile />}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={open}
|
||||
sx={{
|
||||
background: theme.palette.nym.networkExplorer.nav.background,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader
|
||||
sx={{
|
||||
justifyContent: open ? 'flex-end' : 'center',
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={open ? handleDrawerClose : handleDrawerOpen}
|
||||
sx={{
|
||||
padding: 0,
|
||||
ml: '7px',
|
||||
color: theme.palette.nym.networkExplorer.nav.text,
|
||||
}}
|
||||
>
|
||||
{open ? <ChevronLeft /> : <Menu />}
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
|
||||
<List sx={{ pt: 0, pb: 0 }}>
|
||||
{navOptionsState.map((props) => (
|
||||
<ExpandableButton
|
||||
setToActive={setToActive}
|
||||
key={props.url}
|
||||
openDrawer={handleDrawerOpen}
|
||||
drawIsOpen={open}
|
||||
closeDrawer={handleDrawerClose}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box sx={{ width: '100%', p: 4, mt: 7 }}>
|
||||
{children}
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import TwitterIcon from '@mui/icons-material/Twitter';
|
||||
import { GITHUB_LINK, TELEGRAM_LINK, TWITTER_LINK } from 'src/api/constants';
|
||||
|
||||
export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
|
||||
const theme = useTheme();
|
||||
const color = isFooter
|
||||
? theme.palette.nym.networkExplorer.footer.socialIcons
|
||||
: theme.palette.nym.networkExplorer.topNav.socialIcons;
|
||||
return (
|
||||
<Box>
|
||||
<IconButton component="a" href={TELEGRAM_LINK} target="_blank">
|
||||
<TelegramIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={GITHUB_LINK} target="_blank">
|
||||
<GitHubIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={TWITTER_LINK} target="_blank">
|
||||
<TwitterIcon sx={{ color }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Socials.defaultProps = {
|
||||
isFooter: false,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material';
|
||||
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
|
||||
|
||||
interface StatsCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
count: string | number;
|
||||
errorMsg?: Error | string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
export const StatsCard: React.FC<StatsCardProps> = ({
|
||||
icon,
|
||||
title,
|
||||
count,
|
||||
onClick,
|
||||
errorMsg,
|
||||
}) => (
|
||||
<Card onClick={onClick} sx={{ height: '100%' }}>
|
||||
<CardContent
|
||||
sx={{
|
||||
padding: 2,
|
||||
'&:last-child': {
|
||||
paddingBottom: 2,
|
||||
},
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
sx={{ color: (theme) => theme.palette.text.primary, fontSize: 18 }}
|
||||
>
|
||||
{icon}
|
||||
<Typography ml={3} mr={0.75} fontSize="inherit">
|
||||
{count}
|
||||
</Typography>
|
||||
<Typography mr={1} fontSize="inherit">
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton color="inherit">
|
||||
<ArrowRightAltIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
{errorMsg && (
|
||||
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
|
||||
{errorMsg}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
StatsCard.defaultProps = {
|
||||
onClick: undefined,
|
||||
errorMsg: undefined,
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { Button } from '@mui/material';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { LightSwitchSVG } from '../icons/LightSwitchSVG';
|
||||
|
||||
export const DarkLightSwitch = styled(Switch)(({ theme }) => ({
|
||||
width: 55,
|
||||
height: 34,
|
||||
padding: 7,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
margin: 1,
|
||||
padding: 2,
|
||||
transform: 'translateX(4px)',
|
||||
'&.Mui-checked': {
|
||||
color: '#fff',
|
||||
transform: 'translateX(22px)',
|
||||
'& .MuiSwitch-thumb:before': {
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')',
|
||||
},
|
||||
'& + .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
backgroundColor: theme.palette.nym.networkExplorer.nav.text,
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginTop: '2px',
|
||||
'&:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundImage:
|
||||
'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')',
|
||||
},
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
borderRadius: 20 / 2,
|
||||
},
|
||||
}));
|
||||
|
||||
export const DarkLightSwitchMobile: React.FC = () => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button onClick={() => toggleMode()}>
|
||||
<LightSwitchSVG />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const DarkLightSwitchDesktop: React.FC<{ defaultChecked: boolean }> = ({
|
||||
defaultChecked,
|
||||
}) => {
|
||||
const { toggleMode } = useMainContext();
|
||||
return (
|
||||
<Button onClick={() => toggleMode()}>
|
||||
<DarkLightSwitch defaultChecked={defaultChecked} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { Box, useMediaQuery, TextField, MenuItem } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
|
||||
type TableToolBarProps = {
|
||||
onChangeSearch: (arg: string) => void;
|
||||
onChangePageSize: (event: SelectChangeEvent<string>) => void;
|
||||
pageSize: string;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export const TableToolbar: React.FC<TableToolBarProps> = ({
|
||||
searchTerm,
|
||||
onChangeSearch,
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: 2,
|
||||
display: 'flex',
|
||||
flexDirection: matches ? 'column' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={pageSize}
|
||||
onChange={onChangePageSize}
|
||||
sx={{
|
||||
width: 200,
|
||||
marginBottom: matches ? 2 : 0,
|
||||
}}
|
||||
>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={30}>30</MenuItem>
|
||||
<MenuItem value={50}>50</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
sx={{ width: 350 }}
|
||||
value={searchTerm}
|
||||
placeholder="search"
|
||||
onChange={(event) => onChangeSearch(event.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
export const Title: React.FC<{ text: string }> = ({ text }) => (
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
import { CircularProgress, Typography } from '@mui/material';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
|
||||
interface TableProps {
|
||||
title?: string;
|
||||
icons?: boolean[];
|
||||
keys: string[];
|
||||
values: number[];
|
||||
marginBottom?: boolean;
|
||||
error?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const TwoColSmallTable: React.FC<TableProps> = ({
|
||||
loading,
|
||||
title,
|
||||
icons,
|
||||
keys,
|
||||
values,
|
||||
marginBottom,
|
||||
error,
|
||||
}) => (
|
||||
<>
|
||||
{title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>}
|
||||
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}
|
||||
>
|
||||
<Table aria-label="two col small table">
|
||||
<TableBody>
|
||||
{keys.map((each: string, i: number) => (
|
||||
<TableRow key={each}>
|
||||
{icons && (
|
||||
<TableCell>
|
||||
{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell sx={error ? { opacity: 0.4 } : null}>{each}</TableCell>
|
||||
<TableCell sx={error ? { opacity: 0.4 } : null} align="right">
|
||||
{values[i]}
|
||||
</TableCell>
|
||||
{error && (
|
||||
<TableCell align="right" sx={{ opacity: 0.4 }}>
|
||||
{values[i]}
|
||||
</TableCell>
|
||||
)}
|
||||
{!error && loading && (
|
||||
<TableCell align="right">
|
||||
<CircularProgress />
|
||||
</TableCell>
|
||||
)}
|
||||
{error && !icons && (
|
||||
<TableCell sx={{ opacity: 0.2 }} align="right">
|
||||
<ErrorIcon />
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
TwoColSmallTable.defaultProps = {
|
||||
title: undefined,
|
||||
icons: undefined,
|
||||
marginBottom: false,
|
||||
error: undefined,
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as React from 'react';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import {
|
||||
DataGrid,
|
||||
GridColumns,
|
||||
GridRowData,
|
||||
GridSortModel,
|
||||
useGridSlotComponentProps,
|
||||
} from '@mui/x-data-grid';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import { SxProps } from '@mui/system';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
});
|
||||
|
||||
type DataGridProps = {
|
||||
loading?: boolean;
|
||||
rows: GridRowData[];
|
||||
columnsData: GridColumns;
|
||||
pageSize?: string;
|
||||
pagination?: boolean;
|
||||
hideFooter?: boolean;
|
||||
sortModel?: GridSortModel;
|
||||
};
|
||||
|
||||
export const cellStyles: SxProps = {
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
maxHeight: 100,
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 400,
|
||||
fontSize: 12,
|
||||
lineHeight: 2,
|
||||
textAlign: 'start',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'break-spaces',
|
||||
};
|
||||
|
||||
function CustomPagination() {
|
||||
const { state, apiRef } = useGridSlotComponentProps();
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Pagination
|
||||
className={classes.root}
|
||||
color="primary"
|
||||
count={state.pagination.pageCount}
|
||||
page={state.pagination.page + 1}
|
||||
onChange={(event, value) => apiRef.current.setPage(value - 1)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const UniversalDataGrid: React.FC<DataGridProps> = ({
|
||||
loading,
|
||||
rows,
|
||||
columnsData,
|
||||
pageSize,
|
||||
pagination,
|
||||
hideFooter,
|
||||
sortModel,
|
||||
}) => {
|
||||
const [sortModelState, setSortModelState] = React.useState<
|
||||
GridSortModel | undefined
|
||||
>(sortModel);
|
||||
if (columnsData && rows) {
|
||||
return (
|
||||
<DataGrid
|
||||
pagination
|
||||
components={{
|
||||
Pagination: CustomPagination,
|
||||
}}
|
||||
loading={loading}
|
||||
columns={columnsData}
|
||||
rows={rows}
|
||||
pageSize={Number(pageSize)}
|
||||
rowsPerPageOptions={[5]}
|
||||
hideFooterPagination={!pagination}
|
||||
disableColumnFilter
|
||||
disableColumnMenu
|
||||
disableSelectionOnClick
|
||||
columnBuffer={0}
|
||||
autoHeight
|
||||
hideFooter={hideFooter}
|
||||
sortModel={sortModelState}
|
||||
onSortModelChange={setSortModelState}
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
UniversalDataGrid.defaultProps = {
|
||||
loading: false,
|
||||
pageSize: undefined,
|
||||
pagination: false,
|
||||
hideFooter: true,
|
||||
sortModel: undefined,
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from 'react';
|
||||
import { CircularProgress, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Chart } from 'react-google-charts';
|
||||
import { ApiState, UptimeStoryResponse } from 'src/typeDefs/explorer-api';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ChartProps {
|
||||
title?: string;
|
||||
xLabel: string;
|
||||
yLabel: string;
|
||||
uptimeStory: ApiState<UptimeStoryResponse>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type FormattedDateRecord = [string, number, number];
|
||||
type FormattedChartHeadings = string[];
|
||||
type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord];
|
||||
|
||||
export const UptimeChart: React.FC<ChartProps> = ({
|
||||
title,
|
||||
xLabel,
|
||||
yLabel,
|
||||
uptimeStory,
|
||||
loading,
|
||||
}) => {
|
||||
const [formattedChartData, setFormattedChartData] =
|
||||
React.useState<FormattedChartData>();
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
React.useEffect(() => {
|
||||
if (uptimeStory.data?.history) {
|
||||
const allFormattedChartData: FormattedChartData = [
|
||||
['Date', 'UptimeV4', 'UptimeV6'],
|
||||
];
|
||||
uptimeStory.data.history.forEach((eachDate) => {
|
||||
const formattedDateUptimeRecord: FormattedDateRecord = [
|
||||
format(new Date(eachDate.date), 'MMM dd'),
|
||||
eachDate.ipv4_uptime,
|
||||
eachDate.ipv6_uptime,
|
||||
];
|
||||
allFormattedChartData.push(formattedDateUptimeRecord);
|
||||
});
|
||||
setFormattedChartData(allFormattedChartData);
|
||||
} else {
|
||||
const emptyData: any = [
|
||||
['Date', 'UptimeV4', 'UptimeV6'],
|
||||
['Jul 27', 10, 10],
|
||||
];
|
||||
setFormattedChartData(emptyData);
|
||||
}
|
||||
}, [uptimeStory]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && <Typography>{title}</Typography>}
|
||||
{loading && <CircularProgress />}
|
||||
|
||||
{!loading && uptimeStory && (
|
||||
<Chart
|
||||
style={{ minHeight: 480 }}
|
||||
chartType="LineChart"
|
||||
loader={<p>...</p>}
|
||||
data={
|
||||
uptimeStory.data
|
||||
? formattedChartData
|
||||
: [
|
||||
['Date', 'UptimeV4', 'UptimeV6'],
|
||||
[format(new Date(Date.now()), 'MMM dd'), 0, 0],
|
||||
]
|
||||
}
|
||||
options={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.nym.networkExplorer.background.tertiary
|
||||
: undefined,
|
||||
color: uptimeStory.error
|
||||
? 'rgba(255, 255, 255, 0.4)'
|
||||
: 'rgba(255, 255, 255, 1)',
|
||||
colors: ['#FB7A21', '#CC808A'],
|
||||
legend: {
|
||||
textStyle: {
|
||||
color,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
},
|
||||
|
||||
intervals: { style: 'sticks' },
|
||||
hAxis: {
|
||||
// horizontal / date
|
||||
title: xLabel,
|
||||
titleTextStyle: {
|
||||
color,
|
||||
},
|
||||
textStyle: {
|
||||
color,
|
||||
// fontSize: 11
|
||||
},
|
||||
gridlines: {
|
||||
count: -1,
|
||||
},
|
||||
},
|
||||
vAxis: {
|
||||
// % uptime vertical
|
||||
viewWindow: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
title: yLabel,
|
||||
titleTextStyle: {
|
||||
color,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
textStyle: {
|
||||
color,
|
||||
fontSize: 11,
|
||||
opacity: uptimeStory.error ? 0.4 : 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeChart.defaultProps = {
|
||||
title: undefined,
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import * as React from 'react';
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Marker,
|
||||
ZoomableGroup,
|
||||
} from 'react-simple-maps';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ApiState, CountryDataResponse } from 'src/typeDefs/explorer-api';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MAP_TOPOJSON from '../assets/world-110m.json';
|
||||
|
||||
type MapProps = {
|
||||
userLocation?: [number, number];
|
||||
countryData?: ApiState<CountryDataResponse>;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const WorldMap: React.FC<MapProps> = ({
|
||||
countryData,
|
||||
userLocation,
|
||||
loading,
|
||||
}) => {
|
||||
const { palette } = useTheme();
|
||||
|
||||
const colorScale = React.useMemo(() => {
|
||||
if (countryData?.data) {
|
||||
const heighestNumberOfNodes = Math.max(
|
||||
...Object.values(countryData.data).map((country) => country.nodes),
|
||||
);
|
||||
return scaleLinear<string, string>()
|
||||
.domain([
|
||||
0,
|
||||
heighestNumberOfNodes / 4,
|
||||
heighestNumberOfNodes / 2,
|
||||
heighestNumberOfNodes,
|
||||
])
|
||||
.range(palette.nym.networkExplorer.map.fills)
|
||||
.unknown(palette.nym.networkExplorer.map.fills[0]);
|
||||
}
|
||||
return () => palette.nym.networkExplorer.map.fills[0];
|
||||
}, [countryData, palette]);
|
||||
|
||||
const [tooltipContent, setTooltipContent] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ComposableMap
|
||||
data-tip=""
|
||||
style={{
|
||||
backgroundColor: palette.nym.networkExplorer.background.tertiary,
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}}
|
||||
viewBox="0, 50, 800, 350"
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: userLocation ? 200 : 100,
|
||||
center: userLocation,
|
||||
}}
|
||||
>
|
||||
<ZoomableGroup>
|
||||
<Geographies geography={MAP_TOPOJSON}>
|
||||
{({ geographies }) =>
|
||||
geographies.map((geo) => {
|
||||
const d = (countryData?.data || {})[geo.properties.ISO_A3];
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill={colorScale(d?.nodes || 0)}
|
||||
stroke={palette.nym.networkExplorer.map.stroke}
|
||||
strokeWidth={0.2}
|
||||
onMouseEnter={() => {
|
||||
const { NAME_LONG } = geo.properties;
|
||||
if (!userLocation) {
|
||||
setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent('');
|
||||
}}
|
||||
style={{
|
||||
hover:
|
||||
!userLocation && countryData
|
||||
? {
|
||||
fill: palette.nym.highlight,
|
||||
outline: 'white',
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
|
||||
{userLocation && (
|
||||
<Marker coordinates={userLocation}>
|
||||
<g
|
||||
fill="grey"
|
||||
stroke="#FF5533"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(-12, -10)"
|
||||
>
|
||||
<circle cx="12" cy="10" r="5" />
|
||||
</g>
|
||||
</Marker>
|
||||
)}
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
<ReactTooltip>{tooltipContent}</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
WorldMap.defaultProps = {
|
||||
userLocation: undefined,
|
||||
countryData: undefined,
|
||||
};
|
||||
@@ -0,0 +1,295 @@
|
||||
import { PaletteMode } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { MIXNODE_API_ERROR } from 'src/api/constants';
|
||||
import {
|
||||
CountryDataResponse,
|
||||
GatewayResponse,
|
||||
MixNodeResponse,
|
||||
ValidatorsResponse,
|
||||
BlockResponse,
|
||||
ApiState,
|
||||
MixNodeResponseItem,
|
||||
DelegationsResponse,
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
UptimeStoryResponse,
|
||||
} from 'src/typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
|
||||
interface State {
|
||||
mode: PaletteMode;
|
||||
toggleMode: () => void;
|
||||
mixnodes?: ApiState<MixNodeResponse>;
|
||||
fetchMixnodes: () => void;
|
||||
filterMixnodes: (arg: MixNodeResponse) => void;
|
||||
gateways?: ApiState<GatewayResponse>;
|
||||
validators?: ApiState<ValidatorsResponse>;
|
||||
block?: ApiState<BlockResponse>;
|
||||
countryData?: ApiState<CountryDataResponse>;
|
||||
globalError?: string | undefined;
|
||||
mixnodeDetailInfo?: ApiState<MixNodeResponse>;
|
||||
fetchMixnodeById: (arg: string) => void;
|
||||
fetchDelegationsById: (arg: string) => void;
|
||||
delegations?: ApiState<DelegationsResponse>;
|
||||
stats?: ApiState<StatsResponse>;
|
||||
fetchStatsById: (arg: string) => void;
|
||||
status?: ApiState<StatusResponse>;
|
||||
fetchStatusById: (arg: string) => void;
|
||||
fetchUptimeStoryById: (arg: string) => void;
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>;
|
||||
}
|
||||
|
||||
// TODO: remove the export and replace all uses with `useMainContext()` hook
|
||||
export const MainContext = React.createContext<State>({
|
||||
mode: 'dark',
|
||||
fetchMixnodeById: () => null,
|
||||
toggleMode: () => undefined,
|
||||
fetchDelegationsById: () => null,
|
||||
fetchStatsById: () => null,
|
||||
fetchStatusById: () => null,
|
||||
fetchUptimeStoryById: () => null,
|
||||
filterMixnodes: () => null,
|
||||
fetchMixnodes: () => null,
|
||||
status: { data: undefined, isLoading: false, error: undefined },
|
||||
stats: { data: undefined, isLoading: false, error: undefined },
|
||||
mixnodeDetailInfo: { data: undefined, isLoading: false, error: undefined },
|
||||
delegations: { data: undefined, isLoading: false, error: undefined },
|
||||
});
|
||||
|
||||
export const useMainContext = (): React.ContextType<typeof MainContext> =>
|
||||
React.useContext<State>(MainContext);
|
||||
|
||||
export const MainContextProvider: React.FC = ({ children }) => {
|
||||
// light/dark mode
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark');
|
||||
|
||||
// global / banner error messaging
|
||||
const [globalError, setGlobalError] = React.useState<string>();
|
||||
|
||||
// various APIs for Overview page
|
||||
const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>();
|
||||
const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>();
|
||||
const [validators, setValidators] =
|
||||
React.useState<ApiState<ValidatorsResponse>>();
|
||||
const [block, setBlock] = React.useState<ApiState<BlockResponse>>();
|
||||
const [countryData, setCountryData] =
|
||||
React.useState<ApiState<CountryDataResponse>>();
|
||||
const [mixnodeDetailInfo, setMixnodeDetailInfo] =
|
||||
React.useState<ApiState<MixNodeResponse>>();
|
||||
|
||||
// various APIs for Detail page
|
||||
const [delegations, setDelegations] =
|
||||
React.useState<ApiState<DelegationsResponse>>();
|
||||
const [status, setStatus] = React.useState<ApiState<StatusResponse>>();
|
||||
const [stats, setStats] = React.useState<ApiState<StatsResponse>>();
|
||||
const [uptimeStory, setUptimeStory] =
|
||||
React.useState<ApiState<UptimeStoryResponse>>();
|
||||
|
||||
const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark'));
|
||||
|
||||
// actions passed down to Overview and Detail pages
|
||||
const fetchUptimeStoryById = async (id: string) => {
|
||||
setUptimeStory({
|
||||
data: uptimeStory?.data,
|
||||
isLoading: true,
|
||||
error: uptimeStory?.error,
|
||||
});
|
||||
try {
|
||||
const data = await Api.fetchUptimeStoryById(id);
|
||||
setUptimeStory({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setUptimeStory({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Uptime Story api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDelegationsById = async (id: string) => {
|
||||
setDelegations({ data: delegations?.data, isLoading: true });
|
||||
try {
|
||||
const data = await Api.fetchDelegationsById(id);
|
||||
setDelegations({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setDelegations({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Delegations api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatusById = async (id: string) => {
|
||||
setStatus({ data: status?.data, isLoading: true, error: status?.error });
|
||||
try {
|
||||
const data = await Api.fetchStatusById(id);
|
||||
setStatus({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setStatus({
|
||||
error: error instanceof Error ? error : new Error('Status api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatsById = async (id: string) => {
|
||||
setStats({ data: stats?.data, isLoading: true, error: stats?.error });
|
||||
try {
|
||||
const data = await Api.fetchStatsById(id);
|
||||
setStats({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setStats({
|
||||
error: error instanceof Error ? error : new Error('Stats api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMixnodeById = async (id: string) => {
|
||||
setMixnodeDetailInfo({ data: mixnodeDetailInfo?.data, isLoading: true });
|
||||
|
||||
// 1. if mixnode data already exists filter down to this ID
|
||||
if (mixnodes && mixnodes.data) {
|
||||
const [matchedToID] = mixnodes.data.filter(
|
||||
(eachMixnode: MixNodeResponseItem) =>
|
||||
eachMixnode.mix_node.identity_key === id,
|
||||
);
|
||||
|
||||
// b) SUCCESS | if there *IS* a matched ID in mixnodes
|
||||
if (matchedToID) {
|
||||
setMixnodeDetailInfo({ data: [matchedToID], isLoading: false });
|
||||
}
|
||||
// b) FAIL | if there is no matching ID in mixnodes
|
||||
if (!matchedToID) {
|
||||
setGlobalError(MIXNODE_API_ERROR);
|
||||
setMixnodeDetailInfo({
|
||||
isLoading: false,
|
||||
error: new Error(MIXNODE_API_ERROR),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 2. if mixnode data DOESNT already exist, fetch this specific ID's information.
|
||||
try {
|
||||
const data = await Api.fetchMixnodeByID(id);
|
||||
// a) fetches from cache^, then API, then filters down then dumps in `mixnodes` context.
|
||||
if (data) {
|
||||
setMixnodeDetailInfo({ data: [data], isLoading: false });
|
||||
} else {
|
||||
throw Error('api failed to retrieve mixnode via id');
|
||||
}
|
||||
// NOTE: Only returning mixnodes api info at the moment. Other `ping` api required also.
|
||||
} catch (error) {
|
||||
setGlobalError(MIXNODE_API_ERROR);
|
||||
setMixnodeDetailInfo({
|
||||
isLoading: false,
|
||||
error: new Error(MIXNODE_API_ERROR),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const fetchMixnodes = async () => {
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }));
|
||||
try {
|
||||
const data = await Api.fetchMixnodes();
|
||||
setMixnodes({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setMixnodes({
|
||||
error: error instanceof Error ? error : new Error('Mixnode api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
const filterMixnodes = (arr: MixNodeResponse) => {
|
||||
setMixnodes({ data: arr, isLoading: false });
|
||||
};
|
||||
const fetchGateways = async () => {
|
||||
try {
|
||||
const data = await Api.fetchGateways();
|
||||
setGateways({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setGateways({
|
||||
error: error instanceof Error ? error : new Error('Gateways api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchValidators = async () => {
|
||||
try {
|
||||
const data = await Api.fetchValidators();
|
||||
setValidators({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setValidators({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Validators api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBlock = async () => {
|
||||
try {
|
||||
const data = await Api.fetchBlock();
|
||||
setBlock({ data, isLoading: false });
|
||||
} catch (error) {
|
||||
setBlock({
|
||||
error: error instanceof Error ? error : new Error('Block api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCountryData = async () => {
|
||||
setCountryData({ data: undefined, isLoading: true });
|
||||
try {
|
||||
const res = await Api.fetchCountryData();
|
||||
setCountryData({ data: res, isLoading: false });
|
||||
} catch (error) {
|
||||
setCountryData({
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Country Data api fail'),
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([
|
||||
fetchMixnodes(),
|
||||
fetchGateways(),
|
||||
fetchValidators(),
|
||||
fetchBlock(),
|
||||
fetchCountryData(),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MainContext.Provider
|
||||
value={{
|
||||
mode,
|
||||
toggleMode,
|
||||
mixnodes,
|
||||
filterMixnodes,
|
||||
fetchMixnodes,
|
||||
gateways,
|
||||
validators,
|
||||
block,
|
||||
countryData,
|
||||
globalError,
|
||||
mixnodeDetailInfo,
|
||||
fetchMixnodeById,
|
||||
fetchDelegationsById,
|
||||
delegations,
|
||||
stats,
|
||||
fetchStatsById,
|
||||
status,
|
||||
fetchStatusById,
|
||||
uptimeStory,
|
||||
fetchUptimeStoryById,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MainContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
export function useIsMounted(): () => boolean {
|
||||
const ref = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = true;
|
||||
return () => {
|
||||
ref.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useCallback(() => ref.current, [ref]);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const GatewaysSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.2 12H22.7"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M1.30005 12H12"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.1 9.40015L22.7 12.0001L20.1 14.6001"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const LightSwitchSVG: React.FC = () => {
|
||||
const { palette } = useTheme();
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z"
|
||||
fill={palette.background.default}
|
||||
/>
|
||||
<path
|
||||
d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z"
|
||||
fill={palette.text.primary}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const MixnodesSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" />
|
||||
<path
|
||||
d="M2.97681 23.0621L23.0437 2.99512"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0437 23.0621L2.97681 2.99512"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M13.0103 23.0621L23.0437 2.99512"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.97681 2.99512L13.0103 23.0621"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z"
|
||||
fill="#242C3D"
|
||||
stroke={color}
|
||||
strokeWidth="1.2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const NetworkComponentsSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.nym.networkExplorer.nav.text;
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const NodemapSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.nym.networkExplorer.nav.text;
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 19 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M1.5 22.5999H17.1"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export const NymLogoSVG: React.FC = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 5389.9 5389.9"
|
||||
width="40px"
|
||||
height="40px"
|
||||
{...props}
|
||||
>
|
||||
<circle cx={2695} cy={2695} r={2585} fill="#121726" />
|
||||
<linearGradient
|
||||
id="nym_logo_svg__a"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1={0}
|
||||
y1={8058.165}
|
||||
x2={5390}
|
||||
y2={8058.165}
|
||||
gradientTransform="matrix(1 0 0 -1 0 10753.165)"
|
||||
>
|
||||
<stop offset={0} stopColor="#f77846" />
|
||||
<stop offset={1} stopColor="#ed3572" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="M2695 5390c-182.8 0-365.5-18.4-543-54.8-173.1-35.4-343.3-88.3-506-157.1-159.7-67.6-313.7-151.2-457.8-248.5-142.7-96.4-276.8-207.1-398.8-329-121.9-121.9-232.6-256.1-329-398.8-97.4-144-181-298-248.6-457.8-68.8-162.6-121.6-332.9-157-506C18.4 3060.5 0 2877.8 0 2695s18.4-365.5 54.8-543c35.4-173.1 88.3-343.3 157.1-506 67.6-159.7 151.2-313.7 248.5-457.8 96.4-142.7 207.1-276.8 329-398.8s256.1-232.6 398.8-329c144.1-97.3 298.1-180.9 457.8-248.5 162.7-68.8 332.9-121.7 506-157.1C2329.5 18.4 2512.2 0 2695 0s365.5 18.4 543 54.8c173.1 35.4 343.3 88.3 506 157.1 159.7 67.6 313.7 151.2 457.8 248.5 142.7 96.4 276.8 207.1 398.8 329 121.9 121.9 232.6 256.1 329 398.8 97.3 144.1 180.9 298.1 248.5 457.8 68.8 162.7 121.7 332.9 157.1 506 36.3 177.5 54.8 360.2 54.8 543s-18.4 365.5-54.8 543c-35.4 173.1-88.3 343.3-157.1 506-67.6 159.7-151.2 313.7-248.5 457.8-96.4 142.7-207.1 276.8-329 398.8-121.9 121.9-256.1 232.6-398.8 329-144.1 97.3-298.1 180.9-457.8 248.5-162.7 68.8-332.9 121.7-506 157.1-177.5 36.4-360.2 54.8-543 54.8zm0-5170c-168 0-335.9 16.9-498.9 50.3-158.9 32.5-315.1 81-464.4 144.2-146.6 62-288.1 138.8-420.4 228.2C1180.2 731.3 1057 833 944.9 945c-112 112-213.7 235.3-302.3 366.4-89.4 132.3-166.2 273.7-228.2 420.4-63.2 149.3-111.7 305.6-144.2 464.4C236.9 2359.1 220 2527 220 2695s16.9 335.9 50.3 498.9c32.5 158.9 81 315.1 144.2 464.4 62 146.6 138.8 288.1 228.2 420.4C731.3 4209.8 833 4333 945 4445.1c112 112 235.3 213.7 366.4 302.3 132.3 89.4 273.7 166.2 420.4 228.2 149.3 63.2 305.6 111.7 464.4 144.2 163.1 33.4 330.9 50.3 498.9 50.3s335.9-16.9 498.9-50.3c158.9-32.5 315.1-81 464.4-144.2 146.6-62 288.1-138.8 420.4-228.2 131.1-88.6 254.3-190.3 366.4-302.3 112-112 213.7-235.3 302.3-366.4 89.4-132.3 166.2-273.7 228.2-420.4 63.2-149.3 111.7-305.6 144.2-464.4 33.4-163.1 50.3-330.9 50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4-62-146.6-138.8-288.1-228.2-420.4-88.6-131.1-190.3-254.3-302.3-366.4-112-112-235.3-213.7-366.4-302.3-132.3-89.4-273.7-166.2-420.4-228.2-149.3-63.2-305.6-111.7-464.4-144.2-163.1-33.3-331-50.2-499-50.2z"
|
||||
fill="url(#nym_logo_svg__a)"
|
||||
/>
|
||||
<path
|
||||
d="M1958.5 3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8 725.1v-725.1h211.9v930.9zm2420.4-930.9l-335.7 330.9-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4 340.6c26.7 26.3 69.6 26.3 96.3 0l345.4-340.6v725.3h218.4v-930.9h-276.3zm-1789.8 485.9v445h218.4v-445l502.7-485.9H3034l-335.9 330.9-335.7-330.9h-276.2l502.9 485.9z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const OverviewSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.nym.networkExplorer.nav.text;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.4 21.6H22.6"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.4 11.8H3.2V21.6H7.4V11.8Z"
|
||||
stroke={color}
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const ValidatorsSVG: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const color = theme.palette.text.primary;
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 26 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0)">
|
||||
<path
|
||||
d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8"
|
||||
stroke={color}
|
||||
strokeWidth="1.3"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 12.3L7.9 15.3L14.5 8.69995"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nym Network Explorer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { App } from './App';
|
||||
import { MainContextProvider } from './context/main';
|
||||
import './styles.css';
|
||||
import { NetworkExplorerThemeProvider } from './theme';
|
||||
|
||||
ReactDOM.render(
|
||||
<MainContextProvider>
|
||||
<NetworkExplorerThemeProvider>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</NetworkExplorerThemeProvider>
|
||||
</MainContextProvider>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#121726;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<circle class="st0" cx="250" cy="250.1" r="239.8"/>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="4.980469e-02" y1="5613.9395" x2="500.0498" y2="5613.9395" gradientTransform="matrix(1 0 0 -1 0 5863.9897)">
|
||||
<stop offset="0" style="stop-color:#E1864B"/>
|
||||
<stop offset="1" style="stop-color:#DA465B"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M250,500.1c-17,0-33.9-1.7-50.4-5.1c-16.1-3.3-31.8-8.2-46.9-14.6c-14.8-6.3-29.1-14-42.5-23.1
|
||||
c-13.2-8.9-25.7-19.2-37-30.5c-11.3-11.3-21.6-23.8-30.5-37c-9-13.4-16.8-27.6-23.1-42.5c-6.4-15.1-11.3-30.9-14.6-46.9
|
||||
C1.8,284,0,267,0,250.1s1.7-33.9,5.1-50.4c3.3-16.1,8.2-31.8,14.6-46.9c6.3-14.8,14-29.1,23.1-42.5C51.7,97,62,84.6,73.3,73.3
|
||||
s23.8-21.6,37-30.5c13.4-9,27.7-16.8,42.5-23.1c15.1-6.4,30.9-11.3,46.9-14.6c16.5-3.4,33.4-5.1,50.4-5.1c17,0,33.9,1.7,50.4,5.1
|
||||
c16.1,3.3,31.8,8.2,46.9,14.6c14.8,6.3,29.1,14,42.5,23.1c13.2,8.9,25.7,19.2,37,30.5c11.3,11.3,21.6,23.8,30.5,37
|
||||
c9,13.4,16.8,27.7,23.1,42.5c6.4,15.1,11.3,30.9,14.6,46.9c3.4,16.5,5.1,33.4,5.1,50.4s-1.7,33.9-5.1,50.4
|
||||
c-3.3,16.1-8.2,31.8-14.6,46.9c-6.3,14.8-14,29.1-23.1,42.5c-8.9,13.2-19.2,25.7-30.5,37c-11.3,11.3-23.8,21.6-37,30.5
|
||||
c-13.4,9-27.7,16.8-42.5,23.1c-15.1,6.4-30.9,11.3-46.9,14.6C284,498.3,267,500.1,250,500.1z M250,20.5c-15.6,0-31.2,1.6-46.3,4.7
|
||||
c-14.7,3-29.2,7.5-43.1,13.4c-13.6,5.8-26.7,12.9-39,21.2c-12.2,8.2-23.6,17.7-34,28c-10.4,10.4-19.8,21.8-28,34
|
||||
c-8.3,12.3-15.4,25.4-21.2,39c-5.9,13.8-10.4,28.3-13.4,43.1c-3.1,15.1-4.7,30.7-4.7,46.3s1.6,31.2,4.7,46.3
|
||||
c3,14.7,7.5,29.2,13.4,43.1c5.8,13.6,12.9,26.7,21.2,39c8.2,12.2,17.7,23.6,28,34c10.4,10.4,21.8,19.8,34,28
|
||||
c12.3,8.3,25.4,15.4,39,21.2c13.8,5.9,28.3,10.4,43.1,13.4c15.1,3.1,30.7,4.7,46.3,4.7c15.6,0,31.2-1.6,46.3-4.7
|
||||
c14.7-3,29.2-7.5,43.1-13.4c13.6-5.8,26.7-12.9,39-21.2c12.2-8.2,23.6-17.7,34-28c10.4-10.4,19.8-21.8,28-34
|
||||
c8.3-12.3,15.4-25.4,21.2-39c5.9-13.8,10.4-28.3,13.4-43.1c3.1-15.1,4.7-30.7,4.7-46.3s-1.6-31.2-4.7-46.3
|
||||
c-3-14.7-7.5-29.2-13.4-43.1c-5.8-13.6-12.9-26.7-21.2-39c-8.2-12.2-17.7-23.6-28-34c-10.4-10.4-21.8-19.8-34-28
|
||||
c-12.3-8.3-25.4-15.4-39-21.2c-13.8-5.9-28.3-10.4-43.1-13.4C281.2,22,265.6,20.5,250,20.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M369.8,341.2h-52.8l-144-142v142h-42.8V158.9h54.1l144,141.9V158.9h41.5V341.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
|
||||
|
||||
export const Page404: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { mode } = useMainContext();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={0} alignItems="center" justifyContent="center">
|
||||
<Grid item xs={12} sm={12} md={6}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 3,
|
||||
height: 450,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
flexDirection: 'column',
|
||||
background:
|
||||
mode === 'dark'
|
||||
? theme.palette.secondary.dark
|
||||
: theme.palette.primary.light,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<NymLogoSVG />
|
||||
<Typography variant="h2">Oh No!</Typography>
|
||||
<Typography variant="body1">
|
||||
It looks like you might be lost.
|
||||
</Typography>
|
||||
<Typography variant="body1" textAlign="center">
|
||||
Please try the link again or navigate back to{' '}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
bgcolor: theme.palette.primary.main,
|
||||
color: theme.palette.secondary.main,
|
||||
}}
|
||||
onClick={() => history.push('/overview')}
|
||||
>
|
||||
Overview
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Grid, Typography } from '@mui/material';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { gatewayToGridRow } from 'src/utils';
|
||||
import { GatewayResponse } from 'src/typeDefs/explorer-api';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
export const PageGateways: React.FC = () => {
|
||||
const { gateways } = useMainContext();
|
||||
const [filteredGateways, setFilteredGateways] =
|
||||
React.useState<GatewayResponse>([]);
|
||||
const [pageSize, setPageSize] = React.useState<string>('50');
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
|
||||
const handleSearch = (str: string) => {
|
||||
setSearchTerm(str.toLowerCase());
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchTerm === '' && gateways?.data) {
|
||||
setFilteredGateways(gateways?.data);
|
||||
} else {
|
||||
const filtered = gateways?.data?.filter((g) => {
|
||||
if (
|
||||
g.gateway.location.toLowerCase().includes(searchTerm) ||
|
||||
g.gateway.identity_key.toLocaleLowerCase().includes(searchTerm) ||
|
||||
g.owner.toLowerCase().includes(searchTerm)
|
||||
) {
|
||||
return g;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (filtered) {
|
||||
setFilteredGateways(filtered);
|
||||
}
|
||||
}
|
||||
}, [searchTerm, gateways?.data]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
width: 120,
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
width: 150,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Button
|
||||
onClick={() => handleSearch(params.value as string)}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
if (gateways?.data) {
|
||||
return (
|
||||
<>
|
||||
<Title text="Gateways" />
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<ContentCard>
|
||||
<TableToolbar
|
||||
onChangeSearch={handleSearch}
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
loading={gateways?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={gatewayToGridRow(filteredGateways)}
|
||||
pageSize={pageSize}
|
||||
pagination={gateways?.data?.length >= 12}
|
||||
hideFooter={gateways?.data?.length < 12}
|
||||
sortModel={[
|
||||
{
|
||||
field: 'bond',
|
||||
sort: 'desc',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,283 @@
|
||||
import * as React from 'react';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { Box, Grid, Typography } from '@mui/material';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { BondBreakdownTable } from 'src/components/BondBreakdown';
|
||||
import { TwoColSmallTable } from 'src/components/TwoColSmallTable';
|
||||
import { UptimeChart } from 'src/components/UptimeChart';
|
||||
import { mixnodeToGridRow, scrollToRef } from 'src/utils';
|
||||
import { ComponentError } from 'src/components/ComponentError';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { MixNodeResponseItem } from 'src/typeDefs/explorer-api';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
width: 200,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
headerName: 'Bond',
|
||||
type: 'number',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return <Typography sx={cellStyles}>{bondAsPunk}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
headerName: 'Self %',
|
||||
headerAlign: 'left',
|
||||
type: 'number',
|
||||
width: 99,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}%</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<div>
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'layer',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
type: 'number',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const PageMixnodeDetail: React.FC = () => {
|
||||
const ref = React.useRef();
|
||||
const [row, setRow] = React.useState<MixNodeResponseItem[]>([]);
|
||||
const {
|
||||
fetchMixnodeById,
|
||||
mixnodeDetailInfo,
|
||||
fetchStatsById,
|
||||
fetchDelegationsById,
|
||||
fetchUptimeStoryById,
|
||||
fetchStatusById,
|
||||
stats,
|
||||
status,
|
||||
uptimeStory,
|
||||
} = useMainContext();
|
||||
const { id }: any = useParams();
|
||||
|
||||
React.useEffect(() => {
|
||||
const hasNoDetail = id && !mixnodeDetailInfo;
|
||||
const hasIncorrectDetail =
|
||||
id &&
|
||||
mixnodeDetailInfo?.data &&
|
||||
mixnodeDetailInfo?.data[0].mix_node.identity_key !== id;
|
||||
if (hasNoDetail || hasIncorrectDetail) {
|
||||
fetchMixnodeById(id);
|
||||
fetchDelegationsById(id);
|
||||
fetchStatsById(id);
|
||||
fetchStatusById(id);
|
||||
fetchUptimeStoryById(id);
|
||||
} else if (mixnodeDetailInfo?.data !== undefined) {
|
||||
setRow(mixnodeDetailInfo?.data);
|
||||
}
|
||||
}, [id, mixnodeDetailInfo]);
|
||||
|
||||
React.useEffect(() => {
|
||||
scrollToRef(ref);
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box component="main" ref={ref}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Title text="Mixnode Detail" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{mixnodeDetailInfo && (
|
||||
<ContentCard>
|
||||
<UniversalDataGrid
|
||||
columnsData={columns}
|
||||
rows={mixnodeToGridRow(row)}
|
||||
loading={mixnodeDetailInfo.isLoading}
|
||||
pageSize="1"
|
||||
pagination={false}
|
||||
hideFooter
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title="Bond Breakdown">
|
||||
<BondBreakdownTable />
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<ContentCard title="Mixnode Stats">
|
||||
{stats && (
|
||||
<>
|
||||
{stats.error && (
|
||||
<ComponentError text="There was a problem retrieving this nodes stats." />
|
||||
)}
|
||||
<TwoColSmallTable
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since startup"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_startup || 0,
|
||||
stats?.data?.packets_sent_since_startup || 0,
|
||||
stats?.data?.packets_explicitly_dropped_since_startup ||
|
||||
0,
|
||||
]}
|
||||
/>
|
||||
<TwoColSmallTable
|
||||
loading={stats.isLoading}
|
||||
error={stats?.error?.message}
|
||||
title="Since last update"
|
||||
keys={['Received', 'Sent', 'Explicitly dropped']}
|
||||
values={[
|
||||
stats?.data?.packets_received_since_last_update || 0,
|
||||
stats?.data?.packets_sent_since_last_update || 0,
|
||||
stats?.data
|
||||
?.packets_explicitly_dropped_since_last_update || 0,
|
||||
]}
|
||||
marginBottom
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!stats && <Typography>No stats information</Typography>}
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
{uptimeStory && (
|
||||
<ContentCard title="Uptime story">
|
||||
{uptimeStory.error && (
|
||||
<ComponentError text="There was a problem retrieving uptime history." />
|
||||
)}
|
||||
<UptimeChart
|
||||
loading={uptimeStory.isLoading}
|
||||
xLabel="date"
|
||||
yLabel="uptime"
|
||||
uptimeStory={uptimeStory}
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
{status && (
|
||||
<ContentCard title="Mixnode Status">
|
||||
{status.error && (
|
||||
<ComponentError text="There was a problem retrieving port information" />
|
||||
)}
|
||||
<TwoColSmallTable
|
||||
loading={status.isLoading}
|
||||
error={status?.error?.message}
|
||||
keys={['Mix port', 'Verloc port', 'HTTP port']}
|
||||
values={[1789, 1790, 8000].map((each) => each)}
|
||||
icons={
|
||||
(status?.data?.ports &&
|
||||
Object.values(status.data.ports)) || [false, false, false]
|
||||
}
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
{mixnodeDetailInfo && (
|
||||
<ContentCard title="Location">
|
||||
{mixnodeDetailInfo?.error && (
|
||||
<ComponentError text="There was a problem retrieving this mixnode location" />
|
||||
)}
|
||||
{mixnodeDetailInfo.data &&
|
||||
mixnodeDetailInfo?.data[0]?.location && (
|
||||
<WorldMap
|
||||
loading={mixnodeDetailInfo.isLoading}
|
||||
userLocation={[
|
||||
mixnodeDetailInfo?.data[0]?.location?.lng,
|
||||
mixnodeDetailInfo?.data[0]?.location?.lat,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as React from 'react';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { Button, Grid, Link as MuiLink } from '@mui/material';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { mixnodeToGridRow } from 'src/utils';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { MixNodeResponse } from 'src/typeDefs/explorer-api';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { ContentCard } from 'src/components/ContentCard';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { Title } from 'src/components/Title';
|
||||
|
||||
export const PageMixnodes: React.FC = () => {
|
||||
const { mixnodes } = useMainContext();
|
||||
const [filteredMixnodes, setFilteredMixnodes] =
|
||||
React.useState<MixNodeResponse>([]);
|
||||
const [pageSize, setPageSize] = React.useState<string>('10');
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
|
||||
const handleSearch = (str: string) => {
|
||||
setSearchTerm(str.toLowerCase());
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchTerm === '' && mixnodes?.data) {
|
||||
setFilteredMixnodes(mixnodes?.data);
|
||||
} else {
|
||||
const filtered = mixnodes?.data?.filter((m) => {
|
||||
if (
|
||||
m.location?.country_name.toLowerCase().includes(searchTerm) ||
|
||||
m.mix_node.identity_key.toLocaleLowerCase().includes(searchTerm) ||
|
||||
m.owner.toLowerCase().includes(searchTerm)
|
||||
) {
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (filtered) {
|
||||
setFilteredMixnodes(filtered);
|
||||
}
|
||||
}
|
||||
}, [searchTerm, mixnodes?.data]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
flex: 3,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
href={`${BIG_DIPPER}/account/${params.value}`}
|
||||
target="_blank"
|
||||
sx={cellStyles}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
flex: 3,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.value}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
headerName: 'Bond',
|
||||
type: 'number',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
const bondAsPunk = printableCoin({
|
||||
amount: params.value as string,
|
||||
denom: 'upunk',
|
||||
});
|
||||
return (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{bondAsPunk}
|
||||
</MuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'self_percentage',
|
||||
headerName: 'Self %',
|
||||
headerAlign: 'left',
|
||||
type: 'number',
|
||||
width: 99,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}%
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={cellStyles}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Button
|
||||
onClick={() => handleSearch(params.value as string)}
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'layer',
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
|
||||
flex: 1,
|
||||
type: 'number',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles, textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnodes/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title text="Mixnodes" />
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<ContentCard>
|
||||
<TableToolbar
|
||||
onChangeSearch={handleSearch}
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
loading={mixnodes?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={mixnodeToGridRow(filteredMixnodes)}
|
||||
pageSize={pageSize}
|
||||
pagination
|
||||
hideFooter={false}
|
||||
sortModel={[
|
||||
{
|
||||
field: 'bond',
|
||||
sort: 'desc',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
SelectChangeEvent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import {
|
||||
cellStyles,
|
||||
UniversalDataGrid,
|
||||
} from 'src/components/Universal-DataGrid';
|
||||
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { TableToolbar } from 'src/components/TableToolbar';
|
||||
import { CountryDataRowType, countryDataToGridRow } from 'src/utils';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
|
||||
export const PageMixnodesMap: React.FC = () => {
|
||||
const { countryData } = useMainContext();
|
||||
const [pageSize, setPageSize] = React.useState<string>('10');
|
||||
const [formattedCountries, setFormattedCountries] = React.useState<
|
||||
CountryDataRowType[]
|
||||
>([]);
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
|
||||
const handleSearch = (str: string) => {
|
||||
setSearchTerm(str.toLowerCase());
|
||||
};
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'countryName',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'nodes',
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading headingTitle="Number of Nodes" />
|
||||
),
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'percentage',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />,
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles}>{params.value}</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (countryData?.data && searchTerm === '') {
|
||||
setFormattedCountries(
|
||||
countryDataToGridRow(Object.values(countryData.data)),
|
||||
);
|
||||
} else if (countryData?.data !== undefined && searchTerm !== '') {
|
||||
const formatted = countryDataToGridRow(Object.values(countryData?.data));
|
||||
const filtered = formatted.filter((m) => {
|
||||
if (
|
||||
m.countryName.toLowerCase().includes(searchTerm) ||
|
||||
m.ISO3.toLowerCase().includes(searchTerm)
|
||||
) {
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (filtered) {
|
||||
setFormattedCountries(filtered);
|
||||
}
|
||||
}
|
||||
}, [searchTerm, countryData?.data]);
|
||||
|
||||
if (countryData?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (countryData?.data && !countryData.isLoading) {
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={1} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12}>
|
||||
<Title text="Mixnodes Around the Globe" />
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={9}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title="Distribution of nodes">
|
||||
<WorldMap loading={false} countryData={countryData} />
|
||||
<Box sx={{ marginTop: 2 }} />
|
||||
<TableToolbar
|
||||
onChangeSearch={handleSearch}
|
||||
onChangePageSize={handlePageSize}
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid
|
||||
loading={countryData?.isLoading}
|
||||
columnsData={columns}
|
||||
rows={formattedCountries}
|
||||
pageSize={pageSize}
|
||||
pagination
|
||||
/>
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return <Alert severity="error">{countryData?.error}</Alert>;
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Grid, Link } from '@mui/material';
|
||||
import { WorldMap } from 'src/components/WorldMap';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useMainContext } from 'src/context/main';
|
||||
import { formatNumber } from 'src/utils';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { ValidatorsSVG } from 'src/icons/ValidatorsSVG';
|
||||
import { GatewaysSVG } from 'src/icons/GatewaysSVG';
|
||||
import { MixnodesSVG } from 'src/icons/MixnodesSVG';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { StatsCard } from '../../components/StatsCard';
|
||||
|
||||
export const PageOverview: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { mixnodes, gateways, validators, block, countryData } =
|
||||
useMainContext();
|
||||
return (
|
||||
<>
|
||||
<Box component="main" sx={{ flexGrow: 1 }}>
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<Title text="Overview" />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={2}>
|
||||
{mixnodes && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => history.push('/network-components/mixnodes')}
|
||||
title="Mixnodes"
|
||||
icon={<MixnodesSVG />}
|
||||
count={mixnodes?.data?.length || ''}
|
||||
errorMsg={mixnodes?.error}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{gateways && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => history.push('/network-components/gateways')}
|
||||
title="Gateways"
|
||||
count={gateways?.data?.length || ''}
|
||||
errorMsg={gateways?.error}
|
||||
icon={<GatewaysSVG />}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{validators && (
|
||||
<Grid item xs={12} md={4}>
|
||||
<StatsCard
|
||||
onClick={() => window.open(`${BIG_DIPPER}/validators`)}
|
||||
title="Validators"
|
||||
count={validators?.data?.count || ''}
|
||||
errorMsg={validators?.error}
|
||||
icon={<ValidatorsSVG />}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{block?.data && (
|
||||
<Grid item xs={12}>
|
||||
<ContentCard
|
||||
title={
|
||||
<Link
|
||||
href={`${BIG_DIPPER}/blocks`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="none"
|
||||
color="inherit"
|
||||
>
|
||||
Current block height is {formatNumber(block.data)}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<ContentCard title="Distribution of nodes around the world">
|
||||
<WorldMap loading={false} countryData={countryData} />
|
||||
</ContentCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { PageOverview } from 'src/pages/Overview';
|
||||
import { PageMixnodesMap } from 'src/pages/MixnodesMap';
|
||||
import { Page404 } from '../pages/404';
|
||||
import { NetworkComponentsRoutes } from './network-components';
|
||||
|
||||
export const Routes: React.FC = () => (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/overview" />
|
||||
</Route>
|
||||
<Route exact path="/overview">
|
||||
<PageOverview />
|
||||
</Route>
|
||||
<Route path="/network-components">
|
||||
<NetworkComponentsRoutes />
|
||||
</Route>
|
||||
<Route path="/nodemap">
|
||||
<PageMixnodesMap />
|
||||
</Route>
|
||||
<Route component={Page404} />
|
||||
</Switch>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { Switch, Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { BIG_DIPPER } from 'src/api/constants';
|
||||
import { PageGateways } from 'src/pages/Gateways';
|
||||
import { PageMixnodeDetail } from 'src/pages/MixnodeDetail';
|
||||
import { PageMixnodes } from '../pages/Mixnodes';
|
||||
|
||||
export const NetworkComponentsRoutes: React.FC = () => (
|
||||
<>
|
||||
<Switch>
|
||||
<Route exact path="/network-components/mixnodes">
|
||||
<PageMixnodes />
|
||||
</Route>
|
||||
<Route path="/network-components/mixnodes/:id">
|
||||
<PageMixnodeDetail />
|
||||
</Route>
|
||||
<Route path="/network-components/gateways">
|
||||
<PageGateways />
|
||||
</Route>
|
||||
<Route
|
||||
path="/network-components/validators"
|
||||
component={(props: RouteComponentProps) => {
|
||||
window.open(`${BIG_DIPPER}/validators`);
|
||||
props.history.goBack();
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Route path="/network-components/gateways/:id">
|
||||
<h1> Specific Gateways ID</h1>
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/* last resort for styles that cannot be handled by Material UI and Emotion JS */
|
||||
|
||||
/* TODO - this override should take place in either
|
||||
the theme declaration in index.tsx or the style prop
|
||||
in <DataGrid /> */
|
||||
|
||||
.MuiDataGrid-sortIcon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-columnSeparator {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* TODO - this should be managed somehow in MUI DataGrid
|
||||
but documentation doesnt offer a way to do it. Possibly only
|
||||
included in Data Grid Pro package */
|
||||
.MuiDataGrid-header-override {
|
||||
height: 55px;
|
||||
min-height: 55px;
|
||||
}
|
||||
|
||||
/* Again, no offered way to add sx to this specific div to kill the padding
|
||||
which puts it out of sync with other (sx styled) cells */
|
||||
div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Unable to style nested MUI paper within the Drawer
|
||||
so styling manually here */
|
||||
div.MuiDrawer-root > .MuiDrawer-paperAnchorLeft {
|
||||
background-color: #111826;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { Nav } from '../components/Nav';
|
||||
|
||||
describe('Nav', () => {
|
||||
beforeEach(() => {
|
||||
render(<Nav />);
|
||||
});
|
||||
it('should render without exploding', () => {
|
||||
const { container } = render(<Nav />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { WorldMap } from '../components/WorldMap';
|
||||
|
||||
describe('WorldMap', () => {
|
||||
beforeEach(() => {
|
||||
render(<WorldMap loading={false} />);
|
||||
});
|
||||
it('should render without exploding', () => {
|
||||
const { container } = render(<WorldMap loading={false} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
it('should render the expected container/child element', () => {
|
||||
expect(screen.getByTestId('worldMap__container')).toBeInTheDocument();
|
||||
});
|
||||
it('should render the title', () => {
|
||||
expect(screen.getByText('mix-nodes around the globe')).toBeTruthy();
|
||||
});
|
||||
it('should render the map/SVG', () => {
|
||||
expect(screen.getByTestId('svg')).toBeInTheDocument();
|
||||
});
|
||||
it('should render map at correct size/dims', () => {
|
||||
const expectedWidth = '1000';
|
||||
const expectedHeight = '800';
|
||||
expect(screen.getByTestId('svg')).toHaveAttribute('width', expectedWidth);
|
||||
expect(screen.getByTestId('svg')).toHaveAttribute('height', expectedHeight);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
describe('testing jest config', () => {
|
||||
test('jest works with typescript', () => {
|
||||
const foo = () => 42;
|
||||
|
||||
expect(foo()).toBe(42);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user