Compare commits

...

44 Commits

Author SHA1 Message Date
Fouad f2e95d2fd5 Merge pull request #1244 from nymtech/feature/inclusion-probability-ui-update
update inclusion prob UI update
2022-05-03 16:57:22 +01:00
fmtabbara 5730c914e8 use coming soon chip 2022-05-03 16:38:15 +01:00
fmtabbara 7845e32742 update inclusion prob UI 2022-05-03 15:14:16 +01:00
Jon Häggblad 4cce235e13 validator-api: disable swagger temporarily 2022-05-03 15:49:44 +02:00
Jędrzej Stuczyński 521eb98f25 Generated changelog for 1.0.0 2022-05-03 14:27:54 +01:00
Jon Häggblad c77ccddcb3 Add swagger for validator-api (#1239)
* validator-api: add swagger openapi

* Lock file
2022-05-03 14:40:11 +02:00
Drazen Urch b8ce97e005 Mitigate PM change attack (#1236)
* Mitigate PM change attack

* More paranoid test
2022-05-03 14:36:29 +02:00
Bogdan-Ștefan Neacşu 49e29af5f4 Fix gateway test wallet address (#1237)
* Fix gateway test wallet address

Since we're switching to mainnet as default, use the correct wallet
address format.

* Fix other test with prefix problems
2022-05-03 14:36:29 +02:00
durch 41319fe7ad Fix unbonding for new nodes 2022-05-03 14:36:29 +02:00
durch 4fcf0da5c0 Move amount to liquidate to const 2022-05-03 14:36:29 +02:00
durch 0a6b2a8aaf Fix vesting accounting 2022-05-03 14:36:29 +02:00
Drazen Urch b09db50bba Fix checkpointing (#1230)
* Fix checkpoint strategy and order, test

* Debug pending delegation events

* Debug print

* Switch print to log

* Add debug print

* More printing

* Remove problematic checkpoint

* checkpoint mixnodes in separate block

* more debugs

* Removing old migration just in case

* Printing all checkpoint heights at migration

* Purging old checkpoint from storage

* Attempting to load raw storage value

* More printing...

* Removed expect

* deserialization changes

* error handling

* cleanup

* clippy

* dead code

* Reduce minimum age for rewarding to 1 epoch

* Add checkpoint query

* Get checkpoint at height

* Fix delegation compounding, fix undelegate accumulate_rewards

* Fix potential overflow, add debug logging

* Moar logging

* Fix total_delegations + rewards

* Better error for horrible overflow

* fmt

* Fixed unit test assertions in bech32 validation

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2022-05-03 14:36:29 +02:00
Jędrzej Stuczyński 82e6d7335b Bandwidth credentials disabled by default 2022-05-03 14:36:29 +02:00
Jędrzej Stuczyński 21d19d2447 Renamed testnet mode to disabled credentials mode 2022-05-03 14:36:29 +02:00
Jędrzej Stuczyński 5f116f8104 Set mainnet as the default network build target 2022-05-03 14:36:29 +02:00
Jędrzej Stuczyński 04f633446e Updated binaries versions to 1.0.0 2022-05-03 14:36:29 +02:00
durch be5421719c Fix undelegations 2022-05-03 14:36:29 +02:00
Fouad 96f2718b94 Feature/show pending delegations (#1229)
* remove unused import

* use correct types

* set up pending delegations

* remove unused import
2022-04-29 12:53:52 +01:00
Drazen Urch 632612eca0 Bucket inclusion probabilities (#1224)
* Bucket inclusion probabilities

* Make display equal variant name for TS
2022-04-26 16:23:32 +02:00
Drazen Urch c250492f50 Create a new bundled delegation when compounding rewards (#1221)
* Create a new bundled delegation when compounding rewards

* Cleanup migration code

* Revert "Cleanup migration code"

This reverts commit 21d8ad6388.
2022-04-25 20:34:43 +02:00
Mark Sinclair 52fdc3e0cf Bump nym-wallet version 2022-04-25 14:34:23 +01:00
Mark Sinclair 5a91240992 Use new tauri operation to get the runtime env vars 2022-04-25 14:34:23 +01:00
Mark Sinclair c9638097a0 Add tauri operation to get runtime environment vars 2022-04-25 14:34:23 +01:00
Mark Sinclair 383b197e5b Use new query to get pending vesting delegations 2022-04-25 14:34:23 +01:00
Mark Sinclair 787a55a4ba Fix typo 2022-04-25 14:34:23 +01:00
Mark Sinclair bbdd53d2aa Add terminal in dev mode 2022-04-25 14:34:23 +01:00
Mark Sinclair 67f6394a26 Bug fix undelegating locked tokens to use the correct contract methods 2022-04-25 14:34:23 +01:00
Mark Sinclair dbfeaff661 Explorer API - reduce cache time to 60 seconds 2022-04-25 14:19:55 +01:00
Jędrzej Stuczyński 8318002b0a Bugfix/delegation reconcile (#1219)
* Passing proxy value when attempting to compound delegator reward

* Do not attempt to delegate reward to mixnode if its zero

* Additional guards against sending 0 tokens

* Removed sign of sloppiness

* Fixes to rewards and delegation events storage

* Remove block count check, epoch cannot be advanced while in progress

* Add reward compound ops to vesting contract

* Migration to remove 0 value delegationns

Co-authored-by: durch <durch@users.noreply.github.com>
2022-04-25 12:01:45 +01:00
Tommy Verrall 9fb980dc5e Merge pull request #1212 from pwnfoo/patch-1
chore: fix dark mode rendering
2022-04-22 17:39:50 +01:00
Jędrzej Stuczyński ce77b17534 Bugfix/query proxied pending delegations (#1218)
* Removed 'expect' from one of the queries

* Query for pending vesting delegation events
2022-04-22 14:51:53 +01:00
Jędrzej Stuczyński e6372d3b02 Using custom gas multiplier in the wallet (#1217) 2022-04-21 16:52:17 +01:00
Jędrzej Stuczyński 836ef9d4c8 Feature/vesting accounts support (#1216)
* Utility for parsing Vec<ProtoCoin>

* Support for different types of vesting accounts

* Derived Debug for Account

* Exposed method for querying for account details

* Renamed ibid. to a more appropriate name
2022-04-21 13:18:05 +01:00
Jędrzej Stuczyński dd66697884 Release/1.0.0 rc.2 (#1214)
* Updated binaries versions to 1.0.0-rc.2

* Update GitHub Action to build nym binaries

* Fix contract bootstraping

* Relax the active_set check

* Cleanup migration

* Fix validator-api mainnet config

* Removed reward estimation ts-files

* Removed old migration tests

Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: durch <durch@users.noreply.github.com>
2022-04-21 10:36:48 +01:00
Sachin S. Kamath 7c4bb68399 chore: fix dark mode rendering 2022-04-15 16:43:07 +05:30
Dave Hrycyszyn 58f3a96cde Adding security disclosure instructions 2022-04-14 20:44:01 +01:00
Bogdan-Ștefan Neacşu 76a61cb3ae Feature/spend coconut (#1210)
* Import cw3-flex-multisig and cw4-group contracts

* Add release_funds to coconut-bandwidth-contract

* Create contract.rs file

* Add cw multi test and a test that uses it

* Use mnemonic for coconut mode too

* Stricter access to config file, which contains mnemonic

* Update tests

* Remove signed deposits dir after merging that into sql db

* Clippy nits

* More clippy

* Remove backtraces features to pass clippy tests

* Merge the same mnemonic for rewarding and coconut

* Simplify things, letting network monitor use testnet-mode with gateways

* Unify the nymd clients

* Sqlx common storage for buying/consuming credentials

* Link credential storage to credential client

* Trigger rewarded_set update on bootstrap error

* Fix bug on message signing

* Simplify coconut feature in code and set it in validator-api

* Update some local consts

* Link clients to credential storage

* Simplify sql query and change socks5 too

* Update attr handling such that public ones are usable

* Normalize test addresses

* Fix clippy

* Merge storages for (non)coconut creds

* Fmt miss

* Disable wasm client support for now

Co-authored-by: durch <durch@users.noreply.github.com>
2022-04-14 14:25:21 +02:00
Mark Sinclair 94be4c71a4 Update GitHub Action to build nym binaries 2022-04-13 16:22:35 +01:00
Jędrzej Stuczyński 046dd4cbba Bugfix/unique sphinx key (#1207)
* Cosmwasm beta6 => beta8 upgrade

* Introducing additional unique index constraint on sphinx key

* Unit test for checking for duplicate sphinx key

* Fixed other unit tests due to changed constraint
2022-04-13 10:56:39 +01:00
Drazen Urch 485257d29b Add read and write timeouts (#1206) 2022-04-13 11:39:52 +02:00
Jędrzej Stuczyński 37de4bf2f7 Crypto part of the Groth's NIDKG (#1182)
* Work in progress NIDKG

* Encryption of multiple shares

* Extracted baby-step giant-step lookup table as a separate entity

* Proof of discrete log

* Adjusted discrete log domainn

* Producing proof of log during keygen

* Zeroize for epoch

* Proof of secret sharing

* empty main for compiler appeasement

* Construction of proof of chunking

* Initial untested verification of proof of chunking

* Converted chunk responses from Scalar to u64

* Additional tests for proof of chunking

* Minor cleanup and reorganisation

* Fixed enc/dec to use f0

* Deriving node coverage of required tree nodes

* Finally seemingnly working encryption under nonzero epoch

* Branch park

* Decryption key updates to specified epochs

* Ciphertext integrity checks

* Progress in integration tests

* Fixed ciphertext combining and integration test

* Dealing type and simplification of the integration test

* Benchmark for creation of baby-step-giant-step lookup table

* Initial import cleanup + broken 2nd integration test

* Using correct assertions in the integration test (and correctly combining shares)

* Removed unused modules

* Changed proof of sharing to allow for node indices being different from [1,2,...n]

* Reorganised bte module

* Benchmark for g2 precomputation

* Created more strongly typed Epoch type

which is essentially a Tau such that it is a leaf node

* Extending tau with a temporary oracle output

* Using random oracle for tau extension

* More benchmarks!

* encryption-related benchmarks

* Serialization of PublicKeyWithProof

* Typos

* Removed any changes made in validator-api or smart contracts

* Made the integration test slightly more concise

* Further purge of unused modules

* Fixed combining share to use lagrangian interpolation

* Recovery of verification keys from the dealings

* Verification key verification + extended integration tests

* Fixed Tau not being included in digest for producing Tau_h

* Tau serialization

* Serialization of a BTE Node

* Serialization of DecryptionKey

* Serialization of PublicCoefficients

* Utility method for setting constant coefficient of a polynomial

* Serialization of Ciphertexts

* Serialization of Proof of Secret Sharing

* Serialization of Proof of Chunking

* Serialization of Dealing

* Adjusted capacity of responses_r in proof of chunking

* Made notation more consistent with the paper equivalents

* Optional arguments for creating/verifying resharing dealings
2022-04-12 11:59:26 +01:00
Drazen Urch d3372bfc85 Additional, more informative routes (#1204)
* Have reward set updater run its own timer (#1200)

* Have reward set updater run its own timer

* Filter rocket log spam

* Take last day of uptime for rewarding (#1202)

* Take last day of uptime for rewarding

* Rejigger calculations

* Blacklist based on last 24 hr

* Cleanup

* Clippy

* Additional, more informative routes

* Improve blacklist updates

* Fix rewards estimation
2022-04-12 11:55:32 +02:00
Raphaël Walther 25e1bfa345 Moved clean task 2022-04-12 09:26:31 +02:00
dependabot[bot] dc0b9c271c Bump ansi-regex in /docker/typescript_client/upload_contract (#1171)
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 14:07:59 +01:00
216 changed files with 13909 additions and 1647 deletions
+6 -6
View File
@@ -75,12 +75,6 @@ jobs:
command: clippy
args: --workspace --all-targets -- -D warnings
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
# COCONUT stuff
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
@@ -94,6 +88,12 @@ jobs:
command: test
args: --workspace --features=coconut
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
@@ -2,6 +2,9 @@ name: Publish Nym binaries
on:
release:
types: [created]
env:
NETWORK: mainnet
jobs:
publish-nym:
+281
View File
@@ -1,5 +1,285 @@
# Changelog
## [v1.0.0](https://github.com/nymtech/nym/tree/v1.0.0) (2022-05-03)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...v1.0.0)
**Merged pull requests:**
- Feature/show pending delegations [\#1229](https://github.com/nymtech/nym/pull/1229) ([fmtabbara](https://github.com/fmtabbara))
- Bucket inclusion probabilities [\#1224](https://github.com/nymtech/nym/pull/1224) ([durch](https://github.com/durch))
- Create a new bundled delegation when compounding rewards [\#1221](https://github.com/nymtech/nym/pull/1221) ([durch](https://github.com/durch))
## [nym-binaries-1.0.0](https://github.com/nymtech/nym/tree/nym-binaries-1.0.0) (2022-04-27)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.3...nym-binaries-1.0.0)
## [nym-wallet-v1.0.3](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.3) (2022-04-25)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.2...nym-wallet-v1.0.3)
**Fixed bugs:**
- \[Issue\] Wallet 1.0.2 cannot send NYM tokens from a DelayedVestingAccount [\#1215](https://github.com/nymtech/nym/issues/1215)
- Main README not showing properly with GitHub dark mode [\#1211](https://github.com/nymtech/nym/issues/1211)
**Merged pull requests:**
- Bugfix - wallet undelegation for vesting accounts [\#1220](https://github.com/nymtech/nym/pull/1220) ([mmsinclair](https://github.com/mmsinclair))
- Bugfix/delegation reconcile [\#1219](https://github.com/nymtech/nym/pull/1219) ([jstuczyn](https://github.com/jstuczyn))
- Bugfix/query proxied pending delegations [\#1218](https://github.com/nymtech/nym/pull/1218) ([jstuczyn](https://github.com/jstuczyn))
- Using custom gas multiplier in the wallet [\#1217](https://github.com/nymtech/nym/pull/1217) ([jstuczyn](https://github.com/jstuczyn))
- Feature/vesting accounts support [\#1216](https://github.com/nymtech/nym/pull/1216) ([jstuczyn](https://github.com/jstuczyn))
- Release/1.0.0 rc.2 [\#1214](https://github.com/nymtech/nym/pull/1214) ([jstuczyn](https://github.com/jstuczyn))
- chore: fix dark mode rendering [\#1212](https://github.com/nymtech/nym/pull/1212) ([pwnfoo](https://github.com/pwnfoo))
- Feature/spend coconut [\#1210](https://github.com/nymtech/nym/pull/1210) ([neacsu](https://github.com/neacsu))
- Bugfix/unique sphinx key [\#1207](https://github.com/nymtech/nym/pull/1207) ([jstuczyn](https://github.com/jstuczyn))
- Add cache read and write timeouts [\#1206](https://github.com/nymtech/nym/pull/1206) ([durch](https://github.com/durch))
- Additional, more informative routes [\#1204](https://github.com/nymtech/nym/pull/1204) ([durch](https://github.com/durch))
- Feature/aggregated econ dynamics explorer endpoint [\#1203](https://github.com/nymtech/nym/pull/1203) ([jstuczyn](https://github.com/jstuczyn))
- Debugging validator [\#1198](https://github.com/nymtech/nym/pull/1198) ([durch](https://github.com/durch))
- wallet: expose additional validator configuration functionality to the frontend [\#1195](https://github.com/nymtech/nym/pull/1195) ([octol](https://github.com/octol))
- Update rewarding validator address [\#1193](https://github.com/nymtech/nym/pull/1193) ([durch](https://github.com/durch))
- Crypto part of the Groth's NIDKG [\#1182](https://github.com/nymtech/nym/pull/1182) ([jstuczyn](https://github.com/jstuczyn))
- fix unbond page [\#1180](https://github.com/nymtech/nym/pull/1180) ([tommyv1987](https://github.com/tommyv1987))
- Type safe bounds [\#1179](https://github.com/nymtech/nym/pull/1179) ([durch](https://github.com/durch))
- Fix delegation paging [\#1174](https://github.com/nymtech/nym/pull/1174) ([durch](https://github.com/durch))
- Update binaries to rc version [\#1172](https://github.com/nymtech/nym/pull/1172) ([tommyv1987](https://github.com/tommyv1987))
- Bump ansi-regex from 4.1.0 to 4.1.1 in /docker/typescript\_client/upload\_contract [\#1171](https://github.com/nymtech/nym/pull/1171) ([dependabot[bot]](https://github.com/apps/dependabot))
## [nym-binaries-1.0.0-rc.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.0-rc.2) (2022-04-15)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.2...nym-binaries-1.0.0-rc.2)
## [nym-wallet-v1.0.2](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.2) (2022-04-05)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.1...nym-wallet-v1.0.2)
**Merged pull requests:**
- Wallet 1.0.2 visual tweaks [\#1197](https://github.com/nymtech/nym/pull/1197) ([mmsinclair](https://github.com/mmsinclair))
- Password for wallet with routes [\#1196](https://github.com/nymtech/nym/pull/1196) ([fmtabbara](https://github.com/fmtabbara))
- Add auto-updater to Nym Wallet [\#1194](https://github.com/nymtech/nym/pull/1194) ([mmsinclair](https://github.com/mmsinclair))
- Fix clippy warnings for beta toolchain [\#1191](https://github.com/nymtech/nym/pull/1191) ([octol](https://github.com/octol))
- wallet: expose validator urls to the frontend [\#1190](https://github.com/nymtech/nym/pull/1190) ([octol](https://github.com/octol))
- wallet: add test for decrypting stored wallet file [\#1189](https://github.com/nymtech/nym/pull/1189) ([octol](https://github.com/octol))
- Fix clippy warnings [\#1188](https://github.com/nymtech/nym/pull/1188) ([octol](https://github.com/octol))
- Password for wallet with routes [\#1187](https://github.com/nymtech/nym/pull/1187) ([mmsinclair](https://github.com/mmsinclair))
- wallet: add validate\_mnemonic [\#1186](https://github.com/nymtech/nym/pull/1186) ([octol](https://github.com/octol))
- wallet: support removing accounts from the wallet file [\#1185](https://github.com/nymtech/nym/pull/1185) ([octol](https://github.com/octol))
- Feature/adding discord [\#1184](https://github.com/nymtech/nym/pull/1184) ([gala1234](https://github.com/gala1234))
- wallet: config backend for validator selection [\#1183](https://github.com/nymtech/nym/pull/1183) ([octol](https://github.com/octol))
- Add storybook to wallet [\#1178](https://github.com/nymtech/nym/pull/1178) ([mmsinclair](https://github.com/mmsinclair))
- wallet: connection test nymd and api urls independently [\#1170](https://github.com/nymtech/nym/pull/1170) ([octol](https://github.com/octol))
- wallet: wire up account storage [\#1153](https://github.com/nymtech/nym/pull/1153) ([octol](https://github.com/octol))
- Feature/signature on deposit [\#1151](https://github.com/nymtech/nym/pull/1151) ([neacsu](https://github.com/neacsu))
## [nym-wallet-v1.0.1](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.1) (2022-04-05)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.1...nym-wallet-v1.0.1)
**Closed issues:**
- Check enabling bbbc simultaneously with open access. Estimate what it would take to make this the default compilation target. [\#1175](https://github.com/nymtech/nym/issues/1175)
- Get coconut credential for deposited tokens [\#1138](https://github.com/nymtech/nym/issues/1138)
- Make payments lazy [\#1135](https://github.com/nymtech/nym/issues/1135)
- Uptime on node selection for sets [\#1049](https://github.com/nymtech/nym/issues/1049)
## [nym-binaries-1.0.0-rc.1](https://github.com/nymtech/nym/tree/nym-binaries-1.0.0-rc.1) (2022-03-28)
[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.0...nym-binaries-1.0.0-rc.1)
**Fixed bugs:**
- \[Issue\]cargo build --release issue [\#1101](https://github.com/nymtech/nym/issues/1101)
- appimage fail to load in Fedora [\#1098](https://github.com/nymtech/nym/issues/1098)
- \[Issue\] React Example project does not compile when using @nymproject/nym-client-wasm v0.9.0-1 [\#878](https://github.com/nymtech/nym/issues/878)
**Closed issues:**
- Make mainnet coin transfers work [\#1096](https://github.com/nymtech/nym/issues/1096)
- Make Nym wallet validators configurable at runtime [\#1026](https://github.com/nymtech/nym/issues/1026)
- Project Platypus e2e / integration testing [\#942](https://github.com/nymtech/nym/issues/942)
- \[Coconut\]: Replace ElGamal with Pedersen commitments [\#901](https://github.com/nymtech/nym/issues/901)
**Merged pull requests:**
- Different values for mixes and gateways [\#1169](https://github.com/nymtech/nym/pull/1169) ([durch](https://github.com/durch))
- Add global blacklist to validator-cache [\#1168](https://github.com/nymtech/nym/pull/1168) ([durch](https://github.com/durch))
- Feature/upgrade rewarding sandbox [\#1167](https://github.com/nymtech/nym/pull/1167) ([durch](https://github.com/durch))
- Bump node-forge from 1.2.1 to 1.3.0 [\#1165](https://github.com/nymtech/nym/pull/1165) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /nym-wallet/webdriver [\#1164](https://github.com/nymtech/nym/pull/1164) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /clients/tauri-client [\#1163](https://github.com/nymtech/nym/pull/1163) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /clients/webassembly/js-example [\#1162](https://github.com/nymtech/nym/pull/1162) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /clients/native/examples/js-examples/websocket [\#1160](https://github.com/nymtech/nym/pull/1160) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /docker/typescript\_client/upload\_contract [\#1159](https://github.com/nymtech/nym/pull/1159) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/vesting full [\#1158](https://github.com/nymtech/nym/pull/1158) ([fmtabbara](https://github.com/fmtabbara))
- get\_current\_epoch tauri [\#1156](https://github.com/nymtech/nym/pull/1156) ([durch](https://github.com/durch))
- Cleanup [\#1155](https://github.com/nymtech/nym/pull/1155) ([durch](https://github.com/durch))
- Feature flag reward payments [\#1154](https://github.com/nymtech/nym/pull/1154) ([durch](https://github.com/durch))
- Add Query endpoints for calculating rewards [\#1152](https://github.com/nymtech/nym/pull/1152) ([durch](https://github.com/durch))
- Pending endpoints [\#1150](https://github.com/nymtech/nym/pull/1150) ([durch](https://github.com/durch))
- wallet: add logging [\#1149](https://github.com/nymtech/nym/pull/1149) ([octol](https://github.com/octol))
- wallet: use Urls rather than Strings for validator urls [\#1148](https://github.com/nymtech/nym/pull/1148) ([octol](https://github.com/octol))
- Change accumulated reward to Option, migrate delegations [\#1147](https://github.com/nymtech/nym/pull/1147) ([durch](https://github.com/durch))
- wallet: fetch validators url remotely if available [\#1146](https://github.com/nymtech/nym/pull/1146) ([octol](https://github.com/octol))
- Fix delegated\_free calculation [\#1145](https://github.com/nymtech/nym/pull/1145) ([durch](https://github.com/durch))
- Update Nym wallet dependencies to use `ts-packages` [\#1144](https://github.com/nymtech/nym/pull/1144) ([mmsinclair](https://github.com/mmsinclair))
- wallet: try validators one by one if available [\#1143](https://github.com/nymtech/nym/pull/1143) ([octol](https://github.com/octol))
- Update Network Explorer Packages and add mix node identity key copy [\#1142](https://github.com/nymtech/nym/pull/1142) ([mmsinclair](https://github.com/mmsinclair))
- Feature/vesting token pool selector [\#1140](https://github.com/nymtech/nym/pull/1140) ([fmtabbara](https://github.com/fmtabbara))
- Add `ts-packages` for shared Typescript packages [\#1139](https://github.com/nymtech/nym/pull/1139) ([mmsinclair](https://github.com/mmsinclair))
- allow main-net prefix and denom to work [\#1137](https://github.com/nymtech/nym/pull/1137) ([tommyv1987](https://github.com/tommyv1987))
- Upgrade blake3 to v1.3.1 and tauri to 1.0.0-rc.3 [\#1136](https://github.com/nymtech/nym/pull/1136) ([mmsinclair](https://github.com/mmsinclair))
- Bump url-parse from 1.5.7 to 1.5.10 in /clients/native/examples/js-examples/websocket [\#1134](https://github.com/nymtech/nym/pull/1134) ([dependabot[bot]](https://github.com/apps/dependabot))
- Use network explorer map data with disputed areas [\#1133](https://github.com/nymtech/nym/pull/1133) ([Baro1905](https://github.com/Baro1905))
- Feature/vesting UI [\#1132](https://github.com/nymtech/nym/pull/1132) ([fmtabbara](https://github.com/fmtabbara))
- Refactor to a lazy rewarding system [\#1127](https://github.com/nymtech/nym/pull/1127) ([durch](https://github.com/durch))
- Bump ws from 6.2.1 to 6.2.2 in /clients/webassembly/js-example [\#1126](https://github.com/nymtech/nym/pull/1126) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump url-parse from 1.4.7 to 1.5.7 in /clients/webassembly/react-example [\#1125](https://github.com/nymtech/nym/pull/1125) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump url-parse from 1.5.4 to 1.5.7 in /clients/native/examples/js-examples/websocket [\#1124](https://github.com/nymtech/nym/pull/1124) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump url-parse from 1.5.1 to 1.5.7 in /clients/webassembly/js-example [\#1122](https://github.com/nymtech/nym/pull/1122) ([dependabot[bot]](https://github.com/apps/dependabot))
- update contract address [\#1121](https://github.com/nymtech/nym/pull/1121) ([tommyv1987](https://github.com/tommyv1987))
- Refactor GitHub Actions notifications [\#1119](https://github.com/nymtech/nym/pull/1119) ([mmsinclair](https://github.com/mmsinclair))
- Change `pledge` to `bond` in gateway list [\#1118](https://github.com/nymtech/nym/pull/1118) ([mmsinclair](https://github.com/mmsinclair))
- Bump follow-redirects from 1.14.7 to 1.14.8 in /contracts/basic-bandwidth-generation [\#1117](https://github.com/nymtech/nym/pull/1117) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.3 to 1.14.8 in /explorer [\#1116](https://github.com/nymtech/nym/pull/1116) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.5 to 1.14.8 in /nym-wallet [\#1115](https://github.com/nymtech/nym/pull/1115) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.7 to 1.14.8 in /clients/native/examples/js-examples/websocket [\#1114](https://github.com/nymtech/nym/pull/1114) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.7 to 1.14.8 in /testnet-faucet [\#1113](https://github.com/nymtech/nym/pull/1113) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.1 to 1.14.8 in /clients/webassembly/js-example [\#1112](https://github.com/nymtech/nym/pull/1112) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/vesting get current period [\#1111](https://github.com/nymtech/nym/pull/1111) ([durch](https://github.com/durch))
- Bump simple-get from 2.8.1 to 2.8.2 in /contracts/basic-bandwidth-generation [\#1110](https://github.com/nymtech/nym/pull/1110) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump simple-get from 3.1.0 to 3.1.1 in /explorer [\#1109](https://github.com/nymtech/nym/pull/1109) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump simple-get from 3.1.0 to 3.1.1 in /clients/tauri-client [\#1108](https://github.com/nymtech/nym/pull/1108) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump simple-get from 3.1.0 to 3.1.1 in /nym-wallet [\#1107](https://github.com/nymtech/nym/pull/1107) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump node-sass from 4.14.1 to 7.0.0 in /clients/webassembly/react-example [\#1105](https://github.com/nymtech/nym/pull/1105) ([dependabot[bot]](https://github.com/apps/dependabot))
- Fix hardcoded period logic [\#1104](https://github.com/nymtech/nym/pull/1104) ([durch](https://github.com/durch))
- Fixed underflow in rewarding all delegators [\#1099](https://github.com/nymtech/nym/pull/1099) ([jstuczyn](https://github.com/jstuczyn))
- Emit original bond as part of rewarding event [\#1094](https://github.com/nymtech/nym/pull/1094) ([jstuczyn](https://github.com/jstuczyn))
- Add UpdateMixnodeConfigOnBehalf to vestng contract [\#1091](https://github.com/nymtech/nym/pull/1091) ([durch](https://github.com/durch))
- Fixes infinite loops in requests involving pagination [\#1085](https://github.com/nymtech/nym/pull/1085) ([jstuczyn](https://github.com/jstuczyn))
- Removes migration code [\#1071](https://github.com/nymtech/nym/pull/1071) ([jstuczyn](https://github.com/jstuczyn))
- feature/pedersen-commitments [\#1048](https://github.com/nymtech/nym/pull/1048) ([danielementary](https://github.com/danielementary))
- Feature/reuse init owner [\#970](https://github.com/nymtech/nym/pull/970) ([neacsu](https://github.com/neacsu))
## [nym-wallet-v1.0.0](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.0) (2022-02-03)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...nym-wallet-v1.0.0)
**Implemented enhancements:**
- \[Feature Request\] Please enable registration without need for Telegram account [\#1016](https://github.com/nymtech/nym/issues/1016)
- Fast mixnode launch with a pre-built ISO + VM software [\#1001](https://github.com/nymtech/nym/issues/1001)
**Fixed bugs:**
- \[Issue\] [\#1000](https://github.com/nymtech/nym/issues/1000)
- \[Issue\] `nym-client` requires multiple attempts to run a server [\#869](https://github.com/nymtech/nym/issues/869)
- De-'float'-ing `Interval` \(`Display` impl + `serde`\) [\#1065](https://github.com/nymtech/nym/pull/1065) ([jstuczyn](https://github.com/jstuczyn))
- display client address on wallet creation [\#1058](https://github.com/nymtech/nym/pull/1058) ([fmtabbara](https://github.com/fmtabbara))
**Closed issues:**
- Rewarded set inclusion probability API endpoint [\#1037](https://github.com/nymtech/nym/issues/1037)
- Update cw-storage-plus to 0.11 [\#1032](https://github.com/nymtech/nym/issues/1032)
- Change `u128` fields in `RewardEstimationResponse` to `u64` [\#1029](https://github.com/nymtech/nym/issues/1029)
- Test out the mainnet Gravity Bridge [\#1006](https://github.com/nymtech/nym/issues/1006)
- Add vesting contract interface to nym-wallet [\#959](https://github.com/nymtech/nym/issues/959)
- Mixnode crash [\#486](https://github.com/nymtech/nym/issues/486)
**Merged pull requests:**
- create custom urls for mainnet [\#1095](https://github.com/nymtech/nym/pull/1095) ([fmtabbara](https://github.com/fmtabbara))
- Wallet signing on MacOS [\#1093](https://github.com/nymtech/nym/pull/1093) ([mmsinclair](https://github.com/mmsinclair))
- Fix rust 2018 idioms warnings [\#1092](https://github.com/nymtech/nym/pull/1092) ([octol](https://github.com/octol))
- Prevent contract overwriting [\#1090](https://github.com/nymtech/nym/pull/1090) ([durch](https://github.com/durch))
- Logout operation [\#1087](https://github.com/nymtech/nym/pull/1087) ([jstuczyn](https://github.com/jstuczyn))
- Update to rust edition 2021 everywhere [\#1086](https://github.com/nymtech/nym/pull/1086) ([octol](https://github.com/octol))
- Tag contract errors, and print out lines for easier QA [\#1084](https://github.com/nymtech/nym/pull/1084) ([durch](https://github.com/durch))
- Feature/flexible vesting + utility queries [\#1083](https://github.com/nymtech/nym/pull/1083) ([durch](https://github.com/durch))
- Bump @openzeppelin/contracts from 4.3.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1082](https://github.com/nymtech/nym/pull/1082) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nth-check from 2.0.0 to 2.0.1 in /clients/native/examples/js-examples/websocket [\#1081](https://github.com/nymtech/nym/pull/1081) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump url-parse from 1.5.1 to 1.5.4 in /clients/native/examples/js-examples/websocket [\#1080](https://github.com/nymtech/nym/pull/1080) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.1 to 1.14.7 in /clients/native/examples/js-examples/websocket [\#1079](https://github.com/nymtech/nym/pull/1079) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nanoid from 3.1.23 to 3.2.0 in /clients/native/examples/js-examples/websocket [\#1078](https://github.com/nymtech/nym/pull/1078) ([dependabot[bot]](https://github.com/apps/dependabot))
- Setup basic test for mixnode stats reporting [\#1077](https://github.com/nymtech/nym/pull/1077) ([octol](https://github.com/octol))
- Make wallet\_address mandatory for mixnode init [\#1076](https://github.com/nymtech/nym/pull/1076) ([octol](https://github.com/octol))
- Tidy nym-mixnode module visibility [\#1075](https://github.com/nymtech/nym/pull/1075) ([octol](https://github.com/octol))
- Feature/wallet login with password [\#1074](https://github.com/nymtech/nym/pull/1074) ([fmtabbara](https://github.com/fmtabbara))
- Add trait to mock client dependency in DelayForwarder [\#1073](https://github.com/nymtech/nym/pull/1073) ([octol](https://github.com/octol))
- Bump rust-version to latest stable for nym-mixnode [\#1072](https://github.com/nymtech/nym/pull/1072) ([octol](https://github.com/octol))
- Fixes CI for our wasm build [\#1069](https://github.com/nymtech/nym/pull/1069) ([jstuczyn](https://github.com/jstuczyn))
- Add @octol as codeowner [\#1068](https://github.com/nymtech/nym/pull/1068) ([octol](https://github.com/octol))
- set-up inclusion probability [\#1067](https://github.com/nymtech/nym/pull/1067) ([fmtabbara](https://github.com/fmtabbara))
- Feature/wasm client [\#1066](https://github.com/nymtech/nym/pull/1066) ([neacsu](https://github.com/neacsu))
- Changed bech32\_prefix from punk to nymt [\#1064](https://github.com/nymtech/nym/pull/1064) ([jstuczyn](https://github.com/jstuczyn))
- Bump nanoid from 3.1.30 to 3.2.0 in /testnet-faucet [\#1063](https://github.com/nymtech/nym/pull/1063) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump nanoid from 3.1.30 to 3.2.0 in /nym-wallet [\#1062](https://github.com/nymtech/nym/pull/1062) ([dependabot[bot]](https://github.com/apps/dependabot))
- Rework vesting contract storage [\#1061](https://github.com/nymtech/nym/pull/1061) ([durch](https://github.com/durch))
- Mixnet Contract constants extraction [\#1060](https://github.com/nymtech/nym/pull/1060) ([jstuczyn](https://github.com/jstuczyn))
- fix: make explorer footer year dynamic [\#1059](https://github.com/nymtech/nym/pull/1059) ([martinyung](https://github.com/martinyung))
- Add mnemonic just on creation, to display it [\#1057](https://github.com/nymtech/nym/pull/1057) ([neacsu](https://github.com/neacsu))
- Network Explorer: updates to API and UI to show the active set [\#1056](https://github.com/nymtech/nym/pull/1056) ([mmsinclair](https://github.com/mmsinclair))
- Made contract addresses for query NymdClient construction optional [\#1055](https://github.com/nymtech/nym/pull/1055) ([jstuczyn](https://github.com/jstuczyn))
- Introduced RPC query for total token supply [\#1053](https://github.com/nymtech/nym/pull/1053) ([jstuczyn](https://github.com/jstuczyn))
- Feature/tokio console [\#1052](https://github.com/nymtech/nym/pull/1052) ([durch](https://github.com/durch))
- Implemented beta clippy lint recommendations [\#1051](https://github.com/nymtech/nym/pull/1051) ([jstuczyn](https://github.com/jstuczyn))
- add new function to update profit percentage [\#1050](https://github.com/nymtech/nym/pull/1050) ([fmtabbara](https://github.com/fmtabbara))
- Upgrade Clap and use declarative argument parsing for nym-mixnode [\#1047](https://github.com/nymtech/nym/pull/1047) ([octol](https://github.com/octol))
- Feature/additional bond validation [\#1046](https://github.com/nymtech/nym/pull/1046) ([fmtabbara](https://github.com/fmtabbara))
- Fix clippy on relevant lints [\#1044](https://github.com/nymtech/nym/pull/1044) ([neacsu](https://github.com/neacsu))
- Bump shelljs from 0.8.4 to 0.8.5 in /contracts/basic-bandwidth-generation [\#1043](https://github.com/nymtech/nym/pull/1043) ([dependabot[bot]](https://github.com/apps/dependabot))
- Endpoint for rewarded set inclusion probabilities [\#1042](https://github.com/nymtech/nym/pull/1042) ([durch](https://github.com/durch))
- Bump follow-redirects from 1.14.4 to 1.14.7 in /contracts/basic-bandwidth-generation [\#1041](https://github.com/nymtech/nym/pull/1041) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump follow-redirects from 1.14.5 to 1.14.7 in /testnet-faucet [\#1040](https://github.com/nymtech/nym/pull/1040) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/node settings update [\#1036](https://github.com/nymtech/nym/pull/1036) ([fmtabbara](https://github.com/fmtabbara))
- Migrate to cw-storage-plus 0.11.1 [\#1035](https://github.com/nymtech/nym/pull/1035) ([durch](https://github.com/durch))
- Bump @openzeppelin/contracts from 4.4.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1034](https://github.com/nymtech/nym/pull/1034) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/configurable wallet [\#1033](https://github.com/nymtech/nym/pull/1033) ([neacsu](https://github.com/neacsu))
- Feature/downcast reward estimation [\#1031](https://github.com/nymtech/nym/pull/1031) ([durch](https://github.com/durch))
- Wallet UI updates [\#1028](https://github.com/nymtech/nym/pull/1028) ([fmtabbara](https://github.com/fmtabbara))
- Remove migration code [\#1027](https://github.com/nymtech/nym/pull/1027) ([neacsu](https://github.com/neacsu))
- Chore/stricter dependency requirements [\#1025](https://github.com/nymtech/nym/pull/1025) ([jstuczyn](https://github.com/jstuczyn))
- Feature/validator api client endpoints [\#1024](https://github.com/nymtech/nym/pull/1024) ([jstuczyn](https://github.com/jstuczyn))
- Updated cosmrs to 0.4.1 [\#1023](https://github.com/nymtech/nym/pull/1023) ([jstuczyn](https://github.com/jstuczyn))
- Feature/testnet deploy scripts [\#1022](https://github.com/nymtech/nym/pull/1022) ([mfahampshire](https://github.com/mfahampshire))
- Changed wallet's client to a full validator client [\#1021](https://github.com/nymtech/nym/pull/1021) ([jstuczyn](https://github.com/jstuczyn))
- Fix 404 link [\#1020](https://github.com/nymtech/nym/pull/1020) ([RiccardoMasutti](https://github.com/RiccardoMasutti))
- Feature/additional mixnode endpoints [\#1019](https://github.com/nymtech/nym/pull/1019) ([jstuczyn](https://github.com/jstuczyn))
- Introduced denom check when trying to withdraw vested coins [\#1018](https://github.com/nymtech/nym/pull/1018) ([jstuczyn](https://github.com/jstuczyn))
- Add network defaults for qa [\#1017](https://github.com/nymtech/nym/pull/1017) ([neacsu](https://github.com/neacsu))
- Feature/expanded events [\#1015](https://github.com/nymtech/nym/pull/1015) ([jstuczyn](https://github.com/jstuczyn))
- update frontend to use new profit update api [\#1014](https://github.com/nymtech/nym/pull/1014) ([fmtabbara](https://github.com/fmtabbara))
- Feature/node state endpoint [\#1013](https://github.com/nymtech/nym/pull/1013) ([jstuczyn](https://github.com/jstuczyn))
- Feature/hourly set updates [\#1012](https://github.com/nymtech/nym/pull/1012) ([durch](https://github.com/durch))
- Feature/remove unused profit margin [\#1011](https://github.com/nymtech/nym/pull/1011) ([neacsu](https://github.com/neacsu))
- Feature/explorer node status [\#1010](https://github.com/nymtech/nym/pull/1010) ([jstuczyn](https://github.com/jstuczyn))
- Use serial integer instead of random [\#1009](https://github.com/nymtech/nym/pull/1009) ([durch](https://github.com/durch))
- Feature/configure profit [\#1008](https://github.com/nymtech/nym/pull/1008) ([neacsu](https://github.com/neacsu))
- Feature/fix gateway sign [\#1004](https://github.com/nymtech/nym/pull/1004) ([neacsu](https://github.com/neacsu))
- Fix clippy [\#1003](https://github.com/nymtech/nym/pull/1003) ([neacsu](https://github.com/neacsu))
- Update wallet version [\#998](https://github.com/nymtech/nym/pull/998) ([tommyv1987](https://github.com/tommyv1987))
- Fix wallet build instructions [\#997](https://github.com/nymtech/nym/pull/997) ([tommyv1987](https://github.com/tommyv1987))
- Make the separation between testnet-mode and erc20 bandwidth mode clearer [\#994](https://github.com/nymtech/nym/pull/994) ([neacsu](https://github.com/neacsu))
- Bump @openzeppelin/contracts from 3.4.0 to 4.4.1 in /contracts/basic-bandwidth-generation [\#983](https://github.com/nymtech/nym/pull/983) ([dependabot[bot]](https://github.com/apps/dependabot))
- Feature/implicit runtime [\#973](https://github.com/nymtech/nym/pull/973) ([jstuczyn](https://github.com/jstuczyn))
- Differentiate staking and ownership [\#961](https://github.com/nymtech/nym/pull/961) ([durch](https://github.com/durch))
## [v0.12.1](https://github.com/nymtech/nym/tree/v0.12.1) (2021-12-23)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.0...v0.12.1)
**Implemented enhancements:**
- Add version check to binaries [\#967](https://github.com/nymtech/nym/issues/967)
**Fixed bugs:**
- \[Issue\] NYM wallet doesn't work after login [\#995](https://github.com/nymtech/nym/issues/995)
- \[Issue\] [\#993](https://github.com/nymtech/nym/issues/993)
- NYM wallet setup trouble\[Issue\] [\#958](https://github.com/nymtech/nym/issues/958)
## [v0.12.0](https://github.com/nymtech/nym/tree/v0.12.0) (2021-12-21)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.11.0...v0.12.0)
@@ -58,6 +338,7 @@
**Merged pull requests:**
- Update wallet to align with versioning on nodes and gateways [\#991](https://github.com/nymtech/nym/pull/991) ([tommyv1987](https://github.com/tommyv1987))
- Fix success view messages. [\#990](https://github.com/nymtech/nym/pull/990) ([tommyv1987](https://github.com/tommyv1987))
- Feature/enable signature check [\#989](https://github.com/nymtech/nym/pull/989) ([neacsu](https://github.com/neacsu))
- Update mixnet contract address [\#988](https://github.com/nymtech/nym/pull/988) ([neacsu](https://github.com/neacsu))
Generated
+257 -154
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -18,14 +18,15 @@ members = [
"clients/native",
"clients/native/websocket-requests",
"clients/socks5",
# "clients/tauri-client/src-tauri",
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/credential-storage",
"common/coconut-interface",
"common/config",
"common/credentials",
"common/crypto",
"common/crypto/dkg",
"common/bandwidth-claim-contract",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/contracts-common",
+1 -1
View File
@@ -26,7 +26,7 @@ clippy-all-wallet:
cargo clippy --workspace --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
test-main:
cargo test --all-features --workspace
cargo test --all-features --workspace --release
test-contracts:
cargo test --manifest-path contracts/Cargo.toml --all-features
+20 -16
View File
@@ -40,36 +40,40 @@ Node, node operator and delegator rewards are determined according to the princi
|Symbol|Definition|
|---|---|
|<img src="https://render.githubusercontent.com/render/math?math=R">|global share of rewards available, starts at 2% of the reward pool.
|<img src="https://render.githubusercontent.com/render/math?math=R_{i}">|node reward for mixnode `i`.
|<img src="https://render.githubusercontent.com/render/math?math=\sigma_{i}">|ratio of total node stake (node bond + all delegations) to the token circulating supply.
|<img src="https://render.githubusercontent.com/render/math?math=\lambda_{i}">|ratio of stake operator has pledged to their node to the token circulating supply.
|<img src="https://render.githubusercontent.com/render/math?math=\omega_{i}">|fraction of total effort undertaken by node `i`, set to `1/k`.
|<img src="https://render.githubusercontent.com/render/math?math=k">|number of nodes stakeholders are incentivised to create, set by the validators, a matter of governance. Currently determined by the `reward set` size, and set to 720 in testnet Sandbox.
|<img src="https://render.githubusercontent.com/render/math?math=\alpha">|Sybil attack resistance parameter - the higher this parameter is set the stronger the reduction in competitivness gets for a Sybil attacker.
|<img src="https://render.githubusercontent.com/render/math?math=PM_{i}">|declared profit margin of operator `i`, defaults to 10% in.
|<img src="https://render.githubusercontent.com/render/math?math=PF_{i}">|uptime of node `i`, scaled to 0 - 1, for the rewarding epoch
|<img src="https://render.githubusercontent.com/render/math?math=PP_{i}">|cost of operating node `i` for the duration of the rewarding eopoch, set to 40 NYMT.
|<img src="https://render.githubusercontent.com/render/math?math=R#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}R#gh-dark-mode-only">|global share of rewards available, starts at 2% of the reward pool.
|<img src="https://render.githubusercontent.com/render/math?math=R_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}R_{i}#gh-dark-mode-only">|node reward for mixnode `i`.
|<img src="https://render.githubusercontent.com/render/math?math=\sigma_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}\sigma_{i}#gh-dark-mode-only">|ratio of total node stake (node bond + all delegations) to the token circulating supply.
|<img src="https://render.githubusercontent.com/render/math?math=\lambda_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}\lambda_{i}#gh-dark-mode-only">|ratio of stake operator has pledged to their node to the token circulating supply.
|<img src="https://render.githubusercontent.com/render/math?math=\omega_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}\omega_{i}#gh-dark-mode-only">|fraction of total effort undertaken by node `i`, set to `1/k`.
|<img src="https://render.githubusercontent.com/render/math?math=k#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}k#gh-dark-mode-only">|number of nodes stakeholders are incentivised to create, set by the validators, a matter of governance. Currently determined by the `reward set` size, and set to 720 in testnet Sandbox.
|<img src="https://render.githubusercontent.com/render/math?math=\alpha#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}\alpha#gh-dark-mode-only">|Sybil attack resistance parameter - the higher this parameter is set the stronger the reduction in competitivness gets for a Sybil attacker.
|<img src="https://render.githubusercontent.com/render/math?math=PM_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}PM_{i}#gh-dark-mode-only">|declared profit margin of operator `i`, defaults to 10% in.
|<img src="https://render.githubusercontent.com/render/math?math=PF_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}PF_{i}#gh-dark-mode-only">|uptime of node `i`, scaled to 0 - 1, for the rewarding epoch
|<img src="https://render.githubusercontent.com/render/math?math=PP_{i}#gh-light-mode-only"><img src="https://render.githubusercontent.com/render/math?math=\color{white}PP_{i}#gh-dark-mode-only">|cost of operating node `i` for the duration of the rewarding epoch, set to 40 NYMT.
Node reward for node `i` is determined as:
<img src="https://render.githubusercontent.com/render/math?math=R_{i}=PF_{i} \cdot R \cdot (\sigma^'_{i} \cdot \omega_{i} \cdot k %2b \alpha \cdot \lambda^'_{i} \cdot \sigma^'_{i} \cdot k)/(1 %2b \alpha)">
<img src="https://render.githubusercontent.com/render/math?math=R_{i}=PF_{i} \cdot R \cdot (\sigma^'_{i} \cdot \omega_{i} \cdot k %2b \alpha \cdot \lambda^'_{i} \cdot \sigma^'_{i} \cdot k)/(1 %2b \alpha)#gh-light-mode-only">
<img src="https://render.githubusercontent.com/render/math?math=\color{white}R_{i}=PF_{i} \cdot R \cdot (\sigma^'_{i} \cdot \omega_{i} \cdot k %2b \alpha \cdot \lambda^'_{i} \cdot \sigma^'_{i} \cdot k)/(1 %2b \alpha)#gh-dark-mode-only">
where:
<img src="https://render.githubusercontent.com/render/math?math=\sigma^'_{i} = min\{\sigma_{i}, 1/k\}">
<img src="https://render.githubusercontent.com/render/math?math=\sigma^'_{i} = min\{\sigma_{i}, 1/k\}#gh-light-mode-only">
<img src="https://render.githubusercontent.com/render/math?math=\color{white}\sigma^'_{i} = min\{\sigma_{i}, 1/k\}#gh-dark-mode-only">
and
<img src="https://render.githubusercontent.com/render/math?math=\lambda^'_{i} = min\{\lambda_{i}, 1/k\}">
<img src="https://render.githubusercontent.com/render/math?math=\lambda^'_{i} = min\{\lambda_{i}, 1/k\}#gh-light-mode-only">
<img src="https://render.githubusercontent.com/render/math?math=\color{white}\lambda^'_{i} = min\{\lambda_{i}, 1/k\}#gh-dark-mode-only">
Operator of node `i` is credited with the following amount:
<img src="https://render.githubusercontent.com/render/math?math=min\{PP_{i},R_{i})\} %2b max\{0, (PM_{i} %2b (1 - PM_{i}) \cdot \lambda_{i}/\delta_{i}) \cdot (R_{i} - PP_{i})\}">
<img src="https://render.githubusercontent.com/render/math?math=min\{PP_{i},R_{i})\} %2b max\{0, (PM_{i} %2b (1 - PM_{i}) \cdot \lambda_{i}/\delta_{i}) \cdot (R_{i} - PP_{i})\}#gh-light-mode-only">
<img src="https://render.githubusercontent.com/render/math?math=\color{white}min\{PP_{i},R_{i})\} %2b max\{0, (PM_{i} %2b (1 - PM_{i}) \cdot \lambda_{i}/\delta_{i}) \cdot (R_{i} - PP_{i})\}#gh-dark-mode-only">
Delegate with stake `s` recieves:
<img src="https://render.githubusercontent.com/render/math?math=max\{0, (1-PM_{i}) \cdot (s^'/\sigma_{i}) \cdot (R_{i} - PP_{i})\}">
<img src="https://render.githubusercontent.com/render/math?math=max\{0, (1-PM_{i}) \cdot (s^'/\sigma_{i}) \cdot (R_{i} - PP_{i})\}#gh-light-mode-only">
<img src="https://render.githubusercontent.com/render/math?math=\color{white}max\{0, (1-PM_{i}) \cdot (s^'/\sigma_{i}) \cdot (R_{i} - PP_{i})\}#gh-dark-mode-only">
where `s'` is stake `s` scaled over total token circulating supply.
+10
View File
@@ -0,0 +1,10 @@
Critical bug or security issue 💥
If you're here because you're trying to figure out how to notify us of a security issue, go to Discord, and alert the core engineers:
Dave Hrycyszyn futurechimp#5430
Drazen Urch drazen#4873
Jedrzej Stuczynski "Jedrzej | Nym#5666"
Please avoid opening public issues on GitHub that contain information about a potential security vulnerability as this makes it difficult to reduce the impact and harm of valid security issues.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.0.0-rc.1"
version = "1.0.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
+16 -30
View File
@@ -103,22 +103,15 @@ impl<T: NymConfig> Config<T> {
self::Client::<T>::default_reply_encryption_key_store_path(&id);
}
#[cfg(not(feature = "coconut"))]
if self
.client
.backup_bandwidth_token_keys_dir
.as_os_str()
.is_empty()
{
self.client.backup_bandwidth_token_keys_dir =
self::Client::<T>::default_backup_bandwidth_token_keys_dir(&id);
if self.client.database_path.as_os_str().is_empty() {
self.client.database_path = self::Client::<T>::default_database_path(&id);
}
self.client.id = id;
}
pub fn with_testnet_mode(&mut self, testnet_mode: bool) {
self.client.testnet_mode = testnet_mode;
pub fn with_disabled_credentials(&mut self, disabled_credentials_mode: bool) {
self.client.disabled_credentials_mode = disabled_credentials_mode;
}
pub fn with_gateway_endpoint<S: Into<String>>(&mut self, id: S, owner: S, listener: S) {
@@ -161,8 +154,8 @@ impl<T: NymConfig> Config<T> {
self.client.id.clone()
}
pub fn get_testnet_mode(&self) -> bool {
self.client.testnet_mode
pub fn get_disabled_credentials_mode(&self) -> bool {
self.client.disabled_credentials_mode
}
pub fn get_nym_root_directory(&self) -> PathBuf {
@@ -213,9 +206,8 @@ impl<T: NymConfig> Config<T> {
self.client.gateway_endpoint.gateway_listener.clone()
}
#[cfg(not(feature = "coconut"))]
pub fn get_backup_bandwidth_token_keys_dir(&self) -> PathBuf {
self.client.backup_bandwidth_token_keys_dir.clone()
pub fn get_database_path(&self) -> PathBuf {
self.client.database_path.clone()
}
#[cfg(not(feature = "coconut"))]
@@ -302,10 +294,10 @@ pub struct Client<T> {
/// ID specifies the human readable ID of this particular client.
id: String,
/// Indicates whether this client is running in a testnet mode, thus attempting
/// Indicates whether this client is running in a disabled credentials mode, thus attempting
/// to claim bandwidth without presenting bandwidth credentials.
#[serde(default)]
testnet_mode: bool,
disabled_credentials_mode: bool,
/// Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls: Vec<Url>,
@@ -337,11 +329,8 @@ pub struct Client<T> {
/// Information regarding how the client should send data to gateway.
gateway_endpoint: GatewayEndpoint,
/// Path to directory containing public/private keys used for bandwidth token purchase.
/// Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
/// The public key is the name of the file, while the private key is the content.
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: PathBuf,
/// Path to the database containing bandwidth credentials of this client.
database_path: PathBuf,
/// Ethereum private key.
#[cfg(not(feature = "coconut"))]
@@ -365,7 +354,7 @@ impl<T: NymConfig> Default for Client<T> {
Client {
version: env!("CARGO_PKG_VERSION").to_string(),
id: "".to_string(),
testnet_mode: false,
disabled_credentials_mode: true,
validator_api_urls: default_api_endpoints(),
private_identity_key_file: Default::default(),
public_identity_key_file: Default::default(),
@@ -375,8 +364,7 @@ impl<T: NymConfig> Default for Client<T> {
ack_key_file: Default::default(),
reply_encryption_key_store_path: Default::default(),
gateway_endpoint: Default::default(),
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: Default::default(),
database_path: Default::default(),
#[cfg(not(feature = "coconut"))]
eth_private_key: "".to_string(),
#[cfg(not(feature = "coconut"))]
@@ -415,10 +403,8 @@ impl<T: NymConfig> Client<T> {
fn default_reply_encryption_key_store_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("reply_key_store")
}
#[cfg(not(feature = "coconut"))]
fn default_backup_bandwidth_token_keys_dir(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("backup_bandwidth_token_keys")
fn default_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("db.sqlite")
}
}
+1
View File
@@ -20,6 +20,7 @@ tokio = { version = "1.4", features = ["rt-multi-thread", "net", "signal", "macr
coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
coconut-interface = { path = "../../common/coconut-interface" }
credentials = { path = "../../common/credentials" }
credential-storage = { path = "../../common/credential-storage" }
crypto = { path = "../../common/crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
network-defaults = { path = "../../common/network-defaults" }
pemstore = { path = "../../common/pemstore" }
+15 -18
View File
@@ -9,6 +9,8 @@ use std::str::FromStr;
use url::Url;
use coconut_interface::{Attribute, Base58, BlindSignRequest, Bytable, Parameters};
use credential_storage::storage::Storage;
use credential_storage::PersistentStorage;
use credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES};
use credentials::coconut::utils::obtain_aggregate_signature;
use crypto::asymmetric::{encryption, identity};
@@ -32,7 +34,7 @@ pub(crate) enum Commands {
#[async_trait]
pub(crate) trait Execute {
async fn execute(&self, db: &mut PickleDb) -> Result<()>;
async fn execute(&self, db: &mut PickleDb, shared_storage: PersistentStorage) -> Result<()>;
}
#[derive(Args, Clone)]
@@ -44,7 +46,7 @@ pub(crate) struct Deposit {
#[async_trait]
impl Execute for Deposit {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, _shared_storage: PersistentStorage) -> Result<()> {
let mut rng = OsRng;
let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng));
let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng));
@@ -80,7 +82,7 @@ pub(crate) struct ListDeposits {}
#[async_trait]
impl Execute for ListDeposits {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, _shared_storage: PersistentStorage) -> Result<()> {
for kv in db.iter() {
println!("{:?}", kv.get_value::<State>());
}
@@ -102,7 +104,7 @@ pub(crate) struct GetCredential {
#[async_trait]
impl Execute for GetCredential {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, shared_storage: PersistentStorage) -> Result<()> {
let mut state = db
.get::<State>(&self.tx_hash)
.ok_or(CredentialClientError::NoDeposit)?;
@@ -162,6 +164,15 @@ impl Execute for GetCredential {
let signature =
obtain_aggregate_signature(&params, &bandwidth_credential_attributes, &urls).await?;
shared_storage
.insert_coconut_credential(
state.amount.to_string(),
VOUCHER_INFO.to_string(),
bandwidth_credential_attributes.get_private_attributes()[0].to_bs58(),
bandwidth_credential_attributes.get_private_attributes()[1].to_bs58(),
signature.to_bs58(),
)
.await?;
state.signature = Some(signature.to_bs58());
db.set(&self.tx_hash, &state).unwrap();
@@ -170,17 +181,3 @@ impl Execute for GetCredential {
Ok(())
}
}
#[derive(Args, Clone)]
pub(crate) struct SpendCredential {
/// Spend one of the acquired credentials
#[clap(long)]
id: usize,
}
#[async_trait]
impl Execute for SpendCredential {
async fn execute(&self, _db: &mut PickleDb) -> Result<()> {
Ok(())
}
}
+4
View File
@@ -3,6 +3,7 @@
use thiserror::Error;
use credential_storage::error::StorageError;
use credentials::error::Error as CredentialError;
use crypto::asymmetric::encryption::KeyRecoveryError;
use crypto::asymmetric::identity::Ed25519RecoveryError;
@@ -38,4 +39,7 @@ pub enum CredentialClientError {
#[error("Could not parse X25519 data")]
X25519ParseError(#[from] KeyRecoveryError),
#[error("Could not use shared storage")]
SharedStorageError(#[from] StorageError),
}
+7 -5
View File
@@ -15,9 +15,9 @@ cfg_if::cfg_if! {
use clap::Parser;
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
pub const MNEMONIC: &str = "sun surge soon stomach flavor country gorilla dress oblige stamp attract hip soldier agree steel prize nuclear know enjoy arm bargain always theme matter";
pub const MNEMONIC: &str = "jazz fatigue diagram account outer wrist slide cherry mother grid network pause wolf pig round answer mail junior better hair dismiss toward access end";
pub const NYMD_URL: &str = "http://127.0.0.1:26657";
pub const CONTRACT_ADDRESS: &str = "nymt1vhjnzk9ly03dugffvzfcwgry4dgc8x0sscmfl2";
pub const CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s";
pub const SIGNER_AUTHORITIES: [&str; 1] = [
"http://127.0.0.1:8080",
];
@@ -32,6 +32,8 @@ cfg_if::cfg_if! {
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
let shared_storage = credential_storage::initialise_storage(std::path::PathBuf::from("/tmp/credential.db")).await;
let mut db = match PickleDb::load(
"credential.db",
PickleDbDumpPolicy::AutoDump,
@@ -46,9 +48,9 @@ cfg_if::cfg_if! {
};
match &args.command {
Commands::Deposit(m) => m.execute(&mut db).await?,
Commands::ListDeposits(m) => m.execute(&mut db).await?,
Commands::GetCredential(m) => m.execute(&mut db).await?,
Commands::Deposit(m) => m.execute(&mut db, shared_storage).await?,
Commands::ListDeposits(m) => m.execute(&mut db, shared_storage).await?,
Commands::GetCredential(m) => m.execute(&mut db, shared_storage).await?,
}
Ok(())
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.0.0-rc.1"
version = "1.0.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
rust-version = "1.56"
@@ -34,6 +34,7 @@ tokio-tungstenite = "0.14" # websocket
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
credential-storage = { path = "../../common/credential-storage" }
config = { path = "../../common/config" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
@@ -42,12 +43,12 @@ nymsphinx = { path = "../../common/nymsphinx" }
pemstore = { path = "../../common/pemstore" }
topology = { path = "../../common/topology" }
websocket-requests = { path = "websocket-requests" }
validator-client = { path = "../../common/client-libs/validator-client" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut"]
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"]
eth = []
[dev-dependencies]
+4 -6
View File
@@ -19,9 +19,9 @@ version = '{{ client.version }}'
# Human readable ID of this particular client.
id = '{{ client.id }}'
# Indicates whether this client is running in a testnet mode, thus attempting
# Indicates whether this client is running in a disabled credentials mode, thus attempting
# to claim bandwidth without presenting bandwidth credentials.
testnet_mode = {{ client.testnet_mode }}
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls = [
@@ -46,10 +46,8 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to directory containing public/private keys used for bandwidth token purchase.
# Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
# The public key is the name of the file, while the private key is the content.
backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Ethereum private key.
eth_private_key = '{{ client.eth_private_key }}'
+6 -4
View File
@@ -174,14 +174,16 @@ impl NymClient {
#[cfg(feature = "coconut")]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
*self.key_manager.identity_keypair().public_key(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_eth_endpoint(),
self.config.get_base().get_eth_private_key(),
self.config.get_base().get_backup_bandwidth_token_keys_dir(),
)
.expect("Could not create bandwidth controller");
@@ -197,8 +199,8 @@ impl NymClient {
Some(bandwidth_controller),
);
if self.config.get_base().get_testnet_mode() {
gateway_client.set_testnet_mode(true)
if self.config.get_base().get_disabled_credentials_mode() {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
.authenticate_and_start()
+7 -60
View File
@@ -4,21 +4,10 @@
use clap::{App, Arg, ArgMatches};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
#[cfg(feature = "coconut")]
use coconut_interface::{Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::prepare_for_spending, bandwidth::BandwidthVoucher, bandwidth::TOTAL_ATTRIBUTES,
utils::obtain_aggregate_signature,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[cfg(feature = "coconut")]
use network_defaults::{BANDWIDTH_VALUE, VOUCHER_INFO};
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use rand::rngs::OsRng;
@@ -29,16 +18,14 @@ use std::sync::Arc;
use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
#[cfg(feature = "coconut")]
use validator_client::nymd::tx::Hash;
use crate::client::config::Config;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
TESTNET_MODE_ARG_NAME,
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ENABLED_CREDENTIALS_MODE_ARG_NAME,
ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
@@ -79,68 +66,28 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(TESTNET_MODE_ARG_NAME)
.long(TESTNET_MODE_ARG_NAME)
.help("Set this client to work in a testnet mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a disabled credentials mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(TESTNET_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.required(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(TESTNET_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.required(true)
);
app
}
// 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 mut rng = OsRng;
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential =
obtain_aggregate_signature(&params, &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 -3
View File
@@ -5,7 +5,7 @@ use crate::client::config::{Config, SocketType};
use clap::ArgMatches;
use url::Url;
pub(crate) const TESTNET_MODE_ARG_NAME: &str = "testnet-mode";
pub(crate) const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_ENDPOINT_ARG_NAME: &str = "eth_endpoint";
#[cfg(not(feature = "coconut"))]
@@ -72,8 +72,8 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
}
if !cfg!(feature = "eth") || matches.is_present(TESTNET_MODE_ARG_NAME) {
config.get_base_mut().with_testnet_mode(true)
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
}
config
+6 -4
View File
@@ -6,7 +6,9 @@ use crate::client::NymClient;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME, TESTNET_MODE_ARG_NAME};
use crate::commands::{
ENABLED_CREDENTIALS_MODE_ARG_NAME, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
use clap::{App, Arg, ArgMatches};
use config::NymConfig;
use log::*;
@@ -46,9 +48,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(TESTNET_MODE_ARG_NAME)
.long(TESTNET_MODE_ARG_NAME)
.help("Set this client to work in a testnet mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a enabled credentials mode that would attempt to use gateway with bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.0.0-rc.1"
version = "1.0.0"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.56"
@@ -27,6 +27,7 @@ url = "2.2"
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
credential-storage = { path = "../../common/credential-storage" }
config = { path = "../../common/config" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
@@ -37,12 +38,12 @@ socks5-requests = { path = "../../common/socks5/requests" }
topology = { path = "../../common/topology" }
pemstore = { path = "../../common/pemstore" }
proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
validator-client = { path = "../../common/client-libs/validator-client" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut"]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"]
eth = []
[build-dependencies]
+4 -6
View File
@@ -19,9 +19,9 @@ version = '{{ client.version }}'
# Human readable ID of this particular client.
id = '{{ client.id }}'
# Indicates whether this client is running in a testnet mode, thus attempting
# Indicates whether this client is running in a disabled credentials mode, thus attempting
# to claim bandwidth without presenting bandwidth credentials.
testnet_mode = {{ client.testnet_mode }}
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls = [
@@ -46,10 +46,8 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to directory containing public/private keys used for bandwidth token purchase.
# Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
# The public key is the name of the file, while the private key is the content.
backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Ethereum private key.
eth_private_key = '{{ client.eth_private_key }}'
+6 -4
View File
@@ -162,14 +162,16 @@ impl NymClient {
#[cfg(feature = "coconut")]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
*self.key_manager.identity_keypair().public_key(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_eth_endpoint(),
self.config.get_base().get_eth_private_key(),
self.config.get_base().get_backup_bandwidth_token_keys_dir(),
)
.expect("Could not create bandwidth controller");
@@ -185,8 +187,8 @@ impl NymClient {
Some(bandwidth_controller),
);
if self.config.get_base().get_testnet_mode() {
gateway_client.set_testnet_mode(true)
if self.config.get_base().get_disabled_credentials_mode() {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
.authenticate_and_start()
+7 -60
View File
@@ -4,21 +4,10 @@
use clap::{App, Arg, ArgMatches};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
#[cfg(feature = "coconut")]
use coconut_interface::{Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::prepare_for_spending, bandwidth::BandwidthVoucher, bandwidth::TOTAL_ATTRIBUTES,
utils::obtain_aggregate_signature,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[cfg(feature = "coconut")]
use network_defaults::BANDWIDTH_VALUE;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use rand::{prelude::SliceRandom, rngs::OsRng, thread_rng};
@@ -27,16 +16,14 @@ use std::sync::Arc;
use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
#[cfg(feature = "coconut")]
use validator_client::nymd::tx::Hash;
use crate::client::config::Config;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
TESTNET_MODE_ARG_NAME,
DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY, ENABLED_CREDENTIALS_MODE_ARG_NAME,
ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
@@ -79,68 +66,28 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(TESTNET_MODE_ARG_NAME)
.long(TESTNET_MODE_ARG_NAME)
.help("Set this client to work in a testnet mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a enabled credentials mode that would attempt to use gateway with bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
.long(ETH_ENDPOINT_ARG_NAME)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(TESTNET_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_ENDPOINT)
.required(true))
.arg(Arg::with_name(ETH_PRIVATE_KEY_ARG_NAME)
.long(ETH_PRIVATE_KEY_ARG_NAME)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't want to set this value, use --testnet-mode instead")
.takes_value(true)
.default_value_if(TESTNET_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.default_value_if(ENABLED_CREDENTIALS_MODE_ARG_NAME, None, DEFAULT_ETH_PRIVATE_KEY)
.required(true)
);
app
}
// 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 mut rng = OsRng;
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
network_defaults::VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential =
obtain_aggregate_signature(&params, &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 -3
View File
@@ -9,7 +9,7 @@ pub(crate) mod init;
pub(crate) mod run;
pub(crate) mod upgrade;
pub(crate) const TESTNET_MODE_ARG_NAME: &str = "testnet-mode";
pub(crate) const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode";
#[cfg(not(feature = "coconut"))]
pub(crate) const ETH_ENDPOINT_ARG_NAME: &str = "eth_endpoint";
#[cfg(not(feature = "coconut"))]
@@ -68,8 +68,8 @@ pub(crate) fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> C
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
}
if !cfg!(feature = "eth") || matches.is_present(TESTNET_MODE_ARG_NAME) {
config.get_base_mut().with_testnet_mode(true)
if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) {
config.get_base_mut().with_disabled_credentials(false)
}
config
+6 -4
View File
@@ -6,7 +6,9 @@ use crate::client::NymClient;
use crate::commands::override_config;
#[cfg(feature = "eth")]
#[cfg(not(feature = "coconut"))]
use crate::commands::{ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME, TESTNET_MODE_ARG_NAME};
use crate::commands::{
ENABLED_CREDENTIALS_MODE_ARG_NAME, ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME,
};
use clap::{App, Arg, ArgMatches};
use config::NymConfig;
use log::*;
@@ -52,9 +54,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
#[cfg(not(feature = "coconut"))]
let app = app
.arg(
Arg::with_name(TESTNET_MODE_ARG_NAME)
.long(TESTNET_MODE_ARG_NAME)
.help("Set this client to work in a testnet mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
Arg::with_name(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.long(ENABLED_CREDENTIALS_MODE_ARG_NAME)
.help("Set this client to work in a disabled credentials mode that would attempt to use gateway without bandwidth credential requirement. If this value is set, --eth_endpoint and --eth_private_key don't need to be set.")
.conflicts_with_all(&[ETH_ENDPOINT_ARG_NAME, ETH_PRIVATE_KEY_ARG_NAME])
)
.arg(Arg::with_name(ETH_ENDPOINT_ARG_NAME)
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.0.0-rc.1"
version = "1.0.0"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
+11 -14
View File
@@ -26,7 +26,7 @@ const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1_500);
#[wasm_bindgen]
pub struct NymClient {
validator_server: Url,
testnet_mode: bool,
disabled_credentials_mode: bool,
// TODO: technically this doesn't need to be an Arc since wasm is run on a single thread
// however, once we eventually combine this code with the native-client's, it will make things
@@ -72,7 +72,7 @@ impl NymClient {
on_message: None,
on_gateway_connect: None,
testnet_mode: true,
disabled_credentials_mode: true,
}
}
@@ -85,9 +85,12 @@ impl NymClient {
self.on_gateway_connect = Some(on_connect)
}
pub fn set_testnet_mode(&mut self, testnet_mode: bool) {
console_log!("Setting testnet mode to {}", testnet_mode);
self.testnet_mode = testnet_mode;
pub fn set_disabled_credentials_mode(&mut self, disabled_credentials_mode: bool) {
console_log!(
"Setting disabled credentials mode to {}",
disabled_credentials_mode
);
self.disabled_credentials_mode = disabled_credentials_mode;
}
fn self_recipient(&self) -> Recipient {
@@ -107,14 +110,8 @@ impl NymClient {
// Right now it's impossible to have async exported functions to take `&self` rather than self
pub async fn initial_setup(self) -> Self {
let testnet_mode = self.testnet_mode;
let disabled_credentials_mode = self.disabled_credentials_mode;
#[cfg(feature = "coconut")]
let bandwidth_controller = Some(gateway_client::bandwidth::BandwidthController::new(
vec![self.validator_server.clone()],
*self.identity.public_key(),
));
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = None;
let mut client = self.get_and_update_topology().await;
@@ -135,8 +132,8 @@ impl NymClient {
bandwidth_controller,
);
if testnet_mode {
gateway_client.set_testnet_mode(true)
if disabled_credentials_mode {
gateway_client.set_disabled_credentials_mode(true)
}
gateway_client
+5 -2
View File
@@ -17,9 +17,9 @@ url = "2.2"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
secp256k1 = "0.20.3"
web3 = { version = "0.17.0", default-features = false }
async-trait = { version = "0.1.51" }
# internal
cosmrs = { version = "0.4.1", optional = true }
credentials = { path = "../../credentials" }
crypto = { path = "../../crypto" }
gateway-requests = { path = "../../../gateway/gateway-requests" }
@@ -41,6 +41,9 @@ features = ["macros", "rt", "net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.14"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage]
path = "../../credential-storage"
# wasm-only dependencies
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
version = "0.2"
@@ -69,6 +72,6 @@ features = ["js"]
#url = "2.1"
[features]
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut", "cosmrs"]
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"]
wasm = ["web3/wasm", "web3/http", "web3/signing"]
default = ["web3/default"]
@@ -1,33 +1,35 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::{Storage, StorageError};
#[cfg(feature = "coconut")]
use cosmrs::tx::Hash;
use coconut_interface::Base58;
#[cfg(feature = "coconut")]
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::error::StorageError;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::storage::Storage;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::{prepare_for_spending, BandwidthVoucher, TOTAL_ATTRIBUTES},
utils::{obtain_aggregate_signature, obtain_aggregate_verification_key},
bandwidth::prepare_for_spending, utils::obtain_aggregate_verification_key,
};
#[cfg(not(feature = "coconut"))]
use credentials::token::bandwidth::TokenCredential;
#[cfg(feature = "coconut")]
use crypto::asymmetric::encryption;
#[cfg(not(feature = "coconut"))]
use crypto::asymmetric::identity;
use network_defaults::BANDWIDTH_VALUE;
#[cfg(not(feature = "coconut"))]
use network_defaults::{
eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME,
ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME, ETH_ERC20_CONTRACT_ADDRESS,
ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN,
eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE,
ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME,
ETH_ERC20_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN,
};
#[cfg(not(feature = "coconut"))]
use pemstore::traits::PemStorableKeyPair;
#[cfg(not(feature = "coconut"))]
use rand::rngs::OsRng;
#[cfg(not(feature = "coconut"))]
use secp256k1::SecretKey;
#[cfg(not(feature = "coconut"))]
use std::io::{Read, Write};
#[cfg(not(feature = "coconut"))]
use std::str::FromStr;
#[cfg(not(feature = "coconut"))]
use web3::{
@@ -68,35 +70,35 @@ pub fn eth_erc20_contract(web3: Web3<Http>) -> Contract<Http> {
}
#[derive(Clone)]
pub struct BandwidthController {
pub struct BandwidthController<St: Storage> {
storage: St,
#[cfg(feature = "coconut")]
validator_endpoints: Vec<url::Url>,
#[cfg(feature = "coconut")]
identity: identity::PublicKey,
#[cfg(not(feature = "coconut"))]
contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
erc20_contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
eth_private_key: SecretKey,
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: std::path::PathBuf,
}
impl BandwidthController {
impl<St> BandwidthController<St>
where
St: Storage + Clone + 'static,
{
#[cfg(feature = "coconut")]
pub fn new(validator_endpoints: Vec<url::Url>, identity: identity::PublicKey) -> Self {
pub fn new(storage: St, validator_endpoints: Vec<url::Url>) -> Self {
BandwidthController {
storage,
validator_endpoints,
identity,
}
}
#[cfg(not(feature = "coconut"))]
pub fn new(
storage: St,
eth_endpoint: String,
eth_private_key: String,
backup_bandwidth_token_keys_dir: std::path::PathBuf,
) -> Result<Self, GatewayClientError> {
// Fail early, on invalid url
let transport =
@@ -109,60 +111,42 @@ impl BandwidthController {
.map_err(|_| GatewayClientError::InvalidEthereumPrivateKey)?;
Ok(BandwidthController {
storage,
contract,
erc20_contract,
eth_private_key,
backup_bandwidth_token_keys_dir,
})
}
#[cfg(not(feature = "coconut"))]
fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?;
let file_path = self
.backup_bandwidth_token_keys_dir
.join(keypair.public_key().to_base58_string());
let mut file = std::fs::File::create(file_path)?;
file.write_all(&keypair.private_key().to_bytes())?;
async fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
self.storage
.insert_erc20_credential(
keypair.public_key().to_base58_string(),
keypair.private_key().to_base58_string(),
)
.await?;
Ok(())
}
#[cfg(not(feature = "coconut"))]
fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?;
let file = std::fs::read_dir(&self.backup_bandwidth_token_keys_dir)?
.find(|entry| {
entry
.as_ref()
.map(|entry| entry.path().is_file())
.unwrap_or(false)
})
.unwrap_or_else(|| Err(std::io::Error::from(std::io::ErrorKind::NotFound)))?;
let file_path = file.path();
let pub_key = file_path
.file_name()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?
.to_str()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
let mut priv_key = vec![];
std::fs::File::open(file_path.clone())?.read_to_end(&mut priv_key)?;
Ok(identity::KeyPair::from_keys(
identity::PrivateKey::from_bytes(&priv_key).unwrap(),
identity::PublicKey::from_base58_string(pub_key).unwrap(),
))
async fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
let data = self.storage.get_next_erc20_credential().await?;
let public_key = identity::PublicKey::from_base58_string(data.public_key).unwrap();
let private_key = identity::PrivateKey::from_base58_string(data.private_key).unwrap();
Ok(identity::KeyPair::from_keys(private_key, public_key))
}
#[cfg(not(feature = "coconut"))]
fn mark_keypair_as_spent(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
let mut spent_dir = self.backup_bandwidth_token_keys_dir.clone();
spent_dir.push("spent");
std::fs::create_dir_all(&spent_dir)?;
let file_path_old = self
.backup_bandwidth_token_keys_dir
.join(keypair.public_key().to_base58_string());
let file_path_new = spent_dir.join(keypair.public_key().to_base58_string());
std::fs::rename(file_path_old, file_path_new)?;
async fn mark_keypair_as_spent(
&self,
keypair: &identity::KeyPair,
) -> Result<(), GatewayClientError> {
self.storage
.consume_erc20_credential(keypair.public_key().to_base58_string())
.await?;
Ok(())
}
@@ -172,39 +156,24 @@ impl BandwidthController {
&self,
) -> Result<coconut_interface::Credential, GatewayClientError> {
let verification_key = obtain_aggregate_verification_key(&self.validator_endpoints).await?;
let params = coconut_interface::Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let mut rng = OsRng;
// TODO: Decide what is the value and additional info associated with the bandwidth voucher
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
network_defaults::VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential = obtain_aggregate_signature(
&params,
&bandwidth_credential_attributes,
&self.validator_endpoints,
)
.await?;
// the above would presumably be loaded from a file
let bandwidth_credential = self.storage.get_next_coconut_credential().await?;
let voucher_value = u64::from_str(&bandwidth_credential.voucher_value)
.map_err(|_| StorageError::InconsistentData)?;
let voucher_info = bandwidth_credential.voucher_info.clone();
let serial_number =
coconut_interface::Attribute::try_from_bs58(bandwidth_credential.serial_number)?;
let binding_number =
coconut_interface::Attribute::try_from_bs58(bandwidth_credential.binding_number)?;
let signature =
coconut_interface::Signature::try_from_bs58(bandwidth_credential.signature)?;
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
Ok(prepare_for_spending(
&self.identity.to_bytes(),
&bandwidth_credential,
&bandwidth_credential_attributes,
voucher_value,
voucher_info,
serial_number,
binding_number,
&signature,
&verification_key,
)?)
}
@@ -215,12 +184,12 @@ impl BandwidthController {
gateway_identity: identity::PublicKey,
gateway_owner: String,
) -> Result<TokenCredential, GatewayClientError> {
let kp = match self.restore_keypair() {
let kp = match self.restore_keypair().await {
Ok(kp) => kp,
Err(_) => {
let mut rng = OsRng;
let kp = identity::KeyPair::new(&mut rng);
self.backup_keypair(&kp)?;
self.backup_keypair(&kp).await?;
kp
}
};
@@ -230,7 +199,7 @@ impl BandwidthController {
self.buy_token_credential(verification_key, signed_verification_key, gateway_owner)
.await?;
self.mark_keypair_as_spent(&kp)?;
self.mark_keypair_as_spent(&kp).await?;
let message: Vec<u8> = verification_key
.to_bytes()
+14 -10
View File
@@ -9,8 +9,12 @@ pub use crate::packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
};
use crate::socket_state::{PartiallyDelegated, SocketState};
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::PersistentStorage;
#[cfg(feature = "coconut")]
use coconut_interface::Credential;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::PersistentStorage;
#[cfg(not(feature = "coconut"))]
use credentials::token::bandwidth::TokenCredential;
use crypto::asymmetric::identity;
@@ -41,7 +45,7 @@ const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5);
pub struct GatewayClient {
authenticated: bool,
testnet_mode: bool,
disabled_credentials_mode: bool,
bandwidth_remaining: i64,
gateway_address: String,
gateway_identity: identity::PublicKey,
@@ -51,7 +55,7 @@ pub struct GatewayClient {
connection: SocketState,
packet_router: PacketRouter,
response_timeout_duration: Duration,
bandwidth_controller: Option<BandwidthController>,
bandwidth_controller: Option<BandwidthController<PersistentStorage>>,
// reconnection related variables
/// Specifies whether client should try to reconnect to gateway on connection failure.
@@ -75,11 +79,11 @@ impl GatewayClient {
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
response_timeout_duration: Duration,
bandwidth_controller: Option<BandwidthController>,
bandwidth_controller: Option<BandwidthController<PersistentStorage>>,
) -> Self {
GatewayClient {
authenticated: false,
testnet_mode: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
gateway_address,
gateway_identity,
@@ -96,8 +100,8 @@ impl GatewayClient {
}
}
pub fn set_testnet_mode(&mut self, testnet_mode: bool) {
self.testnet_mode = testnet_mode
pub fn set_disabled_credentials_mode(&mut self, disabled_credentials_mode: bool) {
self.disabled_credentials_mode = disabled_credentials_mode
}
// TODO: later convert into proper builder methods
@@ -130,7 +134,7 @@ impl GatewayClient {
GatewayClient {
authenticated: false,
testnet_mode: false,
disabled_credentials_mode: true,
bandwidth_remaining: 0,
gateway_address,
gateway_identity,
@@ -544,13 +548,13 @@ impl GatewayClient {
if self.shared_key.is_none() {
return Err(GatewayClientError::NoSharedKeyAvailable);
}
if self.bandwidth_controller.is_none() && !self.testnet_mode {
if self.bandwidth_controller.is_none() && !self.disabled_credentials_mode {
return Err(GatewayClientError::NoBandwidthControllerAvailable);
}
warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while");
if self.testnet_mode {
info!("The client is running in testnet mode - attempting to claim bandwidth without a credential");
if self.disabled_credentials_mode {
info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential");
return self.try_claim_testnet_bandwidth().await;
}
+14 -4
View File
@@ -1,6 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::StorageError;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::error::StorageError;
use gateway_requests::registration::handshake::error::HandshakeError;
use std::io;
use thiserror::Error;
@@ -21,15 +25,18 @@ pub enum GatewayClientError {
#[error("There was a network error - {0}")]
NetworkError(#[from] WsError),
#[error("There was a credential storage error - {0}")]
CredentialStorageError(#[from] StorageError),
#[cfg(feature = "coconut")]
#[error("Coconut error - {0}")]
CoconutError(#[from] coconut_interface::CoconutError),
// TODO: see if `JsValue` is a reasonable type for this
#[cfg(target_arch = "wasm32")]
#[error("There was a network error")]
NetworkErrorWasm(JsValue),
#[cfg(not(feature = "coconut"))]
#[error("Keypair IO error - {0}")]
IOError(#[from] std::io::Error),
#[cfg(not(feature = "coconut"))]
#[error("Could not burn ERC20 token in Ethereum smart contract - {0}")]
BurnTokenError(#[from] Web3Error),
@@ -69,6 +76,9 @@ pub enum GatewayClientError {
#[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")]
NotEnoughBandwidth(i64, i64),
#[error("There are no more bandwidth credentials acquired. Please buy some more if you want to use the mixnet")]
NoMoreBandwidthCredentials,
#[error("Received an unexpected response")]
UnexpectedResponse,
@@ -13,6 +13,8 @@ pub mod client;
pub mod error;
pub mod packet_router;
pub mod socket_state;
#[cfg(feature = "wasm")]
mod wasm_storage;
/// Helper method for reading from websocket stream. Helps to flatten the structure.
pub(crate) fn cleanup_socket_message(
@@ -0,0 +1,98 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Wasm client is not yet supported")]
WasmNotSupported,
#[allow(dead_code)]
#[error("Code shouldn't reach this point")]
InconsistentData,
}
#[derive(Clone)]
pub struct PersistentStorage {}
pub struct CoconutCredential {
pub id: i64,
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
}
pub struct ERC20Credential {
pub id: i64,
pub public_key: String,
pub private_key: String,
pub consumed: bool,
}
#[async_trait]
pub trait Storage: Send + Sync {
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError>;
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError>;
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>;
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError>;
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError>;
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>;
}
#[async_trait]
impl Storage for PersistentStorage {
async fn insert_coconut_credential(
&self,
_voucher_value: String,
_voucher_info: String,
_serial_number: String,
_binding_number: String,
_signature: String,
) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn remove_coconut_credential(&self, _id: i64) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn insert_erc20_credential(
&self,
_public_key: String,
_private_key: String,
) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn consume_erc20_credential(&self, _public_key: String) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
}
@@ -37,7 +37,7 @@ prost = { version = "0.9", default-features = false, optional = true }
flate2 = { version = "1.0.20", optional = true }
sha2 = { version = "0.9.5", optional = true }
itertools = { version = "0.10", optional = true }
cosmwasm-std = { version = "1.0.0-beta6", optional = true }
cosmwasm-std = { version = "1.0.0-beta8", optional = true }
[dev-dependencies]
ts-rs = "6.1.2"
@@ -153,6 +153,10 @@ impl Client<SigningNymdClient> {
)?;
Ok(())
}
pub fn set_nymd_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.nymd.set_simulated_gas_multiplier(multiplier)
}
}
#[cfg(feature = "nymd-client")]
@@ -260,13 +264,14 @@ impl<C> Client<C> {
&self,
address: String,
mix_identity: IdentityKey,
proxy: Option<String>,
) -> Result<u128, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self
.nymd
.get_delegator_rewards(address, mix_identity)
.get_delegator_rewards(address, mix_identity, proxy)
.await?
.u128())
}
@@ -274,13 +279,14 @@ impl<C> Client<C> {
pub async fn get_pending_delegation_events(
&self,
owner_address: String,
proxy_address: Option<String>,
) -> Result<Vec<DelegationEvent>, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self
.nymd
.get_pending_delegation_events(owner_address)
.get_pending_delegation_events(owner_address, proxy_address)
.await?)
}
@@ -8,9 +8,7 @@ use crate::nymd::cosmwasm_client::types::{
};
use crate::nymd::error::NymdError;
use async_trait::async_trait;
use cosmrs::proto::cosmos::auth::v1beta1::{
BaseAccount, QueryAccountRequest, QueryAccountResponse,
};
use cosmrs::proto::cosmos::auth::v1beta1::{QueryAccountRequest, QueryAccountResponse};
use cosmrs::proto::cosmos::bank::v1beta1::{
QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse,
QueryTotalSupplyRequest, QueryTotalSupplyResponse,
@@ -83,21 +81,16 @@ pub trait CosmWasmClient: rpc::Client {
.make_abci_query::<_, QueryAccountResponse>(path, req)
.await?;
let base_account = res
.account
.map(|account| BaseAccount::decode(account.value.as_ref()))
.transpose()?;
base_account
.map(|base_account| base_account.try_into())
.transpose()
res.account.map(TryFrom::try_from).transpose()
}
async fn get_sequence(&self, address: &AccountId) -> Result<SequenceResponse, NymdError> {
let base_account = self
let account = self
.get_account(address)
.await?
.ok_or_else(|| NymdError::NonExistentAccountError(address.clone()))?;
let base_account = account.try_get_base_account()?;
Ok(SequenceResponse {
account_number: base_account.account_number,
sequence: base_account.sequence,
@@ -3,7 +3,9 @@
use crate::nymd::error::NymdError;
use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse};
use cosmrs::proto::cosmos::base::v1beta1::Coin as ProtoCoin;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::Coin;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
@@ -65,3 +67,14 @@ pub(crate) fn next_page_key(pagination_info: Option<PageResponse>) -> Option<Vec
None
}
pub(crate) fn parse_proto_coin_vec(value: Vec<ProtoCoin>) -> Result<Vec<Coin>, NymdError> {
value
.into_iter()
.map(|proto_coin| {
Coin::try_from(&proto_coin).map_err(|_| NymdError::MalformedCoin {
coin_representation: format!("{:?}", proto_coin),
})
})
.collect()
}
@@ -1,23 +1,34 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: There's a significant argument to pull those out of the package and make a PR on https://github.com/cosmos/cosmos-rust/
use crate::nymd::cosmwasm_client::helpers::parse_proto_coin_vec;
use crate::nymd::cosmwasm_client::logs::Log;
use crate::nymd::error::NymdError;
use cosmrs::crypto::PublicKey;
use cosmrs::proto::cosmos::auth::v1beta1::BaseAccount;
use cosmrs::proto::cosmos::auth::v1beta1::{
BaseAccount as ProtoBaseAccount, ModuleAccount as ProtoModuleAccount,
};
use cosmrs::proto::cosmos::base::abci::v1beta1::{
GasInfo as ProtoGasInfo, Result as ProtoAbciResult,
};
use cosmrs::proto::cosmos::tx::v1beta1::SimulateResponse as ProtoSimulateResponse;
use cosmrs::proto::cosmos::vesting::v1beta1::{
BaseVestingAccount as ProtoBaseVestingAccount,
ContinuousVestingAccount as ProtoContinuousVestingAccount,
DelayedVestingAccount as ProtoDelayedVestingAccount, Period as ProtoPeriod,
PeriodicVestingAccount as ProtoPeriodicVestingAccount,
PermanentLockedAccount as ProtoPermanentLockedAccount,
};
use cosmrs::proto::cosmwasm::wasm::v1::{
CodeInfoResponse, ContractCodeHistoryEntry as ProtoContractCodeHistoryEntry,
ContractCodeHistoryOperationType, ContractInfo as ProtoContractInfo,
};
use cosmrs::tendermint::{abci, chain};
use cosmrs::tx::{AccountNumber, Gas, SequenceNumber};
use cosmrs::{tx, AccountId, Coin};
use cosmrs::{tx, AccountId, Any, Coin};
use prost::Message;
use serde::Serialize;
use std::convert::{TryFrom, TryInto};
@@ -32,8 +43,11 @@ pub struct SequenceResponse {
pub sequence: SequenceNumber,
}
/// BaseAccount defines a base account type. It contains all the necessary fields
/// for basic account functionality. Any custom account type should extend this
/// type for additional functionality (e.g. vesting).
#[derive(Debug)]
pub struct Account {
pub struct BaseAccount {
/// Bech32 account address
pub address: AccountId,
pub pubkey: Option<PublicKey>,
@@ -41,10 +55,10 @@ pub struct Account {
pub sequence: SequenceNumber,
}
impl TryFrom<BaseAccount> for Account {
impl TryFrom<ProtoBaseAccount> for BaseAccount {
type Error = NymdError;
fn try_from(value: BaseAccount) -> Result<Self, Self::Error> {
fn try_from(value: ProtoBaseAccount) -> Result<Self, Self::Error> {
let address: AccountId = value
.address
.parse()
@@ -56,7 +70,7 @@ impl TryFrom<BaseAccount> for Account {
.transpose()
.map_err(|_| NymdError::InvalidPublicKey(address.clone()))?;
Ok(Account {
Ok(BaseAccount {
address,
pubkey,
account_number: value.account_number,
@@ -65,6 +79,261 @@ impl TryFrom<BaseAccount> for Account {
}
}
/// ModuleAccount defines an account for modules that holds coins on a pool.
#[derive(Debug)]
pub struct ModuleAccount {
pub base_account: Option<BaseAccount>,
pub name: String,
pub permissions: Vec<String>,
}
impl TryFrom<ProtoModuleAccount> for ModuleAccount {
type Error = NymdError;
fn try_from(value: ProtoModuleAccount) -> Result<Self, Self::Error> {
let base_account = value.base_account.map(TryFrom::try_from).transpose()?;
Ok(ModuleAccount {
base_account,
name: value.name,
permissions: value.permissions,
})
}
}
/// BaseVestingAccount implements the VestingAccount interface. It contains all
/// the necessary fields needed for any vesting account implementation.
#[derive(Debug)]
pub struct BaseVestingAccount {
pub base_account: Option<BaseAccount>,
pub original_vesting: Vec<Coin>,
pub delegated_free: Vec<Coin>,
pub delegated_vesting: Vec<Coin>,
pub end_time: i64,
}
impl TryFrom<ProtoBaseVestingAccount> for BaseVestingAccount {
type Error = NymdError;
fn try_from(value: ProtoBaseVestingAccount) -> Result<Self, Self::Error> {
let base_account = value.base_account.map(TryFrom::try_from).transpose()?;
let original_vesting = parse_proto_coin_vec(value.original_vesting)?;
let delegated_free = parse_proto_coin_vec(value.delegated_free)?;
let delegated_vesting = parse_proto_coin_vec(value.delegated_vesting)?;
Ok(BaseVestingAccount {
base_account,
original_vesting,
delegated_free,
delegated_vesting,
end_time: value.end_time,
})
}
}
/// ContinuousVestingAccount implements the VestingAccount interface. It
/// continuously vests by unlocking coins linearly with respect to time.
#[derive(Debug)]
pub struct ContinuousVestingAccount {
pub base_vesting_account: Option<BaseVestingAccount>,
pub start_time: i64,
}
impl TryFrom<ProtoContinuousVestingAccount> for ContinuousVestingAccount {
type Error = NymdError;
fn try_from(value: ProtoContinuousVestingAccount) -> Result<Self, Self::Error> {
let base_vesting_account = value
.base_vesting_account
.map(TryFrom::try_from)
.transpose()?;
Ok(ContinuousVestingAccount {
base_vesting_account,
start_time: value.start_time,
})
}
}
/// DelayedVestingAccount implements the VestingAccount interface. It vests all
/// coins after a specific time, but non prior. In other words, it keeps them
/// locked until a specified time.
#[derive(Debug)]
pub struct DelayedVestingAccount {
pub base_vesting_account: Option<BaseVestingAccount>,
}
impl TryFrom<ProtoDelayedVestingAccount> for DelayedVestingAccount {
type Error = NymdError;
fn try_from(value: ProtoDelayedVestingAccount) -> Result<Self, Self::Error> {
let base_vesting_account = value
.base_vesting_account
.map(TryFrom::try_from)
.transpose()?;
Ok(DelayedVestingAccount {
base_vesting_account,
})
}
}
/// Period defines a length of time and amount of coins that will vest.
#[derive(Debug)]
pub struct Period {
pub length: i64,
pub amount: Vec<Coin>,
}
impl TryFrom<ProtoPeriod> for Period {
type Error = NymdError;
fn try_from(value: ProtoPeriod) -> Result<Self, Self::Error> {
Ok(Period {
length: value.length,
amount: parse_proto_coin_vec(value.amount)?,
})
}
}
/// PeriodicVestingAccount implements the VestingAccount interface. It
/// periodically vests by unlocking coins during each specified period.
#[derive(Debug)]
pub struct PeriodicVestingAccount {
pub base_vesting_account: Option<BaseVestingAccount>,
pub start_time: i64,
pub vesting_periods: Vec<Period>,
}
impl TryFrom<ProtoPeriodicVestingAccount> for PeriodicVestingAccount {
type Error = NymdError;
fn try_from(value: ProtoPeriodicVestingAccount) -> Result<Self, Self::Error> {
let base_vesting_account = value
.base_vesting_account
.map(TryFrom::try_from)
.transpose()?;
let vesting_periods = value
.vesting_periods
.into_iter()
.map(TryFrom::try_from)
.collect::<Result<_, _>>()?;
Ok(PeriodicVestingAccount {
base_vesting_account,
start_time: value.start_time,
vesting_periods,
})
}
}
/// PermanentLockedAccount implements the VestingAccount interface. It does
/// not ever release coins, locking them indefinitely. Coins in this account can
/// still be used for delegating and for governance votes even while locked.
#[derive(Debug)]
pub struct PermanentLockedAccount {
pub base_vesting_account: Option<BaseVestingAccount>,
}
impl TryFrom<ProtoPermanentLockedAccount> for PermanentLockedAccount {
type Error = NymdError;
fn try_from(value: ProtoPermanentLockedAccount) -> Result<Self, Self::Error> {
let base_vesting_account = value
.base_vesting_account
.map(TryFrom::try_from)
.transpose()?;
Ok(PermanentLockedAccount {
base_vesting_account,
})
}
}
#[derive(Debug)]
pub enum Account {
Base(BaseAccount),
Module(ModuleAccount),
BaseVesting(BaseVestingAccount),
ContinuousVesting(ContinuousVestingAccount),
DelayedVesting(DelayedVestingAccount),
PeriodicVesting(PeriodicVestingAccount),
PermanentLockedVesting(PermanentLockedAccount),
}
impl Account {
pub fn try_get_base_account(&self) -> Result<&BaseAccount, NymdError> {
match self {
Account::Base(acc) => Ok(acc),
Account::Module(acc) => acc
.base_account
.as_ref()
.ok_or(NymdError::NoBaseAccountInformationAvailable),
Account::BaseVesting(acc) => acc
.base_account
.as_ref()
.ok_or(NymdError::NoBaseAccountInformationAvailable),
Account::ContinuousVesting(acc) => acc
.base_vesting_account
.as_ref()
.and_then(|vesting_acc| vesting_acc.base_account.as_ref())
.ok_or(NymdError::NoBaseAccountInformationAvailable),
Account::DelayedVesting(acc) => acc
.base_vesting_account
.as_ref()
.and_then(|vesting_acc| vesting_acc.base_account.as_ref())
.ok_or(NymdError::NoBaseAccountInformationAvailable),
Account::PeriodicVesting(acc) => acc
.base_vesting_account
.as_ref()
.and_then(|vesting_acc| vesting_acc.base_account.as_ref())
.ok_or(NymdError::NoBaseAccountInformationAvailable),
Account::PermanentLockedVesting(acc) => acc
.base_vesting_account
.as_ref()
.and_then(|vesting_acc| vesting_acc.base_account.as_ref())
.ok_or(NymdError::NoBaseAccountInformationAvailable),
}
}
}
impl TryFrom<Any> for Account {
type Error = NymdError;
fn try_from(raw_account: Any) -> Result<Self, Self::Error> {
match raw_account.type_url.as_ref() {
"/cosmos.auth.v1beta1.BaseAccount" => Ok(Account::Base(
ProtoBaseAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.auth.v1beta1.ModuleAccount" => Ok(Account::Module(
ProtoModuleAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.vesting.v1beta1.BaseVestingAccount" => Ok(Account::BaseVesting(
ProtoBaseVestingAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.vesting.v1beta1.ContinuousVestingAccount" => Ok(Account::ContinuousVesting(
ProtoContinuousVestingAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.vesting.v1beta1.DelayedVestingAccount" => Ok(Account::DelayedVesting(
ProtoDelayedVestingAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.vesting.v1beta1.PeriodicVestingAccount" => Ok(Account::PeriodicVesting(
ProtoPeriodicVestingAccount::decode(raw_account.value.as_ref())?.try_into()?,
)),
"/cosmos.vesting.v1beta1.PermanentLockedAccount" => {
Ok(Account::PermanentLockedVesting(
ProtoPermanentLockedAccount::decode(raw_account.value.as_ref())?.try_into()?,
))
}
_ => Err(NymdError::UnsupportedAccountType {
type_url: raw_account.type_url,
}),
}
}
}
#[derive(Debug)]
pub struct Code {
pub code_id: ContractCodeId,
@@ -108,6 +108,15 @@ pub enum NymdError {
#[error("Abci query failed with code {0} - {1}")]
AbciError(u32, abci::Log),
#[error("Unsupported account type: {type_url}")]
UnsupportedAccountType { type_url: String },
#[error("{coin_representation} is not a valid Cosmos Coin")]
MalformedCoin { coin_representation: String },
#[error("This account does not have BaseAccount information available to it")]
NoBaseAccountInformationAvailable,
}
impl NymdError {
@@ -3,8 +3,8 @@
use crate::nymd::cosmwasm_client::signing_client;
use crate::nymd::cosmwasm_client::types::{
ChangeAdminResult, ContractCodeId, ExecuteResult, InstantiateOptions, InstantiateResult,
MigrateResult, SequenceResponse, UploadResult,
Account, ChangeAdminResult, ContractCodeId, ExecuteResult, InstantiateOptions,
InstantiateResult, MigrateResult, SequenceResponse, UploadResult,
};
use crate::nymd::error::NymdError;
use crate::nymd::wallet::DirectSecp256k1HdWallet;
@@ -171,6 +171,10 @@ impl<C> NymdClient<C> {
.ok_or(NymdError::NoContractAddressAvailable)
}
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.simulated_gas_multiplier = multiplier;
}
pub fn address(&self) -> &AccountId
where
C: SigningCosmWasmClient,
@@ -226,6 +230,16 @@ impl<C> NymdClient<C> {
self.client.get_sequence(self.address()).await
}
pub async fn get_account_details(
&self,
address: &AccountId,
) -> Result<Option<Account>, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
self.client.get_account(address).await
}
pub async fn get_current_block_timestamp(&self) -> Result<TendermintTime, NymdError>
where
C: CosmWasmClient + Sync,
@@ -315,6 +329,7 @@ impl<C> NymdClient<C> {
&self,
address: String,
mix_identity: IdentityKey,
proxy: Option<String>,
) -> Result<Uint128, NymdError>
where
C: CosmWasmClient + Sync,
@@ -322,6 +337,7 @@ impl<C> NymdClient<C> {
let request = QueryMsg::QueryDelegatorReward {
address,
mix_identity,
proxy,
};
self.client
.query_contract_smart(self.mixnet_contract_address()?, &request)
@@ -331,11 +347,15 @@ impl<C> NymdClient<C> {
pub async fn get_pending_delegation_events(
&self,
owner_address: String,
proxy_address: Option<String>,
) -> Result<Vec<DelegationEvent>, NymdError>
where
C: CosmWasmClient + Sync,
{
let request = QueryMsg::GetPendingDelegationEvents { owner_address };
let request = QueryMsg::GetPendingDelegationEvents {
owner_address,
proxy_address,
};
self.client
.query_contract_smart(self.mixnet_contract_address()?, &request)
.await
@@ -256,7 +256,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::DeledateToMixnode",
"VestingContract::DelegateToMixnode",
vec![],
)
.await
@@ -197,38 +197,45 @@ impl DirectSecp256k1HdWalletBuilder {
#[cfg(test)]
mod tests {
use super::*;
use network_defaults::DEFAULT_NETWORK;
use network_defaults::all::Network::*;
#[test]
fn generating_account_addresses() {
let (addr1, addr2, addr3) = match DEFAULT_NETWORK.bech32_prefix() {
"punk" => (
"punk1jw6mp7d5xqc7w6xm79lha27glmd0vdt32a3fj2",
"punk1h5hgn94nsq4kh99rjj794hr5h5q6yfm22mcqqn",
"punk17n9flp6jflljg6fp05dsy07wcprf2uuujse962",
),
"nymt" => (
"nymt1jw6mp7d5xqc7w6xm79lha27glmd0vdt339me94",
"nymt1h5hgn94nsq4kh99rjj794hr5h5q6yfm23rjshv",
"nymt17n9flp6jflljg6fp05dsy07wcprf2uuufgn4d4",
),
_ => panic!("Test needs to be updated with new bech32 prefix"),
};
// test vectors produced from our js wallet
let mnemonic_address = vec![
("crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove", addr1),
("acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel", addr2),
("step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball", addr3)
let mnemonics = vec![
"crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove",
"acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel",
"step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball"
];
let prefixes = vec![
MAINNET.bech32_prefix(),
SANDBOX.bech32_prefix(),
QA.bech32_prefix(),
];
for (mnemonic, address) in mnemonic_address.into_iter() {
let prefix = DEFAULT_NETWORK.bech32_prefix();
let wallet =
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse().unwrap()).unwrap();
assert_eq!(
wallet.try_derive_accounts().unwrap()[0].address,
address.parse().unwrap()
)
for prefix in prefixes {
let addrs = match prefix {
"nymt" => vec![
"nymt1jw6mp7d5xqc7w6xm79lha27glmd0vdt339me94",
"nymt1h5hgn94nsq4kh99rjj794hr5h5q6yfm23rjshv",
"nymt17n9flp6jflljg6fp05dsy07wcprf2uuufgn4d4",
],
"n" => vec![
"n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
"n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf",
],
_ => panic!("Test needs to be updated with new bech32 prefix"),
};
for (idx, mnemonic) in mnemonics.iter().enumerate() {
let wallet =
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse().unwrap())
.unwrap();
assert_eq!(
wallet.try_derive_accounts().unwrap()[0].address,
addrs[idx].parse().unwrap()
)
}
}
}
}
+6
View File
@@ -10,4 +10,10 @@ pub enum CoconutInterfaceError {
#[error("Could not decode base 58 string - {0}")]
MalformedString(#[from] bs58::decode::Error),
#[error("Not enough public attributes were specified")]
NotEnoughPublicAttributes,
#[error("Could not recover bandwidth value")]
InvalidBandwidth,
}
+16 -3
View File
@@ -5,6 +5,7 @@ pub mod error;
use getset::{CopyGetters, Getters};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use error::CoconutInterfaceError;
@@ -24,9 +25,11 @@ impl Credential {
pub fn new(
n_params: u32,
theta: Theta,
public_attributes: Vec<Vec<u8>>,
voucher_value: String,
voucher_info: String,
signature: &Signature,
) -> Credential {
let public_attributes = vec![voucher_value.into_bytes(), voucher_info.into_bytes()];
Credential {
n_params,
theta,
@@ -35,8 +38,18 @@ impl Credential {
}
}
pub fn public_attributes(&self) -> Vec<Vec<u8>> {
self.public_attributes.clone()
pub fn voucher_value(&self) -> Result<u64, CoconutInterfaceError> {
let bandwidth_vec = self
.public_attributes
.get(0)
.ok_or(CoconutInterfaceError::NotEnoughPublicAttributes)?
.to_owned();
let bandwidth_str = String::from_utf8(bandwidth_vec)
.map_err(|_| CoconutInterfaceError::InvalidBandwidth)?;
let value =
u64::from_str(&bandwidth_str).map_err(|_| CoconutInterfaceError::InvalidBandwidth)?;
Ok(value)
}
pub fn verify(&self, verification_key: &VerificationKey) -> bool {
+15 -5
View File
@@ -4,6 +4,8 @@
use handlebars::Handlebars;
use serde::de::DeserializeOwned;
use serde::Serialize;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::{fs, io};
@@ -64,11 +66,19 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
None => fs::create_dir_all(self.config_directory()),
}?;
fs::write(
custom_location
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name())),
templated_config,
)
let location = custom_location
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
fs::write(location.clone(), templated_config)?;
#[cfg(unix)]
let mut perms = fs::metadata(location.clone())?.permissions();
#[cfg(unix)]
perms.set_mode(0o600);
#[cfg(unix)]
fs::set_permissions(location, perms)?;
Ok(())
}
fn load_from_file(id: Option<&str>) -> io::Result<Self> {
@@ -6,5 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta6"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
@@ -1,18 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::deposit::DepositData;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {}
pub struct InstantiateMsg {
pub multisig_addr: String,
pub pool_addr: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
DepositFunds { data: DepositData },
ReleaseFunds { funds: Coin },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -7,4 +7,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta6"
cosmwasm-std = "1.0.0-beta8"
@@ -7,7 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta6"
cosmwasm-std = "1.0.0-beta8"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
@@ -13,4 +13,11 @@ pub enum MixnetContractError {
},
#[error("Error casting from U128")]
CastError,
#[error("{source}")]
StdErr {
#[from]
source: cosmwasm_std::StdError,
},
#[error("Division by zero at {}", line!())]
DivisionByZero,
}
@@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Env;
use schemars::gen::SchemaGenerator;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use std::fmt::{Display, Formatter};
@@ -64,6 +67,39 @@ pub struct Interval {
length: Duration,
}
impl JsonSchema for Interval {
fn schema_name() -> String {
"Interval".to_owned()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema_object = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..SchemaObject::default()
};
let object_validation = schema_object.object();
object_validation
.properties
.insert("id".to_owned(), gen.subschema_for::<u32>());
object_validation.required.insert("id".to_owned());
// PrimitiveDateTime does not implement JsonSchema. However it has a custom
// serialization to string, so we just specify the schema to be String.
object_validation
.properties
.insert("start".to_owned(), gen.subschema_for::<String>());
object_validation.required.insert("start".to_owned());
object_validation
.properties
.insert("length".to_owned(), gen.subschema_for::<Duration>());
object_validation.required.insert("length".to_owned());
Schema::Object(schema_object)
}
}
impl Interval {
/// Initialize epoch in the contract with default values.
pub fn init_epoch(env: Env) -> Self {
@@ -222,13 +222,19 @@ impl DelegatorRewardParams {
}
pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 {
if self.sigma == 0 {
return 0;
}
// change all values into their fixed representations
let delegation_amount = U128::from_num(delegation_amount.u128());
let circulating_supply = U128::from_num(self.reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
// Div by zero checked above
let delegator_reward =
(ONE - self.profit_margin) * scaled_delegation_amount / self.sigma * self.node_profit;
(ONE - self.profit_margin) * (scaled_delegation_amount / self.sigma) * self.node_profit;
let reward = delegator_reward.max(U128::ZERO);
if let Some(int_reward) = reward.checked_cast() {
@@ -250,8 +256,14 @@ impl DelegatorRewardParams {
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct StoredNodeRewardResult {
reward: Uint128,
lambda: Uint128,
sigma: Uint128,
#[schemars(with = "String")]
#[serde(with = "fixed_U128_as_string")]
lambda: U128,
#[schemars(with = "String")]
#[serde(with = "fixed_U128_as_string")]
sigma: U128,
}
impl StoredNodeRewardResult {
@@ -259,11 +271,11 @@ impl StoredNodeRewardResult {
self.reward
}
pub fn lambda(&self) -> Uint128 {
pub fn lambda(&self) -> U128 {
self.lambda
}
pub fn sigma(&self) -> Uint128 {
pub fn sigma(&self) -> U128 {
self.sigma
}
}
@@ -279,18 +291,8 @@ impl TryFrom<NodeRewardResult> for StoredNodeRewardResult {
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
lambda: Uint128::new(
node_reward_result
.lambda()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
sigma: Uint128::new(
node_reward_result
.sigma()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
lambda: node_reward_result.lambda(),
sigma: node_reward_result.sigma(),
})
}
}
@@ -426,17 +428,17 @@ impl MixNodeBond {
&self,
params: &RewardParams,
) -> Result<(u64, u64, u64), MixnetContractError> {
let total_node_reward = self.reward(params);
let total_node_reward = self
.reward(params)
.reward()
.checked_to_num::<u128>()
.unwrap_or_default();
let operator_reward = self.operator_reward(params);
// TODO: This overestimates the reward by a lot, it should take a Uint128 and return estiamte for that
let delegators_reward = self.reward_delegation(self.total_delegation().amount, params);
// Total reward has to be the sum of operator and delegator rewards
let delegators_reward = total_node_reward - operator_reward;
Ok((
total_node_reward
.reward()
.checked_to_num::<u128>()
.unwrap_or_default()
.try_into()?,
total_node_reward.try_into()?,
operator_reward.try_into()?,
delegators_reward.try_into()?,
))
@@ -469,12 +471,16 @@ impl MixNodeBond {
pub fn operator_reward(&self, params: &RewardParams) -> u128 {
let reward = self.reward(params);
if reward.sigma == 0 {
return 0;
}
let profit = if reward.reward < params.node.operator_cost() {
U128::from_num(0u128)
} else {
reward.reward - params.node.operator_cost()
};
let operator_base_reward = reward.reward.min(params.node.operator_cost());
// Div by zero checked above
let operator_reward = (self.profit_margin()
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
* profit;
@@ -171,9 +171,18 @@ pub enum QueryMsg {
QueryDelegatorReward {
address: String,
mix_identity: IdentityKey,
proxy: Option<String>,
},
GetPendingDelegationEvents {
owner_address: String,
proxy_address: Option<String>,
},
GetCheckpointsForMixnode {
mix_identity: IdentityKey,
},
GetMixnodeAtHeight {
mix_identity: IdentityKey,
height: u64,
},
}
@@ -25,11 +25,11 @@ impl NodeEpochRewards {
self.epoch_id
}
pub fn sigma(&self) -> Uint128 {
pub fn sigma(&self) -> U128 {
self.result.sigma()
}
pub fn lambda(&self) -> Uint128 {
pub fn lambda(&self) -> U128 {
self.result.lambda()
}
@@ -57,10 +57,12 @@ impl NodeEpochRewards {
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
let reward = self.node_profit();
let operator_base_reward = reward.min(self.operator_cost());
let operator_reward = (profit_margin
+ (ONE - profit_margin) * U128::from_num(self.lambda().u128())
/ U128::from_num(self.sigma().u128()))
* reward;
let div_by_zero_check = if let Some(value) = self.lambda().checked_div(self.sigma()) {
value
} else {
return Err(MixnetContractError::DivisionByZero);
};
let operator_reward = (profit_margin + (ONE - profit_margin) * div_by_zero_check) * reward;
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128));
@@ -82,9 +84,15 @@ impl NodeEpochRewards {
let circulating_supply = U128::from_num(epoch_reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward = (ONE - profit_margin) * scaled_delegation_amount
/ U128::from_num(self.sigma().u128())
* self.node_profit();
let check_div_by_zero =
if let Some(value) = scaled_delegation_amount.checked_div(self.sigma()) {
value
} else {
return Err(MixnetContractError::DivisionByZero);
};
let delegator_reward = (ONE - profit_margin) * check_div_by_zero * self.node_profit();
let reward = delegator_reward.max(U128::ZERO);
if let Some(int_reward) = reward.checked_cast() {
@@ -201,6 +209,11 @@ impl RewardParams {
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
if denom == 0 {
return U128::ZERO;
}
// Div by zero checked above
if self.in_active_set() {
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
self.active_set_work_factor() / denom * self.rewarded_set_size()
@@ -6,11 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta6"
cosmwasm-std = "1.0.0-beta8"
mixnet-contract-common = { path = "../mixnet-contract" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
cw-storage-plus = "0.13.1"
cw-storage-plus = "0.13.2"
config = { path = "../../config" }
[dev-dependencies]
@@ -49,6 +49,10 @@ impl VestingSpecification {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
CompoundDelegatorReward {
mix_identity: String,
},
CompoundOperatorReward {},
UpdateMixnodeConfig {
profit_margin_percent: u8,
},
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "credential-storage"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
nymcoconut = { path = "../nymcoconut" }
log = "0.4"
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
thiserror = "1.0"
tokio = { version = "1.4", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
[build-dependencies]
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] }
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let database_path = format!("{}/coconut-credential-example.sqlite", out_dir);
let mut conn = SqliteConnection::connect(&*format!("sqlite://{}?mode=rwc", database_path))
.await
.expect("Failed to create SQLx database connection");
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
#[cfg(target_family = "windows")]
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
@@ -0,0 +1,22 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE coconut_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
voucher_value TEXT NOT NULL,
voucher_info TEXT NOT NULL,
serial_number TEXT NOT NULL,
binding_number TEXT NOT NULL,
signature TEXT NOT NULL UNIQUE
);
CREATE TABLE erc20_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
consumed BOOLEAN NOT NULL
);
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoconutCredential;
#[derive(Clone)]
pub(crate) struct CoconutCredentialManager {
connection_pool: sqlx::SqlitePool,
}
impl CoconutCredentialManager {
/// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
CoconutCredentialManager { connection_pool }
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `voucher_value`: Plaintext bandwidth value of the credential.
/// * `voucher_info`: Plaintext information of the credential.
/// * `serial_number`: Base58 representation of the serial number attribute.
/// * `binding_number`: Base58 representation of the binding number attribute.
/// * `signature`: Coconut credential in the form of a signature.
pub(crate) async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coconut_credentials(voucher_value, voucher_info, serial_number, binding_number, signature) VALUES (?, ?, ?, ?, ?)",
voucher_value, voucher_info, serial_number, binding_number, signature
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve one of the stored, unused credentials.
pub(crate) async fn get_next_coconut_credential(
&self,
) -> Result<CoconutCredential, sqlx::Error> {
sqlx::query_as!(CoconutCredential, "SELECT * FROM coconut_credentials")
.fetch_one(&self.connection_pool)
.await
}
/// Removes from the database the specified credential.
///
/// # Arguments
///
/// * `id`: Database id.
pub(crate) async fn remove_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM coconut_credentials WHERE id = ?", id)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::ERC20Credential;
#[derive(Clone)]
pub(crate) struct ERC20CredentialManager {
connection_pool: sqlx::SqlitePool,
}
impl ERC20CredentialManager {
/// Creates new instance of the `ERC20CredentialManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
ERC20CredentialManager { connection_pool }
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `public_key`: Base58 representation of a public key.
/// * `private_key`: Base58 representation of a private key.
pub(crate) async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO erc20_credentials(public_key, private_key, consumed) VALUES (?, ?, ?)",
public_key,
private_key,
false,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve one of the stored, unused credentials.
pub(crate) async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, sqlx::Error> {
sqlx::query_as!(
ERC20Credential,
"SELECT * FROM erc20_credentials WHERE consumed = false"
)
.fetch_one(&self.connection_pool)
.await
}
/// Mark a credential as being consumed.
pub(crate) async fn consume_erc20_credential(
&self,
public_key: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE erc20_credentials
SET consumed = true
WHERE public_key = ?
"#,
public_key
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Database experienced an internal error - {0}")]
InternalDatabaseError(#[from] sqlx::Error),
#[error("Failed to perform database migration - {0}")]
MigrationError(#[from] sqlx::migrate::MigrateError),
#[error("Inconsistent data in database")]
InconsistentData,
}
+144
View File
@@ -0,0 +1,144 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
use crate::coconut::CoconutCredentialManager;
use crate::erc20::ERC20CredentialManager;
use crate::error::StorageError;
use crate::storage::Storage;
use crate::models::{CoconutCredential, ERC20Credential};
use async_trait::async_trait;
use log::{debug, error};
use sqlx::ConnectOptions;
use std::path::{Path, PathBuf};
mod coconut;
mod erc20;
pub mod error;
mod models;
pub mod storage;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct PersistentStorage {
coconut_credential_manager: CoconutCredentialManager,
erc20_credential_manager: ERC20CredentialManager,
}
impl PersistentStorage {
/// Initialises `PersistentStorage` using the provided path.
///
/// # Arguments
///
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(true);
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(err) => {
error!("Failed to connect to SQLx database: {}", err);
return Err(err.into());
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {}", err);
return Err(err.into());
}
Ok(PersistentStorage {
coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()),
erc20_credential_manager: ERC20CredentialManager::new(connection_pool),
})
}
}
#[async_trait]
impl Storage for PersistentStorage {
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_coconut_credential(
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
)
.await?;
Ok(())
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
let credential = self
.coconut_credential_manager
.get_next_coconut_credential()
.await?;
Ok(credential)
}
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
self.coconut_credential_manager
.remove_coconut_credential(id)
.await?;
Ok(())
}
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError> {
self.erc20_credential_manager
.insert_erc20_credential(public_key, private_key)
.await?;
Ok(())
}
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError> {
let credential = self
.erc20_credential_manager
.get_next_erc20_credential()
.await?;
Ok(credential)
}
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError> {
let credential = self
.erc20_credential_manager
.consume_erc20_credential(public_key)
.await?;
Ok(credential)
}
}
pub async fn initialise_storage(path: PathBuf) -> PersistentStorage {
match PersistentStorage::init(path).await {
Err(err) => panic!("failed to initialise credential storage - {}", err),
Ok(storage) => storage,
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub struct CoconutCredential {
#[allow(dead_code)]
pub id: i64,
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
}
pub struct ERC20Credential {
#[allow(dead_code)]
pub id: i64,
pub public_key: String,
pub private_key: String,
pub consumed: bool,
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use crate::models::{CoconutCredential, ERC20Credential};
use crate::StorageError;
#[async_trait]
pub trait Storage: Send + Sync {
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `signature`: Coconut credential in the form of a signature.
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError>;
/// Tries to retrieve one of the stored, unused credentials.
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError>;
/// Removes from the database the specified credential.
///
/// # Arguments
///
/// * `signature`: Coconut credential in the form of a signature.
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>;
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `public_key`: Base58 representation of a public key.
/// * `private_key`: Base58 representation of a private key.
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError>;
/// Tries to retrieve one of the stored, unused credential data.
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError>;
/// Mark a credential as being consumed.
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>;
}
+9 -12
View File
@@ -11,7 +11,6 @@ use coconut_interface::{
PrivateAttribute, PublicAttribute, Signature, VerificationKey,
};
use crypto::asymmetric::{encryption, identity};
use network_defaults::BANDWIDTH_VALUE;
use cosmrs::tx::Hash;
@@ -159,29 +158,27 @@ impl BandwidthVoucher {
pub fn sign(&self, request: &BlindSignRequest) -> identity::Signature {
let mut message = request.to_bytes();
message.extend_from_slice(self.tx_hash.as_bytes());
message.extend_from_slice(self.tx_hash.to_string().as_bytes());
self.signing_key.sign(&message)
}
}
pub fn prepare_for_spending(
raw_identity: &[u8],
voucher_value: u64,
voucher_info: String,
serial_number: PrivateAttribute,
binding_number: PrivateAttribute,
signature: &Signature,
attributes: &BandwidthVoucher,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let public_attributes = vec![
raw_identity.to_vec(),
BANDWIDTH_VALUE.to_be_bytes().to_vec(),
];
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
prepare_credential_for_spending(
&params,
public_attributes,
attributes.serial_number,
attributes.binding_number,
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
verification_key,
)
+6 -4
View File
@@ -11,7 +11,7 @@ use crypto::shared_key::recompute_shared_key;
use crypto::symmetric::stream_cipher;
use url::Url;
use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES};
use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES};
use crate::coconut::params::{
ValidatorApiCredentialEncryptionAlgorithm, ValidatorApiCredentialHkdfAlgorithm,
};
@@ -175,7 +175,8 @@ pub async fn obtain_aggregate_signature(
// TODO: better type flow
pub fn prepare_credential_for_spending(
params: &Parameters,
public_attributes: Vec<Vec<u8>>,
voucher_value: u64,
voucher_info: String,
serial_number: Attribute,
binding_number: Attribute,
signature: &Signature,
@@ -190,9 +191,10 @@ pub fn prepare_credential_for_spending(
)?;
Ok(Credential::new(
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES,
theta,
public_attributes,
voucher_value.to_string(),
voucher_info,
signature,
))
}
+7 -9
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "coconut")]
use coconut_interface::CoconutError;
use coconut_interface::{error::CoconutInterfaceError, CoconutError};
use crypto::asymmetric::encryption::KeyRecoveryError;
use validator_client::ValidatorClientError;
@@ -17,18 +17,16 @@ pub enum Error {
NoValidatorsAvailable,
#[cfg(feature = "coconut")]
#[error("Run into a coconut error - {0}")]
#[error("Ran into a coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Run into a validato client error - {0}")]
#[cfg(feature = "coconut")]
#[error("Ran into a coconut interface error - {0}")]
CoconutInterfaceError(#[from] CoconutInterfaceError),
#[error("Ran into a validator client error - {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("Not enough public attributes were specified")]
NotEnoughPublicAttributes,
#[error("Bandwidth is expected to be represented on 8 bytes")]
InvalidBandwidthSize,
#[error("Bandwidth operation overflowed. {0}")]
BandwidthOverflow(String),
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "dkg"
version = "0.1.0"
edition = "2021"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitvec = "1.0.0"
# unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork
# as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm
bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["alloc", "pairings", "experimental", "zeroize"] }
bs58 = "0.4"
lazy_static = "1.4.0"
rand = { version = "0.8.5", default-features = false}
rand_chacha = "0.3"
rand_core = "0.6.3"
sha2 = "0.9"
serde = "1.0"
serde_derive = "1.0"
thiserror = "1.0"
zeroize = { version = "1.4", features = ["zeroize_derive"] }
[dependencies.group]
version = "0.11"
default-features = false
[dependencies.ff]
version = "0.11"
default-features = false
[dev-dependencies]
criterion = "0.3"
[[bench]]
name = "benchmarks"
harness = false
+541
View File
@@ -0,0 +1,541 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G1Projective, G2Affine, G2Prepared, Scalar};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dkg::bte::encryption::BabyStepGiantStepLookup;
use dkg::bte::proof_chunking::ProofOfChunking;
use dkg::bte::proof_discrete_log::ProofOfDiscreteLog;
use dkg::bte::proof_sharing::ProofOfSecretSharing;
use dkg::bte::{
decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey,
Epoch, PublicKey,
};
use dkg::interpolation::polynomial::Polynomial;
use dkg::{Dealing, NodeIndex, Share};
use ff::Field;
use rand_core::{RngCore, SeedableRng};
use std::collections::BTreeMap;
pub fn precompute_default_bsgs_table(c: &mut Criterion) {
c.bench_function("bsgs default table", |b| {
b.iter(|| black_box(BabyStepGiantStepLookup::default()))
});
}
pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
let g2 = G2Affine::generator();
c.bench_function("precomputing G2Prepared", |b| {
b.iter(|| black_box(G2Prepared::from(g2)))
});
}
fn prepare_keys(
mut rng: impl RngCore,
nodes: usize,
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
let params = setup();
let mut node_indices = (0..nodes).map(|_| rng.next_u64()).collect::<Vec<_>>();
node_indices.sort_unstable();
let mut receivers = BTreeMap::new();
let mut dks = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
dks.push(dk)
}
(receivers, dks)
}
pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 3);
c.bench_function("creating single dealing for 3 parties (threshold 2)", |b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
});
}
pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 3);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 3 parties (threshold 2) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 14;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 20);
c.bench_function(
"creating single dealing for 20 parties (threshold 14)",
|b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
},
);
}
pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 14;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 20);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 20 parties (threshold 14) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 67;
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
c.bench_function(
"creating single dealing for 100 parties (threshold 67)",
|b| {
b.iter(|| {
black_box({
Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
)
})
})
},
);
}
pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 67;
let epoch = Epoch::new(2);
let (receivers, mut dks) = prepare_keys(&mut rng, 100);
let (dealing, _) = Dealing::create(
&mut rng,
&params,
receivers.keys().next().copied().unwrap(),
threshold,
epoch,
&receivers,
);
let first_key = dks.get_mut(0).unwrap();
first_key.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function(
"verifying single dealing made for 100 parties (threshold 67) and recovering share",
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
},
);
}
pub fn creating_proof_of_key_possession(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let x = Scalar::random(&mut rng);
let y = g1 * x;
c.bench_function("creating proof of key possession", |b| {
b.iter(|| black_box(ProofOfDiscreteLog::construct(&mut rng, &y, &x)))
});
}
pub fn verifying_proof_of_key_possession(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let x = Scalar::random(&mut rng);
let y = g1 * x;
let zk_proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x);
c.bench_function("verifying proof of key possession", |b| {
b.iter(|| black_box(zk_proof.verify(&y)))
});
}
pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
c.bench_function("creating proof of chunking for 100 parties", |b| {
b.iter(|| {
let chunking_instance =
proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
black_box(
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking"),
)
})
});
}
pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
let proof_of_chunking =
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking");
c.bench_function("verifying proof of chunking for 100 parties", |b| {
b.iter(|| {
let chunking_instance =
proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
black_box(proof_of_chunking.verify(chunking_instance))
})
});
}
pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
c.bench_function("creating proof of secret sharing for 100 parties", |b| {
b.iter(|| {
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
black_box(
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing"),
)
})
});
}
pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 67);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, &params, &mut rng);
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
let proof_of_secret_sharing =
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing");
c.bench_function("verifying proof of secret sharing for 100 parties", |b| {
b.iter(|| {
let sharing_instance = proof_sharing::Instance::new(
&receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
black_box(proof_of_secret_sharing.verify(sharing_instance))
})
});
}
pub fn single_share_encryption(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (_, pk) = keygen(&params, &mut rng);
let polynomial = Polynomial::new_random(&mut rng, 3);
let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into();
c.bench_function("single share encryption", |b| {
b.iter(|| {
black_box(encrypt_shares(
&[(&share, pk.public_key())],
epoch,
&params,
&mut rng,
))
})
});
}
pub fn share_encryption_100(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (receivers, _) = prepare_keys(&mut rng, 100);
let polynomial = Polynomial::new_random(&mut rng, 3);
let shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
c.bench_function("100 shares encryption", |b| {
b.iter(|| {
black_box(encrypt_shares(
&remote_share_key_pairs,
epoch,
&params,
&mut rng,
))
})
});
}
pub fn share_decryption(c: &mut Criterion) {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let epoch = Epoch::new(2);
let (mut dk, pk) = keygen(&params, &mut rng);
let polynomial = Polynomial::new_random(&mut rng, 3);
let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into();
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, &params, &mut rng);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
c.bench_function("single share decryption", |b| {
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None)))
});
}
criterion_group!(
utils,
precompute_default_bsgs_table,
precomputing_g2_generator_for_miller_loop,
);
criterion_group!(
dealings_creation,
creating_dealing_for_3_parties,
creating_dealing_for_20_parties,
creating_dealing_for_100_parties,
);
// note: in our setting each party will have to create at least 4 dealings (one per attribute in credential)
// and verify 99 * 4 of them (4 from each other dealer)
criterion_group!(
dealings_verification,
verifying_dealing_made_for_3_parties_and_recovering_share,
verifying_dealing_made_for_20_parties_and_recovering_share,
verifying_dealing_made_for_100_parties_and_recovering_share,
);
criterion_group!(
proofs_of_knowledge,
creating_proof_of_key_possession,
verifying_proof_of_key_possession,
creating_proof_of_chunking_for_100_parties,
verifying_proof_of_chunking_for_100_parties,
creating_proof_of_secret_sharing_for_100_parties,
verifying_proof_of_secret_sharing_for_100_parties
);
criterion_group!(
encryption,
single_share_encryption,
share_encryption_100,
share_decryption,
);
criterion_main!(
utils,
dealings_creation,
dealings_verification,
proofs_of_knowledge,
encryption
);
// TODO: benchmark using affine vs projective representation throughout the crate
// (when conversion / serialization / computation is involved)
+772
View File
@@ -0,0 +1,772 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::keys::{DecryptionKey, PublicKey};
use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE};
use crate::error::DkgError;
use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2};
use crate::{Chunk, ChunkedShare, Share};
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
use ff::Field;
use group::{Curve, Group, GroupEncoding};
use rand_core::RngCore;
use std::collections::HashMap;
use std::ops::Neg;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct Ciphertexts {
pub rr: [G1Projective; NUM_CHUNKS],
pub ss: [G1Projective; NUM_CHUNKS],
pub zz: [G2Projective; NUM_CHUNKS],
pub ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
}
impl Ciphertexts {
pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool {
// if this checks fails it means the ciphertext is undefined as values
// in `r`, `s` and `z` are meaningless since technically this ciphertext
// has been created for 0 parties
if self.ciphertext_chunks.is_empty() {
return false;
}
let g1_neg = G1Affine::generator().neg();
let f = epoch
.as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks)
.evaluate_f(params);
// we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid),
// so perform some precomputation on it
let f_prepared = G2Prepared::from(f.to_affine());
// for each triple (R_i, S_i, Z_i) check whether e(g1, Z_i) == e(R_j, f) • e(S_i, h),
// which is equivalent to checking whether e(R_j, f) • e(S_i, h) • e(g1, Z_i)^-1 == id
// and due to bilinear property whether e(R_j, f) • e(S_i, h) • e(g1^-1, Z_i) == id
for i in 0..self.rr.len() {
let miller = bls12_381::multi_miller_loop(&[
(&self.rr[i].to_affine(), &f_prepared),
(&self.ss[i].to_affine(), &params._h_prepared),
(&g1_neg, &G2Prepared::from(self.zz[i].to_affine())),
]);
let res = miller.final_exponentiation();
if !bool::from(res.is_identity()) {
return false;
}
}
true
}
pub fn combine_rs(&self) -> G1Projective {
combine_g1_chunks(&self.rr)
}
// required for the purposes of the proof of secret sharing
pub fn combine_ciphertexts(&self) -> Vec<G1Projective> {
self.ciphertext_chunks
.iter()
.map(|share_ciphertext| combine_g1_chunks(share_ciphertext))
.collect()
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let num_receivers = self.ciphertext_chunks.len();
let mut bytes = Vec::with_capacity(NUM_CHUNKS * ((num_receivers + 2) * 48 + 96) + 4);
for r_i in &self.rr {
bytes.extend_from_slice(r_i.to_bytes().as_ref())
}
for s_i in &self.ss {
bytes.extend_from_slice(s_i.to_bytes().as_ref())
}
for z_i in &self.zz {
bytes.extend_from_slice(z_i.to_bytes().as_ref())
}
bytes.extend_from_slice(&(num_receivers as u32).to_be_bytes());
for c_i in &self.ciphertext_chunks {
for c_ij in c_i {
bytes.extend_from_slice(c_ij.to_bytes().as_ref())
}
}
bytes
}
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// at the very minimum we must have enough bytes for a single receiver
if bytes.len() < NUM_CHUNKS * (3 * 48 + 96) + 4 {
return Err(DkgError::new_deserialization_failure(
"Ciphertexts",
"insufficient number of bytes provided",
));
}
let mut rr = Vec::with_capacity(NUM_CHUNKS);
let mut ss = Vec::with_capacity(NUM_CHUNKS);
let mut zz = Vec::with_capacity(NUM_CHUNKS);
let mut i = 0;
for _ in 0..NUM_CHUNKS {
rr.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.r", "invalid curve point")
})?);
i += 48;
}
for _ in 0..NUM_CHUNKS {
ss.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.s", "invalid curve point")
})?);
i += 48;
}
for _ in 0..NUM_CHUNKS {
zz.push(deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("Ciphertexts.z", "invalid curve point")
})?);
i += 96;
}
let num_receivers = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != num_receivers * NUM_CHUNKS * 48 {
return Err(DkgError::new_deserialization_failure(
"Ciphertexts",
"invalid number of bytes provided",
));
}
let mut ciphertext_chunks = Vec::with_capacity(num_receivers);
for _ in 0..num_receivers {
let mut ci = Vec::with_capacity(NUM_CHUNKS);
for _ in 0..NUM_CHUNKS {
ci.push(deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"Ciphertexts.ciphertext_chunks",
"invalid curve point",
)
})?);
i += 48;
}
// this unwrap is fine as we have exactly NUM_CHUNKS elements in each vector
ciphertext_chunks.push(ci.try_into().unwrap())
}
// and the same is true here, the unwraps are fine as we have exactly NUM_CHUNKS elements in each as required
Ok(Ciphertexts {
rr: rr.try_into().unwrap(),
ss: ss.try_into().unwrap(),
zz: zz.try_into().unwrap(),
ciphertext_chunks,
})
}
}
#[derive(Zeroize)]
#[zeroize(drop)]
/// Randomness generated during ciphertext generation that is required for proofs of knowledge.
/// It must be handled with extreme care as its misuse might help malicious parties to recover
/// the underlying plaintext.
pub struct HazmatRandomness {
r: [Scalar; NUM_CHUNKS],
s: [Scalar; NUM_CHUNKS],
}
impl HazmatRandomness {
pub fn r(&self) -> &[Scalar; NUM_CHUNKS] {
&self.r
}
pub fn s(&self) -> &[Scalar; NUM_CHUNKS] {
&self.s
}
pub fn combine_rs(&self) -> Scalar {
combine_scalar_chunks(&self.r)
}
}
pub fn encrypt_shares(
shares: &[(&Share, &PublicKey)],
epoch: Epoch,
params: &Params,
mut rng: impl RngCore,
) -> (Ciphertexts, HazmatRandomness) {
let g1 = G1Projective::generator();
let mut rand_rs = Vec::with_capacity(NUM_CHUNKS);
let mut rand_ss = Vec::with_capacity(NUM_CHUNKS);
let mut rr = Vec::with_capacity(NUM_CHUNKS);
let mut ss = Vec::with_capacity(NUM_CHUNKS);
// generate relevant re-usable pseudorandom data
for _ in 0..NUM_CHUNKS {
let rand_r = Scalar::random(&mut rng);
let rand_s = Scalar::random(&mut rng);
// g1^r
let rr_i = g1 * rand_r;
// g1^s
let ss_i = g1 * rand_s;
rand_rs.push(rand_r);
rand_ss.push(rand_s);
rr.push(rr_i);
ss.push(ss_i);
}
// produce per-chunk ciphertexts
let mut cc = Vec::with_capacity(shares.len());
for (share, pk) in shares {
let m = share.to_chunks();
let mut ci = Vec::with_capacity(NUM_CHUNKS);
for (j, chunk) in m.chunks.iter().enumerate() {
// can't really have a more efficient implementation until https://github.com/zkcrypto/bls12_381/pull/70 is merged...
let c = pk.0 * rand_rs[j] + g1 * Scalar::from(*chunk as u64);
ci.push(c)
}
// the conversion must succeed since we must have EXACTLY `NUM_CHUNKS` elements
cc.push(ci.try_into().unwrap())
}
// convert into arrays, note that the unwraps are fine as we have exactly `NUM_CHUNKS` elements in each vector
let rr = rr.try_into().unwrap();
let ss = ss.try_into().unwrap();
let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params);
let mut zz = Vec::with_capacity(NUM_CHUNKS);
for i in 0..NUM_CHUNKS {
zz.push(f * rand_rs[i] + params.h * rand_ss[i]);
}
// the conversions here must also succeed since the other vecs also have `NUM_CHUNKS` elements
(
Ciphertexts {
rr,
ss,
zz: zz.try_into().unwrap(),
ciphertext_chunks: cc,
},
HazmatRandomness {
r: rand_rs.try_into().unwrap(),
s: rand_ss.try_into().unwrap(),
},
)
}
pub fn decrypt_share(
dk: &DecryptionKey,
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
i: usize,
ciphertext: &Ciphertexts,
epoch: Epoch,
lookup_table: Option<&BabyStepGiantStepLookup>,
) -> Result<Share, DkgError> {
let mut plaintext = ChunkedShare::default();
let decryption_node = dk.try_get_compatible_node(epoch)?;
let extended_tau = epoch.as_extended_tau(
&ciphertext.rr,
&ciphertext.ss,
&ciphertext.ciphertext_chunks,
);
if i >= ciphertext.ciphertext_chunks.len() {
return Err(DkgError::UnavailableCiphertext(i));
}
let height = decryption_node.tau.height();
let b_neg = decryption_node
.ds
.iter()
.chain(decryption_node.dh.iter())
.zip(extended_tau.0.iter().by_vals().skip(height))
.filter(|(_, i)| *i)
.map(|(d_i, _)| d_i)
.fold(decryption_node.b, |acc, d_i| acc + d_i)
.neg()
.to_affine();
let e_neg = decryption_node.e.neg().to_affine();
for j in 0..NUM_CHUNKS {
let rr_j = &ciphertext.rr[j];
let ss_j = &ciphertext.ss[j];
let zz_j = ciphertext.zz[j].to_affine();
let cc_ij = &ciphertext.ciphertext_chunks[i][j];
let miller = bls12_381::multi_miller_loop(&[
(&cc_ij.to_affine(), &G2_GENERATOR_PREPARED),
(&rr_j.to_affine(), &G2Prepared::from(b_neg)),
(&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)),
(&ss_j.to_affine(), &G2Prepared::from(e_neg)),
]);
let m = miller.final_exponentiation();
plaintext.chunks[j] = baby_step_giant_step(&m, &PAIRING_BASE, lookup_table)?;
}
plaintext.try_into()
}
pub struct BabyStepGiantStepLookup {
base: Gt,
m: Chunk,
lookup: HashMap<[u8; 576], Chunk>,
}
impl BabyStepGiantStepLookup {
pub fn precompute(base: &Gt) -> Self {
let mut lookup = HashMap::new();
let mut g = Gt::identity();
// 1. m ← Ceiling(√n)
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
// 2. For all j where 0 ≤ j < m:
for j in 0..m {
// Compute α^j and store the pair (j, α^j) in a table.
lookup.insert(g.to_uncompressed(), j);
g += base;
}
BabyStepGiantStepLookup {
base: *base,
m,
lookup,
}
}
pub fn try_solve(&self, target: &Gt) -> Result<Chunk, DkgError> {
// 3. Compute α^{m}
let m_neg = Scalar::from(self.m as u64).neg();
let alpha_m = self.base * m_neg;
// 4. γ ← β. (set γ = β)
let mut gamma = *target;
// 5. For all i where 0 ≤ i < m:
for i in 0..self.m {
// 1. Check to see if γ is the second component (αj) of any pair in the table.
if let Some(j) = self.lookup.get(&gamma.to_uncompressed()) {
// 2. If so, return im + j.
return Ok(i * self.m + j);
} else {
// 3. If not, γγα^{m}.
gamma += alpha_m;
}
}
Err(DkgError::UnsolvableDiscreteLog)
}
}
impl Default for BabyStepGiantStepLookup {
fn default() -> Self {
BabyStepGiantStepLookup::precompute(&PAIRING_BASE)
}
}
/// Attempts to solve the discrete log problem g^m, where g is in the Gt group and
/// m should be within the [0, CHUNK_MAX] range.
///
/// The implementation follows the following algorithm: https://en.wikipedia.org/wiki/Baby-step_giant-step#The_algorithm
///
/// # Arguments
///
/// * `target`: the result of the exponentiation, M in M = g^m,
/// * `base`: the base used for exponentiation, g in M = g^m
/// * `lookup_table`: precomputed table containing (j, α^j) pairs
pub fn baby_step_giant_step(
target: &Gt,
base: &Gt,
lookup_table: Option<&BabyStepGiantStepLookup>,
) -> Result<Chunk, DkgError> {
if let Some(lookup_table) = lookup_table {
// compute expected m to make sure the provided lookup is valid
let m = (CHUNK_SIZE as f32).sqrt().ceil() as Chunk;
if &lookup_table.base != base || lookup_table.lookup.len() != m as usize {
return Err(DkgError::MismatchedLookupTable);
}
lookup_table.try_solve(target)
} else {
BabyStepGiantStepLookup::precompute(base).try_solve(target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{keygen, setup, DEFAULT_BSGS_TABLE};
use rand_core::SeedableRng;
fn verify_hazmat_rand(ciphertext: &Ciphertexts, randomness: &HazmatRandomness) {
let g1 = G1Projective::generator();
for i in 0..ciphertext.rr.len() {
assert_eq!(ciphertext.rr[i], g1 * randomness.r[i]);
assert_eq!(ciphertext.ss[i], g1 * randomness.s[i]);
}
}
#[test]
fn baby_giant_100_without_table() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
for i in 0u64..100 {
let base = Gt::random(&mut rng);
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
let target = base * Scalar::from(x);
assert_eq!(
baby_step_giant_step(&target, &base, None).unwrap(),
x as Chunk
);
}
}
#[test]
fn baby_giant_100_with_table() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let base = Gt::random(&mut rng);
let lookup_table = BabyStepGiantStepLookup::precompute(&base);
let table = Some(&lookup_table);
for i in 0u64..100 {
let x = (rng.next_u64() + i) % CHUNK_SIZE as u64;
let target = base * Scalar::from(x);
assert_eq!(
baby_step_giant_step(&target, &base, table).unwrap(),
x as Chunk
);
}
}
#[test]
fn share_decryption_20() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (decryption_key1, public_key1) = keygen(&params, &mut rng);
let (decryption_key2, public_key2) = keygen(&params, &mut rng);
let epoch = Epoch::new(0);
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
}
#[test]
fn share_encryption_under_nonzero_epoch() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key1, public_key1) = keygen(&params, &mut rng);
let (mut decryption_key2, public_key2) = keygen(&params, &mut rng);
let epoch = Epoch::new(12345);
decryption_key1
.try_update_to(epoch, &params, &mut rng)
.unwrap();
decryption_key2
.try_update_to(epoch, &params, &mut rng)
.unwrap();
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)];
let (ciphertext, hazmat) = encrypt_shares(shares, epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
}
#[test]
fn decryption_with_root_key() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (root_key, public_key) = keygen(&params, &mut rng);
let share = Share::random(&mut rng);
let epoch0 = Epoch::new(0);
let epoch42 = Epoch::new(42);
let epoch_big = Epoch::new(3292547435);
let (ciphertext1, hazmat1) =
encrypt_shares(&[(&share, &public_key.key)], epoch0, &params, &mut rng);
verify_hazmat_rand(&ciphertext1, &hazmat1);
let (ciphertext2, hazmat2) =
encrypt_shares(&[(&share, &public_key.key)], epoch42, &params, &mut rng);
verify_hazmat_rand(&ciphertext2, &hazmat2);
let (ciphertext3, hazmat3) =
encrypt_shares(&[(&share, &public_key.key)], epoch_big, &params, &mut rng);
verify_hazmat_rand(&ciphertext3, &hazmat3);
let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap();
let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap();
let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap();
assert_eq!(share, recovered1);
assert_eq!(share, recovered2);
assert_eq!(share, recovered3);
}
#[test]
fn update_and_decrypt_10() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key, public_key) = keygen(&params, &mut rng);
for epoch_value in 0..10 {
let epoch = Epoch::new(epoch_value);
let share = Share::random(&mut rng);
decryption_key
.try_update_to(epoch, &params, &mut rng)
.unwrap();
let (ciphertext, hazmat) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
assert_eq!(share, recovered);
}
}
#[test]
fn reblinding_node_doesnt_affect_decryption() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let (mut decryption_key, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(12345);
decryption_key
.try_update_to(epoch, &params, &mut rng)
.unwrap();
for node in decryption_key.nodes.iter_mut() {
node.reblind(&params, &mut rng);
}
let share = Share::random(&mut rng);
let (ciphertext, hazmat) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap();
assert_eq!(share, recovered);
// attempt to update the key again so we have to derive fresh nodes using previous reblinded results
let epoch2 = Epoch::new(67890);
decryption_key
.try_update_to(epoch2, &params, &mut rng)
.unwrap();
for node in decryption_key.nodes.iter_mut() {
node.reblind(&params, &mut rng);
}
let share2 = Share::random(&mut rng);
let (ciphertext, hazmat) =
encrypt_shares(&[(&share2, &public_key.key)], epoch2, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap();
assert_eq!(share2, recovered);
}
#[test]
fn ciphertext_integrity_check_passes_for_valid_data() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
assert!(ciphertext.verify_integrity(&params, epoch))
}
#[test]
fn ciphertext_integrity_check_passes_fails_for_malformed_data() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
let mut bad_cipher1 = ciphertext.clone();
bad_cipher1.rr[4] = G1Projective::generator();
assert!(!bad_cipher1.verify_integrity(&params, epoch));
let mut bad_cipher2 = ciphertext.clone();
bad_cipher2.ss[4] = G1Projective::generator();
assert!(!bad_cipher2.verify_integrity(&params, epoch));
let mut bad_cipher3 = ciphertext;
bad_cipher3.zz[4] = G2Projective::generator();
assert!(!bad_cipher3.verify_integrity(&params, epoch));
}
#[test]
fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, public_key) = keygen(&params, &mut rng);
let epoch = Epoch::new(1);
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let share = Share::random(&mut rng);
let (ciphertext, _) =
encrypt_shares(&[(&share, &public_key.key)], epoch, &params, &mut rng);
let another_epoch = Epoch::new(2);
assert!(!ciphertext.verify_integrity(&params, another_epoch))
}
#[test]
fn ciphertext_combining() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let nodes = 3;
let mut shares = Vec::new();
let mut public_keys = Vec::new();
for _ in 0..nodes {
shares.push(Share::random(&mut rng));
let (_, pk) = keygen(&params, &mut rng);
public_keys.push(*pk.public_key());
}
let refs = shares.iter().zip(public_keys.iter()).collect::<Vec<_>>();
let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), &params, &mut rng);
let combined_r = combine_scalar_chunks(hazmat.r());
let combined_rr = ciphertext.combine_rs();
let combined_ciphertexts = ciphertext.combine_ciphertexts();
let g1 = G1Projective::generator();
for i in 0..nodes {
let expected = public_keys[i].0 * combined_r + g1 * shares[i].0;
assert_eq!(expected, combined_ciphertexts[i]);
assert_eq!(combined_rr, g1 * combined_r);
}
}
#[test]
fn ciphertexts_roundtrip() {
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
Ciphertexts {
rr: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
ss: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
zz: (0..NUM_CHUNKS)
.map(|_| G2Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
ciphertext_chunks: (0..num_receivers)
.map(|_| {
(0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
.collect::<Vec<_>>()
.try_into()
.unwrap()
})
.collect(),
}
}
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let good_ciphertexts = vec![
random_ciphertexts(&mut rng, 1),
random_ciphertexts(&mut rng, 2),
random_ciphertexts(&mut rng, 10),
];
for ciphertexts in &good_ciphertexts {
let bytes = ciphertexts.to_bytes();
let recovered = Ciphertexts::try_from_bytes(&bytes).unwrap();
assert_eq!(ciphertexts, &recovered);
}
// ciphertext for 0 receivers is invalid by default
let ciphertexts = random_ciphertexts(&mut rng, 0);
let bytes = ciphertexts.to_bytes();
assert!(Ciphertexts::try_from_bytes(&bytes).is_err());
}
}
+875
View File
@@ -0,0 +1,875 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::proof_discrete_log::ProofOfDiscreteLog;
use crate::bte::{Epoch, Params, Tau};
use crate::error::DkgError;
use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use zeroize::Zeroize;
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub(crate) struct Node {
pub(crate) tau: Tau,
// g1^rho
pub(crate) a: G1Projective,
// g2^x
pub(crate) b: G2Projective,
// f_i^rho, up to lambda_t elements
pub(crate) ds: Vec<G2Projective>,
// fh_i^rho, always lambda_h elements
pub(crate) dh: Vec<G2Projective>,
// h^rho
pub(crate) e: G2Projective,
}
impl Node {
fn new_root(
a: G1Projective,
b: G2Projective,
ds: Vec<G2Projective>,
dh: Vec<G2Projective>,
e: G2Projective,
) -> Self {
Node {
tau: Tau::new_root(),
a,
b,
ds,
dh,
e,
}
}
fn is_root(&self) -> bool {
self.tau.0.is_empty()
}
pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) {
let delta = Scalar::random(&mut rng);
self.a += G1Projective::generator() * delta;
// TODO: or do we have to do full tau evaluation here?
self.b += self.tau.evaluate_partial_f(params) * delta;
self.ds
.iter_mut()
.zip(params.fs.iter().skip(self.tau.height()))
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
self.dh
.iter_mut()
.zip(params.fh.iter())
.for_each(|(d_i, f_i)| *d_i += f_i * delta);
self.e += params.h * delta;
}
// note: it's unsafe to use this method outside `try_update_to` as
// we have guaranteed there that `self` is parent of the target
// and that `self.tau != target_tau`
/// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`,
/// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material
/// for its construction.
fn derive_target_child_with_partials(
&self,
params: &Params,
target_tau: Tau,
partial_b: &G2Projective,
partial_f: &G2Projective,
mut rng: impl RngCore,
) -> Self {
debug_assert!(self.tau.is_parent_of(&target_tau));
debug_assert_ne!(self.tau, target_tau);
let delta = Scalar::random(&mut rng);
let a = self.a + G1Projective::generator() * delta;
let b = partial_b + partial_f * delta;
let ds = self
.ds
.iter()
.zip(params.fs.iter())
.skip(target_tau.height())
.map(|(d_i, f_i)| d_i + f_i * delta)
.collect();
let dh = self
.dh
.iter()
.zip(params.fh.iter())
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
.collect();
let e = self.e + params.h * delta;
Node {
tau: target_tau,
a,
b,
ds,
dh,
e,
}
}
// note: it's unsafe to use this method outside `try_update_to` as
// we have guaranteed there that `self` is parent of the target
// and that `self.tau != target_tau`
/// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`,
/// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1`
fn derive_right_nonfinal_child_of_with_partials(
&self,
params: &Params,
most_direct_parent: Tau,
partial_b: &G2Projective,
partial_f: &G2Projective,
mut rng: impl RngCore,
) -> Self {
let right_branch = most_direct_parent.right_child();
debug_assert!(self.tau.is_parent_of(&most_direct_parent));
debug_assert!(self.tau.is_parent_of(&right_branch));
debug_assert_ne!(self.tau, right_branch);
// n is height difference between self and the child
let n = right_branch.height() - self.tau.height();
// i is the index of the last bit we just added
let i = right_branch.height() - 1;
let delta = Scalar::random(&mut rng);
let a = self.a + G1Projective::generator() * delta;
let d0 = self.ds[n - 1];
let b = partial_b + d0 + (partial_f + params.fs[i]) * delta;
let ds = self
.ds
.iter()
.skip(n)
.zip(params.fs.iter().skip(right_branch.height()))
.map(|(d_i, f_i)| d_i + f_i * delta)
.collect();
let dh = self
.dh
.iter()
.zip(params.fh.iter())
.map(|(dh_i, fh_i)| dh_i + fh_i * delta)
.collect();
let e = self.e + params.h * delta;
Node {
tau: right_branch,
a,
b,
ds,
dh,
e,
}
}
// tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let g1_elements = 1;
let g2_elements = self.ds.len() + self.dh.len() + 2;
let tau_bytes = self.tau.to_bytes();
// the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh
let mut bytes =
Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12);
bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes()));
bytes.extend_from_slice(&tau_bytes);
bytes.extend_from_slice(self.a.to_bytes().as_ref());
bytes.extend_from_slice(self.b.to_bytes().as_ref());
bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes()));
for d_i in &self.ds {
bytes.extend_from_slice(d_i.to_bytes().as_ref());
}
bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes()));
for dh_i in &self.dh {
bytes.extend_from_slice(dh_i.to_bytes().as_ref());
}
bytes.extend_from_slice(self.e.to_bytes().as_ref());
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// at the very least we require bytes for:
// - tau_len ( 4 )
// - tau ( could be 0 for root node )
// - a ( 48 )
// - b ( 96 )
// - length indication of ds ( 4 )
// - length indication of dh ( 4 )
// - e ( 96 )
if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided",
));
}
let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize;
let mut i = 4;
let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?;
i += tau_len;
// perform another length check to account for bytes consumed by tau
if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided",
));
}
let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.a", "invalid curve point")
})?;
i += 48;
let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.b", "invalid curve point")
})?;
i += 96;
let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() < ds_len * 96 + 4 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided (ds)",
));
}
let mut ds = Vec::with_capacity(ds_len);
for j in 0..ds_len {
let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
format!("Node.ds_{}", j),
"invalid curve point",
)
})?;
ds.push(d_i);
i += 96;
}
let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != (dh_len + 1) * 96 {
return Err(DkgError::new_deserialization_failure(
"Node",
"insufficient number of bytes provided (dh)",
));
}
let mut dh = Vec::with_capacity(dh_len);
for j in 0..dh_len {
let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
format!("Node.dh_{}", j),
"invalid curve point",
)
})?;
dh.push(dh_i);
i += 96;
}
let e = deserialize_g2(&bytes[i..]).ok_or_else(|| {
DkgError::new_deserialization_failure("Node.h", "invalid curve point")
})?;
Ok(Node {
tau,
a,
b,
ds,
dh,
e,
})
}
}
// produces public key and a decryption key for the root of the tree
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let mut x = Scalar::random(&mut rng);
let y = g1 * x;
let proof = ProofOfDiscreteLog::construct(&mut rng, &y, &x);
let mut rho = Scalar::random(&mut rng);
let a = g1 * rho;
let b = g2 * x + params.f0 * rho;
let ds = params.fs.iter().map(|f_i| f_i * rho).collect();
let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect();
let e = params.h * rho;
let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e));
let public_key = PublicKey(y);
let key_with_proof = PublicKeyWithProof {
key: public_key,
proof,
};
x.zeroize();
rho.zeroize();
(dk, key_with_proof)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PublicKey(pub(crate) G1Projective);
impl PublicKey {
pub fn verify(&self, proof: &ProofOfDiscreteLog) -> bool {
proof.verify(&self.0)
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PublicKeyWithProof {
pub(crate) key: PublicKey,
pub(crate) proof: ProofOfDiscreteLog,
}
impl PublicKeyWithProof {
pub fn verify(&self) -> bool {
self.key.verify(&self.proof)
}
pub fn public_key(&self) -> &PublicKey {
&self.key
}
pub fn to_bytes(&self) -> Vec<u8> {
// we have 2 G1 elements and 1 Scalar
let mut bytes = Vec::with_capacity(2 * 48 + 32);
bytes.extend_from_slice(self.key.0.to_bytes().as_ref());
bytes.extend_from_slice(self.proof.rand_commitment.to_bytes().as_ref());
bytes.extend_from_slice(self.proof.response.to_bytes().as_ref());
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
if bytes.len() != 2 * 48 + 32 {
return Err(DkgError::new_deserialization_failure(
"PublicKeyWithProof",
"provided bytes had invalid length",
));
}
let y_bytes = &bytes[..48];
let commitment_bytes = &bytes[48..96];
let response_bytes = &bytes[96..];
let y = deserialize_g1(y_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure("PublicKeyWithProof.key.0", "invalid curve point")
})?;
let rand_commitment = deserialize_g1(commitment_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicKeyWithProof.proof.rand_commitment",
"invalid curve point",
)
})?;
let response = deserialize_scalar(response_bytes).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicKeyWithProof.proof.response",
"invalid scalar",
)
})?;
Ok(PublicKeyWithProof {
key: PublicKey(y),
proof: ProofOfDiscreteLog {
rand_commitment,
response,
},
})
}
}
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
#[cfg_attr(test, derive(PartialEq))]
pub struct DecryptionKey {
// note that the nodes are ordered from "right" to "left"
pub(crate) nodes: Vec<Node>,
}
impl DecryptionKey {
fn new_root(root_node: Node) -> Self {
DecryptionKey {
nodes: vec![root_node],
}
}
fn current(&self) -> Result<&Node, DkgError> {
// we must have at least a single node, otherwise we have a malformed key
self.nodes.last().ok_or(DkgError::MalformedDecryptionKey)
}
pub fn current_epoch(&self, params: &Params) -> Result<Option<Epoch>, DkgError> {
let current_node = self.current()?;
if current_node.is_root() {
Ok(None)
} else {
Epoch::try_from_tau(&current_node.tau, params).map(Option::Some)
}
}
pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> {
let tau = epoch.as_tau();
self.nodes
.iter()
.rev()
.find(|node| node.tau.is_parent_of(&tau))
.ok_or(DkgError::ExpiredKey)
}
pub fn try_update_to_next_epoch(
&mut self,
params: &Params,
mut rng: impl RngCore,
) -> Result<(), DkgError> {
if self.nodes.is_empty() {
return Err(DkgError::MalformedDecryptionKey);
}
let mut target_epoch = Epoch::new(0);
if self.nodes.len() == 1 && self.nodes[0].is_root() {
return self.try_update_to(target_epoch, params, &mut rng);
}
// unwrap is fine as we have asserted self.nodes is not empty
self.nodes.pop().unwrap();
if let Some(tail) = self.nodes.last() {
target_epoch = tail.tau.lowest_valid_epoch_child(params)?;
} else {
// essentially our key consisted of only a single node and it wasn't a root,
// so either it was malformed or we somehow reached the final epoch and wanted to update
// beyond that. Either way, update to l + 1 is impossible
return Err(DkgError::MalformedDecryptionKey);
}
self.try_update_to(target_epoch, params, &mut rng)
}
/// Attempts to update `self` to the provided `epoch`. If the update is not possible,
/// because the target was in the past or the key is malformed, an error is returned.
///
/// Note that this method mutates the key in place and if the original key was malformed,
/// there are no guarantees about its internal state post-call.
pub fn try_update_to(
&mut self,
target_epoch: Epoch,
params: &Params,
mut rng: impl RngCore,
) -> Result<(), DkgError> {
if self.nodes.is_empty() {
// somehow we have an empty decryption key
return Err(DkgError::MalformedDecryptionKey);
}
// makes it easier to work with since we will be generating non-leaf nodes
let target_tau = target_epoch.as_tau();
let current_tau = &self.current()?.tau;
if current_tau == &target_tau {
// our key is already updated to the target
return Ok(());
}
if current_tau > &target_tau {
// we cannot derive keys for past epochs
return Err(DkgError::TargetEpochUpdateInThePast);
}
// drop the nodes that are no longer required and get the most direct parent for the target epoch available
let mut parent = loop {
// if pop() fails the key is malformed since we checked that the target_epoch > current_epoch,
// hence the update should have been possible
let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?;
if tail.tau.is_parent_of(&target_tau) {
break tail;
}
};
// essentially the case of updating epoch n to n + 1, where n is even;
// in that case the last two nodes are [..., epoch_{n+1}, epoch_n]
// so we just have to reblind the n+1 node and we're done
if parent.tau == target_tau {
parent.reblind(params, &mut rng);
self.nodes.push(parent);
return Ok(());
}
// accumulators, note that the previous elements have already been included by the parent,
// i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l}
// new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n}
// new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t)
let mut new_b_accumulator = parent.b;
let mut new_f_accumulator = parent.tau.evaluate_partial_f(params);
let parent_height = parent.tau.height();
// path from the parent to the child
for (n, bit) in target_tau
.0
.iter()
.by_vals()
.skip(parent.tau.height())
.enumerate()
{
// ith bit of the [child] epoch
// note that n represents height difference between parent and the current bit
let i = n + parent_height;
// if the bit is NOT set, push the right '1' subtree (for future keys)
// so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`,
// in the first loop iteration we're going to look at bit `0` and
// derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1`
// in the next loop iteration we're going to look at bit `1` and simply update the accumulators,
// as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past
// finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`,
// i.e. the one that FOLLOWS the target node.
if !bit {
let direct_parent = target_tau.try_get_parent_at_height(i)?;
self.nodes
.push(parent.derive_right_nonfinal_child_of_with_partials(
params,
direct_parent,
&new_b_accumulator,
&new_f_accumulator,
&mut rng,
));
} else {
// only update the accumulators when the bit is set, as d^0 == identity, so there's
// no point in doing anything else;
// note that we don't have to generate any new nodes when going into the right branch
// of the tree as everything on the left would have been in the past, so we don't care about them
new_b_accumulator += parent.ds[n]; // add d0
new_f_accumulator += params.fs[i]; // f_i
}
}
self.nodes.push(parent.derive_target_child_with_partials(
params,
target_epoch.as_tau(),
&new_b_accumulator,
&new_f_accumulator,
&mut rng,
));
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let num_nodes = self.nodes.len() as u32;
// unfortunately we're not going to know the expected capacity
let mut bytes = Vec::new();
bytes.extend_from_slice(&num_nodes.to_be_bytes());
for node in &self.nodes {
let mut node_bytes = node.to_bytes();
bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes()));
bytes.append(&mut node_bytes)
}
bytes
}
pub fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
// we have to be able to read the length of nodes
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey",
"insufficient number of bytes provided",
));
}
let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
let mut nodes = Vec::with_capacity(nodes_len);
let mut i = 4;
for _ in 0..nodes_len {
// check if we can actually read the length...
if b[i..].len() < 4 {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey.Node",
"insufficient number of bytes provided for BTE Node recovery",
));
}
let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize;
if b[i + 4..].len() < node_bytes {
return Err(DkgError::new_deserialization_failure(
"DecryptionKey.Node",
"insufficient number of bytes provided for BTE Node recovery",
));
}
i += 4;
let node = Node::try_from_bytes(&b[i..i + node_bytes])?;
nodes.push(node);
i += node_bytes;
}
Ok(DecryptionKey { nodes })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::setup;
use bitvec::bitvec;
use bitvec::order::Msb0;
use rand_core::SeedableRng;
#[test]
fn basic_coverage_nodes() {
// it's some basic test I've been performing when writing the update function, but figured
// might as well put it into a unit test. note that it doesn't check the entire structure,
// but just the few last nodes of low height
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let root_node_copy = dk.nodes.clone();
// this is a root node
assert_eq!(dk.nodes.len(), 1);
assert!(dk.nodes[0].is_root());
// we have to have a node for right branch on each height (1, 01, 001, ... etc)
// plus an additional one for the two left-most leaves (epochs "0" and "1")
dk.try_update_to(Epoch::new(0), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.len(), 33);
let expected_last = Tau::new(0);
// (and yes, I had to look up those names in a thesaurus)
let expected_penultimate = Tau::new(1);
// note that this value is 31bit long
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1
]);
let mut nodes_iter = dk.nodes.iter().rev();
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
let mut epoch_zero_nodes = dk.nodes.clone();
// nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf
dk.try_update_to(Epoch::new(1), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.len(), 32);
epoch_zero_nodes.pop().unwrap();
assert_eq!(
epoch_zero_nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>(),
dk.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>()
);
dk.try_update_to(Epoch::new(2), &params, &mut rng).unwrap();
dk.try_update_to(Epoch::new(3), &params, &mut rng).unwrap();
dk.try_update_to(Epoch::new(4), &params, &mut rng).unwrap();
let expected_last = Tau::new(4);
let expected_penultimate = Tau::new(5);
let expected_antepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1
]);
let expected_preantepenultimate = Tau(bitvec![u32, Msb0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
]);
assert_eq!(dk.nodes.len(), 32);
let mut nodes_iter = dk.nodes.iter().rev();
assert_eq!(expected_last, nodes_iter.next().unwrap().tau);
assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau);
assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau);
// the result should be the same of regardless if we update incrementally or go to the target immediately
let mut new_root = DecryptionKey {
nodes: root_node_copy,
};
new_root
.try_update_to(Epoch::new(4), &params, &mut rng)
.unwrap();
assert_eq!(
dk.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>(),
new_root
.nodes
.iter()
.map(|node| node.tau.clone())
.collect::<Vec<_>>()
);
// getting expected nodes for those epochs is non-trivial for test purposes, but the last node
// should ALWAYS be equal to the target epoch
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42));
dk.try_update_to(Epoch::new(123456), &params, &mut rng)
.unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456));
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435));
// trying to go to past epochs fails
assert!(dk
.try_update_to(Epoch::new(531), &params, &mut rng)
.is_err())
}
#[test]
fn updating_to_next_epoch() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
// for root node current epoch is `None`
assert_eq!(None, dk.current_epoch(&params).unwrap());
// for root node it should result in epoch 0
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(0)), dk.current_epoch(&params).unwrap());
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(1)), dk.current_epoch(&params).unwrap());
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(2)), dk.current_epoch(&params).unwrap());
// if we start from some non-root epoch, it should result in l + 1
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(43)), dk.current_epoch(&params).unwrap());
dk.try_update_to(Epoch::new(12345), &params, &mut rng)
.unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(&params).unwrap());
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
dk.try_update_to_next_epoch(&params, &mut rng).unwrap();
assert_eq!(
Some(Epoch::new(3292547436)),
dk.current_epoch(&params).unwrap()
);
}
#[test]
fn public_key_with_proof_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (_, pk) = keygen(&params, &mut rng);
let bytes = pk.to_bytes();
let recovered = PublicKeyWithProof::try_from_bytes(&bytes).unwrap();
assert_eq!(pk, recovered)
}
#[test]
fn bte_node_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let root_node = dk.nodes[0].clone();
let bytes = root_node.to_bytes();
let recovered = Node::try_from_bytes(&bytes).unwrap();
assert_eq!(root_node, recovered);
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
for node in &dk.nodes {
let bytes = node.to_bytes();
let recovered = Node::try_from_bytes(&bytes).unwrap();
assert_eq!(node, &recovered);
}
}
#[test]
fn decryption_key_node_roundtrip() {
let params = setup();
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (mut dk, _) = keygen(&params, &mut rng);
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(0), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(1), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(42), &params, &mut rng).unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
dk.try_update_to(Epoch::new(3292547435), &params, &mut rng)
.unwrap();
let bytes = dk.to_bytes();
let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap();
assert_eq!(dk, recovered);
}
}
+463
View File
@@ -0,0 +1,463 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use crate::utils::{hash_g2, RandomOracleBuilder};
use crate::{Chunk, Share};
use bitvec::field::BitField;
use bitvec::order::Msb0;
use bitvec::vec::BitVec;
use bitvec::view::BitView;
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt};
use group::Curve;
use lazy_static::lazy_static;
use zeroize::Zeroize;
pub mod encryption;
pub mod keys;
pub mod proof_chunking;
pub mod proof_discrete_log;
pub mod proof_sharing;
pub use encryption::{decrypt_share, encrypt_shares, Ciphertexts};
pub use keys::{keygen, DecryptionKey, PublicKey, PublicKeyWithProof};
lazy_static! {
pub(crate) static ref PAIRING_BASE: Gt =
bls12_381::pairing(&G1Affine::generator(), &G2Affine::generator());
pub(crate) static ref G2_GENERATOR_PREPARED: G2Prepared =
G2Prepared::from(G2Affine::generator());
pub(crate) static ref DEFAULT_BSGS_TABLE: encryption::BabyStepGiantStepLookup =
encryption::BabyStepGiantStepLookup::default();
}
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP";
// this particular domain is not for curve hashing, but might as well also follow the same naming pattern
const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION";
const MAX_EPOCHS_EXP: usize = 32;
const HASH_SECURITY_PARAM: usize = 256;
// note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE
pub const CHUNK_BYTES: usize = 2;
pub const NUM_CHUNKS: usize = 16;
pub const SCALAR_SIZE: usize = 32;
/// In paper B; number of distinct chunks
pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3);
pub(crate) type EpochStore = u32;
#[derive(Clone, Debug, PartialEq, PartialOrd)]
// None empty bitvec implies this is a root node
pub(crate) struct Tau(BitVec<EpochStore, Msb0>);
impl Tau {
pub fn new_root() -> Self {
Tau(BitVec::new())
}
// TODO: perhaps this should be explicitly moved to some test module
#[cfg(test)]
pub(crate) fn new(epoch: EpochStore) -> Self {
Tau(epoch.view_bits().to_bitvec())
}
#[allow(unused)]
pub fn left_child(&self) -> Self {
let mut child = self.0.clone();
child.push(false);
Tau(child)
}
pub fn right_child(&self) -> Self {
let mut child = self.0.clone();
child.push(true);
Tau(child)
}
pub fn is_leaf(&self, params: &Params) -> bool {
self.height() == params.lambda_t
}
pub fn try_get_parent_at_height(&self, height: usize) -> Result<Self, DkgError> {
if height > self.0.len() {
return Err(DkgError::NotAValidParent);
}
Ok(Tau(self.0[..height].to_bitvec()))
}
// essentially is this tau prefixing the other
pub fn is_parent_of(&self, other: &Tau) -> bool {
if self.0.len() > other.0.len() {
return false;
}
for (i, b) in self.0.iter().enumerate() {
if b != other.0[i] {
return false;
}
}
true
}
pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result<Epoch, DkgError> {
if self.0.len() > params.lambda_t {
// this node is already BELOW a valid leaf-epoch node. it can only happen
// if either some invariant was broken or additional data was pushed to `tau`
// in order compute some intermediate results, but in that case this method should have
// never been called anyway. tl;dr: if this is called, the underlying key is malformed
return Err(DkgError::NotAValidParent);
}
let mut child = self.0.clone();
for _ in 0..(params.lambda_t - self.0.len()) {
child.push(false)
}
// the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here
// (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway
// but I'd prefer to just blow up since this would be a serious error
Ok(Epoch::try_from_tau(&Tau(child), params).unwrap())
}
pub fn height(&self) -> usize {
self.0.len()
}
fn extend(
&self,
rr: &[G1Projective; NUM_CHUNKS],
ss: &[G1Projective; NUM_CHUNKS],
cc: &[[G1Projective; NUM_CHUNKS]],
) -> Self {
let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN);
random_oracle_builder.update_with_g1_elements(rr.iter());
random_oracle_builder.update_with_g1_elements(ss.iter());
for ciphertext_chunks in cc {
random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter());
}
let tau_mem = self.0.as_raw_slice();
assert_eq!(tau_mem.len(), 1, "tau length invariant was broken");
random_oracle_builder.update(&tau_mem[0].to_be_bytes());
let oracle_output = random_oracle_builder.finalize();
debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM);
let mut extended_tau = self.clone();
for byte in oracle_output {
extended_tau
.0
.extend_from_bitslice(byte.view_bits::<Msb0>())
}
extended_tau
}
// considers all lambda_t + lambda_h bits
fn evaluate_f(&self, params: &Params) -> G2Projective {
self.0
.iter()
.by_vals()
.zip(params.fs.iter().chain(params.fh.iter()))
.filter(|(i, _)| *i)
.map(|(_, f_i)| f_i)
.fold(params.f0, |acc, f_i| acc + f_i)
}
// only considers up to lambda_t bits
fn evaluate_partial_f(&self, params: &Params) -> G2Projective {
self.0
.iter()
.by_vals()
.zip(params.fs.iter())
.filter(|(i, _)| *i)
.map(|(_, f_i)| f_i)
.fold(params.f0, |acc, f_i| acc + f_i)
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let len_bytes = (self.0.len() as u32).to_be_bytes();
len_bytes
.into_iter()
.chain(self.0.chunks(8).map(BitField::load_be))
.collect()
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"Tau",
"insufficient number of bytes provided",
));
}
let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
// maximum theoretical length
if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM {
return Err(DkgError::new_deserialization_failure(
"Tau",
format!(
"malformed length {} is greater than maximum {}",
tau_len,
MAX_EPOCHS_EXP + HASH_SECURITY_PARAM
),
));
}
if tau_len == 0 {
if b.len() != 4 {
Err(DkgError::new_deserialization_failure(
"Tau",
"malformed bytes",
))
} else {
Ok(Tau::new_root())
}
} else if b.len() == 4 {
Err(DkgError::new_deserialization_failure(
"Tau",
"insufficient number of bytes provided",
))
} else {
let mut inner = BitVec::repeat(false, tau_len);
for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) {
slot.store_be(byte);
}
Ok(Tau(inner))
}
}
}
impl Zeroize for Tau {
fn zeroize(&mut self) {
for v in self.0.as_raw_mut_slice() {
v.zeroize()
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Epoch(EpochStore);
impl Epoch {
pub fn new(value: EpochStore) -> Self {
Epoch(value)
}
pub(crate) fn as_tau(&self) -> Tau {
(*self).into()
}
pub(crate) fn as_extended_tau(
&self,
rr: &[G1Projective; NUM_CHUNKS],
ss: &[G1Projective; NUM_CHUNKS],
cc: &[[G1Projective; NUM_CHUNKS]],
) -> Tau {
self.as_tau().extend(rr, ss, cc)
}
pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result<Self, DkgError> {
if !tau.is_leaf(params) {
Err(DkgError::MalformedEpoch)
} else {
Ok(Epoch(tau.0.load_be()))
}
}
}
impl From<Epoch> for Tau {
fn from(epoch: Epoch) -> Self {
Tau(epoch.0.view_bits().to_bitvec())
}
}
impl From<EpochStore> for Epoch {
fn from(epoch: EpochStore) -> Self {
Epoch(epoch)
}
}
pub struct Params {
/// Maximum size of an epoch, in bits.
pub lambda_t: usize,
/// Security parameter of our $H_{\Lamda_H}$ hash function
pub lambda_h: usize,
// keeping f0 separate from the rest of the curve points makes it easier to work with tau
f0: G2Projective,
fs: Vec<G2Projective>, // f_1, f_2, .... f_{lambda_t} in the paper
fh: Vec<G2Projective>, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper
h: G2Projective,
/// Precomputed `h` used for the miller loop
_h_prepared: G2Prepared,
}
pub fn setup() -> Params {
let f0 = hash_g2(b"f0", SETUP_DOMAIN);
let fs = (1..=MAX_EPOCHS_EXP)
.map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN))
.collect();
let fh = (0..HASH_SECURITY_PARAM)
.map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN))
.collect();
let h = hash_g2(b"h", SETUP_DOMAIN);
Params {
lambda_t: MAX_EPOCHS_EXP,
lambda_h: HASH_SECURITY_PARAM,
f0,
fs,
fh,
h,
_h_prepared: G2Prepared::from(h.to_affine()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitvec::bitvec;
use bitvec::order::Msb0;
#[test]
fn creating_tau_from_epoch() {
assert!(Tau::new_root().0.is_empty());
let zero = Tau::new(0);
assert!(zero.0.iter().by_vals().all(|b| !b));
let one = Tau::new(1);
let mut iter = one.0.iter().by_vals();
// first 31 bits are 0, the last one is 1
for _ in 0..31 {
assert!(!iter.next().unwrap())
}
assert!(iter.next().unwrap());
// 101010 in binary
let forty_two = Tau::new(42);
// first 26 bits are not set
let mut iter = forty_two.0.iter().by_vals();
for _ in 0..26 {
assert!(!iter.next().unwrap())
}
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
assert!(iter.next().unwrap());
assert!(!iter.next().unwrap());
// value that requires an actual u32 (i.e. takes 4 bytes to represent)
// 11000100_01000000_01001001_01101011 in binary
let big_val = Tau::new(3292547435);
let expected = bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1, 1
];
assert_eq!(expected, big_val.0)
}
#[test]
fn getting_parent_at_height() {
let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]);
let expected_0 = Tau(BitVec::new());
let expected_1 = Tau(bitvec![u32, Msb0; 1]);
let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]);
assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap());
assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap());
assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap());
assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap());
assert!(tau.try_get_parent_at_height(8).is_err())
}
#[test]
fn converting_tau_to_epoch() {
let params = setup();
let tau0: Tau = Epoch::new(0).into();
let tau1: Tau = Epoch::new(1).into();
let tau42: Tau = Epoch::new(42).into();
let tau_big: Tau = Epoch::new(3292547435).into();
assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, &params).unwrap());
assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, &params).unwrap());
assert_eq!(
Epoch::new(42),
Epoch::try_from_tau(&tau42, &params).unwrap()
);
assert_eq!(
Epoch::new(3292547435),
Epoch::try_from_tau(&tau_big, &params).unwrap()
);
assert!(Epoch::try_from_tau(&Tau(BitVec::new()), &params).is_err());
assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), &params).is_err());
let _31bit_tau = Tau(bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1
]);
assert!(Epoch::try_from_tau(&_31bit_tau, &params).is_err());
let _33bit_tau = Tau(bitvec![u32, Msb0;
1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1, 1, 0
]);
assert!(Epoch::try_from_tau(&_33bit_tau, &params).is_err());
}
#[test]
fn tau_roundtrip() {
let good_taus = vec![
Tau::new_root(),
Tau::new(0),
Tau::new(1),
Tau::new(2),
Tau::new(42),
Tau::new(123456),
Tau::new(3292547435),
Tau::new(u32::MAX),
];
for tau in good_taus {
let bytes = tau.to_bytes();
let recovered = Tau::try_from_bytes(&bytes).unwrap();
assert_eq!(tau, recovered);
}
// more valid variants
let mut another_tau = Tau::new(u32::MAX);
another_tau.0.push(true);
another_tau.0.push(false);
another_tau.0.push(true);
let bytes = another_tau.to_bytes();
let recovered = Tau::try_from_bytes(&bytes).unwrap();
assert_eq!(another_tau, recovered);
// ensure there are no panics
let big_length_bytes = [255, 255, 255, 255, 42];
assert!(Tau::try_from_bytes(&big_length_bytes).is_err());
assert!(Tau::try_from_bytes(&[]).is_err());
assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err());
assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err());
assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err());
assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::utils::hash_to_scalar;
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use zeroize::Zeroize;
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const DISCRETE_LOG_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG";
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ProofOfDiscreteLog {
pub(crate) rand_commitment: G1Projective,
pub(crate) response: Scalar,
}
impl ProofOfDiscreteLog {
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
let mut rand_x = Scalar::random(&mut rng);
let rand_commitment = G1Projective::generator() * rand_x;
let challenge = Self::compute_challenge(public, &rand_commitment);
let response = rand_x + challenge * witness;
rand_x.zeroize();
ProofOfDiscreteLog {
rand_commitment,
response,
}
}
// note: we don't have to explicitly check whether points are on correct curves / fields
// as if they weren't, they'd fail to get deserialized
pub fn verify(&self, public: &G1Projective) -> bool {
let challenge = Self::compute_challenge(public, &self.rand_commitment);
// y^c • a == g1^rand_x
public * challenge + self.rand_commitment == G1Projective::generator() * self.response
}
pub(crate) fn compute_challenge(public: &G1Projective, rand_commit: &G1Projective) -> Scalar {
let public_bytes = public.to_bytes();
let rand_commit_bytes = rand_commit.to_bytes();
let mut bytes = Vec::with_capacity(96);
bytes.extend_from_slice(public_bytes.as_ref());
bytes.extend_from_slice(rand_commit_bytes.as_ref());
hash_to_scalar(bytes, DISCRETE_LOG_DOMAIN)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_core::SeedableRng;
#[test]
fn should_verify_a_valid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let witness = Scalar::random(&mut rng);
let public = G1Projective::generator() * witness;
let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness);
assert!(proof.verify(&public))
}
#[test]
fn should_fail_on_invalid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let witness = Scalar::random(&mut rng);
let public = G1Projective::generator() * witness;
let other_witness = Scalar::random(&mut rng);
let other_public = G1Projective::generator() * other_witness;
let proof = ProofOfDiscreteLog::construct(&mut rng, &public, &witness);
let other_proof = ProofOfDiscreteLog::construct(&mut rng, &other_public, &other_witness);
assert!(!proof.verify(&other_public));
assert!(!other_proof.verify(&public));
}
}
+615
View File
@@ -0,0 +1,615 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::PublicKey;
use crate::error::DkgError;
use crate::interpolation::polynomial::PublicCoefficients;
use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar, hash_to_scalar};
use crate::{NodeIndex, Share};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use std::collections::BTreeMap;
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const INSTANCE_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_INSTANCE";
const CHALLENGE_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_SECRET_SHARING_CHALLENGE";
#[cfg_attr(test, derive(Clone))]
pub struct Instance<'a> {
public_keys: &'a BTreeMap<NodeIndex, PublicKey>,
public_coefficients: &'a PublicCoefficients,
combined_randomizer: &'a G1Projective,
combined_ciphertexts: &'a [G1Projective],
}
impl<'a> Instance<'a> {
pub fn new(
public_keys: &'a BTreeMap<NodeIndex, PublicKey>,
public_coefficients: &'a PublicCoefficients,
combined_randomizer: &'a G1Projective,
combined_ciphertexts: &'a [G1Projective],
) -> Instance<'a> {
Instance {
public_keys,
public_coefficients,
combined_randomizer,
combined_ciphertexts,
}
}
fn hash_to_scalar(&self) -> Scalar {
let g1s = self.public_keys.len() + 1 + self.combined_ciphertexts.len();
let g2s = self.public_coefficients.size();
let mut bytes = Vec::with_capacity(g1s * 48 + g2s * 96);
for pk in self.public_keys.values() {
bytes.extend_from_slice(pk.0.to_bytes().as_ref())
}
for coeff in self.public_coefficients.inner() {
bytes.extend_from_slice(coeff.to_bytes().as_ref())
}
bytes.extend_from_slice(self.combined_randomizer.to_bytes().as_ref());
for ciphertext in self.combined_ciphertexts {
bytes.extend_from_slice(ciphertext.to_bytes().as_ref())
}
hash_to_scalar(&bytes, INSTANCE_DOMAIN)
}
fn validate(&self) -> bool {
if self.public_keys.is_empty() || self.public_coefficients.is_empty() {
return false;
}
if self.public_keys.len() != self.combined_ciphertexts.len() {
return false;
}
true
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct ProofOfSecretSharing {
ff: G1Projective,
aa: G2Projective,
yy: G1Projective,
response_r: Scalar,
response_alpha: Scalar,
}
impl ProofOfSecretSharing {
pub fn construct(
mut rng: impl RngCore,
instance: Instance,
witness_r: &Scalar,
witnesses_s: &[Share],
) -> Result<Self, DkgError> {
if !instance.validate() {
return Err(DkgError::MalformedProofOfSharingInstance);
}
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let x = instance.hash_to_scalar();
// alpha, rho ← random_scalars
let alpha = Scalar::random(&mut rng);
let rho = Scalar::random(&mut rng);
// F = g1^rho
let ff = g1 * rho;
// A = g2^alpha
let aa = g2 * alpha;
// Y = (y_1^{x^1} • ... y_n^{x^n})^rho • g1^alpha
// produce intermediate product (y_1^{x^1} • ... y_n^{x^n})
let product =
instance
.public_keys
.values()
.rev()
.fold(G1Projective::identity(), |mut acc, pk| {
acc += pk.0;
acc *= x;
acc
});
let yy = product * rho + g1 * alpha;
let challenge = Self::compute_challenge(&x, &ff, &aa, &yy);
// response_r = r • challenge + rho
let response_r = witness_r * challenge + rho;
// response_alpha = (share_1 • x^1 + ... share_n • x^n) • challenge + alpha
// produce intermediate sum (share_1 • x^1 + ... share_n • x^n)
let sum = witnesses_s
.iter()
.rev()
.fold(Scalar::zero(), |mut acc, witness| {
acc += witness.inner();
acc *= x;
acc
});
let response_alpha = sum * challenge + alpha;
Ok(ProofOfSecretSharing {
ff,
aa,
yy,
response_r,
response_alpha,
})
}
pub fn verify(&self, instance: Instance) -> bool {
if !instance.validate() {
return false;
}
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
let x = instance.hash_to_scalar();
let challenge = Self::compute_challenge(&x, &self.ff, &self.aa, &self.yy);
// check if R^challenge * F == g1^response_r
if instance.combined_randomizer * challenge + self.ff != g1 * self.response_r {
return false;
}
// check if
// (A_0 ^ (id1^0 • x^1 + ... idn^0 • x^n) • ... A_{t-1} ^ (id1^{t-1} • x^{t-1} + ... idn^{t-1} • x^n))^challenge * A
// ==
// g2^response_alpha
let product = instance
.public_coefficients
.inner()
.iter()
.enumerate()
.fold(G2Projective::identity(), |mut acc, (k, coeff)| {
// intermediate (id1^k • x^1 + ... + idn^k • x^n) sum
let sum: Scalar = instance
.public_keys
.keys()
.enumerate()
.map(|(i, node_id)| {
let id_scalar = Scalar::from(*node_id);
id_scalar.pow(&[k as u64, 0, 0, 0]) * x.pow(&[(i + 1) as u64, 0, 0, 0])
})
.sum();
acc += coeff * sum;
acc
});
if product * challenge + self.aa != g2 * self.response_alpha {
return false;
}
// check if
// (ciphertext_1 ^ (x^1) • ... ciphertext_n ^ (x^n)) ^ challenge • Y
// ==
// (pk_1 ^ (x^1) • ... pk_n ^ (x^n)) ^ response_r • g1^response_alpha
let product_1 = instance.combined_ciphertexts.iter().rev().fold(
G1Projective::identity(),
|mut acc, ciphertext| {
acc += ciphertext;
acc *= x;
acc
},
);
let product_2 =
instance
.public_keys
.values()
.rev()
.fold(G1Projective::identity(), |mut acc, pk| {
acc += pk.0;
acc *= x;
acc
});
if product_1 * challenge + self.yy != product_2 * self.response_r + g1 * self.response_alpha
{
return false;
}
true
}
pub(crate) fn compute_challenge(
commitment: &Scalar,
blinder_g1: &G1Projective,
blinder_g2: &G2Projective,
blinded_instance: &G1Projective,
) -> Scalar {
let mut bytes = Vec::with_capacity(224);
bytes.extend_from_slice(commitment.to_bytes().as_ref());
bytes.extend_from_slice(blinder_g1.to_bytes().as_ref());
bytes.extend_from_slice(blinder_g2.to_bytes().as_ref());
bytes.extend_from_slice(blinded_instance.to_bytes().as_ref());
hash_to_scalar(&bytes, CHALLENGE_DOMAIN)
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
// we have 2 G1 elements, single G2 element and 2 scalars
let mut bytes = Vec::with_capacity(2 * 48 + 96 + 2 * 32);
bytes.extend_from_slice(self.ff.to_bytes().as_ref());
bytes.extend_from_slice(self.aa.to_bytes().as_ref());
bytes.extend_from_slice(self.yy.to_bytes().as_ref());
bytes.extend_from_slice(self.response_r.to_bytes().as_ref());
bytes.extend_from_slice(self.response_alpha.to_bytes().as_ref());
bytes
}
pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
if bytes.len() != 2 * 48 + 96 + 2 * 32 {
return Err(DkgError::new_deserialization_failure(
"ProofOfSecretSharing",
"invalid number of bytes provided",
));
}
let mut i = 0;
let f = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.f", "invalid curve point")
})?;
i += 48;
let a = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.a", "invalid curve point")
})?;
i += 96;
let y = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| {
DkgError::new_deserialization_failure("ProofOfSecretSharing.y", "invalid curve point")
})?;
i += 48;
let response_r = deserialize_scalar(&bytes[i..i + 32]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"ProofOfSecretSharing.response_r",
"invalid scalar",
)
})?;
i += 32;
let response_alpha = deserialize_scalar(&bytes[i..]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"ProofOfSecretSharing.response_alpha",
"invalid scalar",
)
})?;
Ok(ProofOfSecretSharing {
ff: f,
aa: a,
yy: y,
response_r,
response_alpha,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interpolation::polynomial::Polynomial;
use group::Group;
use rand_core::SeedableRng;
const NODES: u64 = 50;
const THRESHOLD: u64 = 40;
fn setup(
mut rng: impl RngCore,
) -> (
BTreeMap<NodeIndex, PublicKey>,
PublicCoefficients,
G1Projective,
Vec<G1Projective>,
Scalar,
Vec<Share>,
) {
let g1 = G1Projective::generator();
let mut pks = BTreeMap::new();
let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1);
let public_coefficients = polynomial.public_coefficients();
let mut shares: Vec<Share> = Vec::new();
let mut node_indices = (0..NODES).map(|_| rng.next_u64()).collect::<Vec<_>>();
node_indices.sort_unstable();
for node_index in node_indices {
let share = polynomial.evaluate_at(&Scalar::from(node_index));
shares.push(share.into());
pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng)));
}
let r = Scalar::random(&mut rng);
let rr = g1 * r;
let ciphertexts = pks
.values()
.zip(&shares)
.map(|(pk, share)| pk.0 * r + g1 * share.inner())
.collect();
(pks, public_coefficients, rr, ciphertexts, r, shares)
}
#[test]
fn should_fail_to_create_proof_with_invalid_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let g1 = G1Projective::generator();
let mut pks = BTreeMap::new();
let polynomial = Polynomial::new_random(&mut rng, THRESHOLD - 1);
let public_coefficients = polynomial.public_coefficients();
let mut shares: Vec<Share> = Vec::new();
for _ in 0..NODES {
let node_index = rng.next_u64();
let share = polynomial.evaluate_at(&Scalar::from(node_index));
shares.push(share.into());
pks.insert(node_index, PublicKey(g1 * Scalar::random(&mut rng)));
}
let r = Scalar::random(&mut rng);
let rr = g1 * r;
let mut shares = Vec::new();
for node_id in 1..NODES + 1 {
let share = polynomial.evaluate_at(&Scalar::from(node_id));
shares.push(share);
}
let ciphertexts = pks
.values()
.zip(&shares)
.map(|(pk, share)| pk.0 * r + g1 * share)
.collect::<Vec<_>>();
// no public keys
let bad_instance1 = Instance {
public_keys: &BTreeMap::new(),
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!bad_instance1.validate());
// no public coefficients
let bad_instance2 = Instance {
public_keys: &pks,
public_coefficients: &PublicCoefficients {
coefficients: Vec::new(),
},
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!bad_instance2.validate());
// no ciphertexts
let bad_instance3 = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &[],
};
assert!(!bad_instance3.validate());
// public_keys.len() != combined_ciphertexts.len()
let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::<Vec<_>>();
let bad_instance4 = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!bad_instance4.validate());
// changed index of one of the keys
let mut bad_pks = pks.clone();
let first_id = bad_pks.keys().copied().take(1).collect::<Vec<_>>();
let first_val = bad_pks.remove(&first_id[0]).unwrap();
bad_pks.insert(rng.next_u64(), first_val);
let bad_instance5 = Instance {
public_keys: &bad_pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!bad_instance5.validate());
let good_instance = Instance {
public_keys: &pks,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(good_instance.validate())
}
#[test]
fn should_verify_a_valid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
assert!(sharing_proof.verify(instance))
}
#[test]
fn should_fail_to_verify_proof_with_invalid_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
// no public keys
let bad_instance1 = Instance {
public_keys: &BTreeMap::new(),
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance1));
// no public coefficients
let bad_instance2 = Instance {
public_keys: &public_keys,
public_coefficients: &PublicCoefficients {
coefficients: Vec::new(),
},
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance2));
// no ciphertexts
let bad_instance3 = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &[],
};
assert!(!sharing_proof.verify(bad_instance3));
// public_keys.len() != combined_ciphertexts.len()
let bad_ciphertexts = ciphertexts.iter().skip(1).cloned().collect::<Vec<_>>();
let bad_instance4 = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &bad_ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance4));
}
#[test]
fn should_fail_to_verify_proof_with_wrong_instance() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let sharing_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
let (public_keys, public_coefficients, rr, ciphertexts, _, _) = setup(&mut rng);
let bad_instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
assert!(!sharing_proof.verify(bad_instance));
}
#[test]
fn should_fail_to_verify_invalid_proof() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let (public_keys, public_coefficients, rr, ciphertexts, r, shares) = setup(&mut rng);
let instance = Instance {
public_keys: &public_keys,
public_coefficients: &public_coefficients,
combined_randomizer: &rr,
combined_ciphertexts: &ciphertexts,
};
let good_proof =
ProofOfSecretSharing::construct(&mut rng, instance.clone(), &r, &shares).unwrap();
let mut bad_proof = good_proof.clone();
bad_proof.ff = G1Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.aa = G2Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.yy = G1Projective::generator();
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof.clone();
bad_proof.response_r = Scalar::from(42);
assert!(!bad_proof.verify(instance.clone()));
let mut bad_proof = good_proof;
bad_proof.response_alpha = Scalar::from(42);
assert!(!bad_proof.verify(instance));
}
#[test]
fn proof_of_secret_sharing_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let proof_fixture = ProofOfSecretSharing {
ff: G1Projective::random(&mut rng),
aa: G2Projective::random(&mut rng),
yy: G1Projective::random(&mut rng),
response_r: Scalar::random(&mut rng),
response_alpha: Scalar::random(&mut rng),
};
let bytes = proof_fixture.to_bytes();
let recovered = ProofOfSecretSharing::try_from_bytes(&bytes).unwrap();
assert_eq!(proof_fixture, recovered);
assert!(ProofOfSecretSharing::try_from_bytes(&bytes[1..]).is_err())
}
}
+500
View File
@@ -0,0 +1,500 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::proof_chunking::ProofOfChunking;
use crate::bte::proof_sharing::ProofOfSecretSharing;
use crate::bte::{
encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey,
};
use crate::error::DkgError;
use crate::interpolation::polynomial::{Polynomial, PublicCoefficients};
use crate::interpolation::{
perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x,
};
use crate::{NodeIndex, Share, Threshold};
use bls12_381::{G2Projective, Scalar};
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Dealing {
pub public_coefficients: PublicCoefficients,
pub ciphertexts: Ciphertexts,
pub proof_of_chunking: ProofOfChunking,
pub proof_of_sharing: ProofOfSecretSharing,
}
impl Dealing {
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
pub fn create(
mut rng: impl RngCore,
params: &Params,
dealer_index: NodeIndex,
threshold: Threshold,
epoch: Epoch,
// BTreeMap ensures the keys are sorted by their indices
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_secret: Option<Scalar>,
) -> (Self, Option<Share>) {
assert!(threshold > 0);
let mut polynomial = Polynomial::new_random(&mut rng, threshold - 1);
if let Some(prior_secret) = prior_resharing_secret {
polynomial.set_constant_coefficient(prior_secret)
}
let mut shares = receivers
.keys()
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) =
encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng);
// create proofs of knowledge
let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts);
let proof_of_chunking =
ProofOfChunking::construct(&mut rng, chunking_instance, hazmat.r(), &shares)
.expect("failed to construct proof of chunking");
let combined_ciphertexts = ciphertexts.combine_ciphertexts();
let mut combined_r = hazmat.combine_rs();
let combined_rr = ciphertexts.combine_rs();
let public_coefficients = polynomial.public_coefficients();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&public_coefficients,
&combined_rr,
&combined_ciphertexts,
);
let proof_of_sharing =
ProofOfSecretSharing::construct(&mut rng, sharing_instance, &combined_r, &shares)
.expect("failed to construct proof of secret sharing");
combined_r.zeroize();
let dealing = Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
};
let dealers_key_index = receivers
.keys()
.position(|node_index| node_index == &dealer_index);
if let Some(dealer_key_index) = dealers_key_index {
let dealers_share = shares.remove(dealer_key_index);
shares.zeroize();
(dealing, Some(dealers_share))
} else {
(dealing, None)
}
}
// rather than returning a bool for whether the dealing is valid or not, a Result is returned
// instead so that we would have more information regarding a possible failure cause
pub fn verify(
&self,
params: &Params,
epoch: Epoch,
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
prior_resharing_public: Option<G2Projective>,
) -> Result<(), DkgError> {
if threshold == 0 || threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
if self.ciphertexts.ciphertext_chunks.len() != receivers.len() {
return Err(DkgError::WrongCiphertextSize {
actual: self.ciphertexts.ciphertext_chunks.len(),
expected: receivers.len(),
});
}
if self.public_coefficients.size() != threshold as usize {
return Err(DkgError::WrongPublicCoefficientsSize {
actual: self.public_coefficients.size(),
expected: threshold as usize,
});
}
if !self.ciphertexts.verify_integrity(params, epoch) {
return Err(DkgError::FailedCiphertextIntegrityCheck);
}
// TODO: perhaps change the underlying arguments in proofs of knowledge to avoid this allocation?
let sorted_receivers = receivers.values().copied().collect::<Vec<_>>();
let chunking_instance = proof_chunking::Instance::new(&sorted_receivers, &self.ciphertexts);
if !self.proof_of_chunking.verify(chunking_instance) {
return Err(DkgError::InvalidProofOfChunking);
}
let combined_randomizer = &self.ciphertexts.combine_rs();
let combined_ciphertexts = &self.ciphertexts.combine_ciphertexts();
let sharing_instance = proof_sharing::Instance::new(
receivers,
&self.public_coefficients,
combined_randomizer,
combined_ciphertexts,
);
if !self.proof_of_sharing.verify(sharing_instance) {
return Err(DkgError::InvalidProofOfSharing);
}
if let Some(prior_public) = prior_resharing_public {
let dealt_public = &self.public_coefficients[0];
if dealt_public != &prior_public {
return Err(DkgError::InvalidResharing);
}
}
Ok(())
}
// coeff_len || coeff || cc_len || cc || pi_c_len || pi_c || pi_s_len || pi_s
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let mut coefficients_bytes = self.public_coefficients.to_bytes();
bytes.extend_from_slice(&(coefficients_bytes.len() as u32).to_be_bytes());
bytes.append(&mut coefficients_bytes);
let mut ciphertexts_bytes = self.ciphertexts.to_bytes();
bytes.extend_from_slice(&(ciphertexts_bytes.len() as u32).to_be_bytes());
bytes.append(&mut ciphertexts_bytes);
let mut proof_sharing_bytes = self.proof_of_sharing.to_bytes();
bytes.extend_from_slice(&(proof_sharing_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_sharing_bytes);
let mut proof_chunking_bytes = self.proof_of_chunking.to_bytes();
bytes.extend_from_slice(&(proof_chunking_bytes.len() as u32).to_be_bytes());
bytes.append(&mut proof_chunking_bytes);
bytes
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, DkgError> {
// can we read the length of serialized public coefficients?
if bytes.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"insufficient number of bytes provided",
));
}
let mut i = 0;
let coefficients_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let public_coefficients =
PublicCoefficients::try_from_bytes(&bytes[i..i + coefficients_bytes_len])?;
i += coefficients_bytes_len;
let ciphertexts_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let ciphertexts = Ciphertexts::try_from_bytes(&bytes[i..i + ciphertexts_bytes_len])?;
i += ciphertexts_bytes_len;
let proof_of_sharing_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
let proof_of_sharing =
ProofOfSecretSharing::try_from_bytes(&bytes[i..i + proof_of_sharing_bytes_len])?;
i += proof_of_sharing_bytes_len;
let proof_of_chunking_bytes_len =
u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize;
i += 4;
if bytes[i..].len() != proof_of_chunking_bytes_len {
return Err(DkgError::new_deserialization_failure(
"Dealing",
"invalid number of bytes provided",
));
}
let proof_of_chunking = ProofOfChunking::try_from_bytes(&bytes[i..])?;
Ok(Dealing {
public_coefficients,
ciphertexts,
proof_of_chunking,
proof_of_sharing,
})
}
}
// this assumes all dealings have been verified
pub fn try_recover_verification_keys(
dealings: &[Dealing],
threshold: Threshold,
receivers: &BTreeMap<NodeIndex, PublicKey>,
) -> Result<(G2Projective, Vec<G2Projective>), DkgError> {
if dealings.is_empty() {
return Err(DkgError::NoDealingsAvailable);
}
let threshold_usize = threshold as usize;
if !dealings
.iter()
.all(|dealing| dealing.public_coefficients.size() == threshold_usize)
{
return Err(DkgError::MismatchedDealings);
}
// currently we expect every dealer to also be a receiver. This restriction might be relaxed in the future
if dealings.len() != receivers.len() {
return Err(DkgError::MismatchedDealings);
}
let indices = receivers.keys().collect::<Vec<_>>();
// Compute A0, ..., A_{t-1}
let mut interpolated_coefficients = Vec::with_capacity(threshold_usize);
for k in 0..threshold_usize {
let mut samples = Vec::with_capacity(indices.len());
for (j, dealing) in dealings.iter().enumerate() {
samples.push((
Scalar::from(*indices[j]),
*dealing.public_coefficients.nth(k),
))
}
let interpolated = perform_lagrangian_interpolation_at_origin(&samples)?;
interpolated_coefficients.push(interpolated);
}
let master_verification_key = interpolated_coefficients[0];
let interpolated_coefficients = PublicCoefficients {
coefficients: interpolated_coefficients,
};
// shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}}
let verification_key_shares = receivers
.keys()
.map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index)))
.collect();
Ok((master_verification_key, verification_key_shares))
}
pub fn verify_verification_keys(
master_key: &G2Projective,
shares: &[G2Projective],
receivers: &BTreeMap<NodeIndex, PublicKey>,
threshold: Threshold,
) -> Result<(), DkgError> {
if shares.len() != receivers.len() {
return Err(DkgError::NotEnoughReceiversProvided);
}
if threshold as usize > receivers.len() {
return Err(DkgError::InvalidThreshold {
actual: threshold as usize,
participating: receivers.len(),
});
}
let indices = receivers.keys().copied().collect::<Vec<_>>();
let indices_with_origin = std::iter::once(&0)
.chain(receivers.keys())
.collect::<Vec<_>>();
let all_shares = std::iter::once(master_key)
.chain(shares.iter())
.collect::<Vec<_>>();
for (i, share) in shares.iter().enumerate() {
let samples = indices_with_origin
.iter()
.zip(all_shares.iter())
.map(|(&node_index, &share)| (Scalar::from(*node_index), *share))
.take(threshold as usize)
.collect::<Vec<_>>();
let interpolated =
perform_lagrangian_interpolation_at_x(&Scalar::from(indices[i]), &samples)?;
if share != &interpolated {
return Err(DkgError::MismatchedVerificationKey);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{decrypt_share, keygen, setup};
use crate::combine_shares;
use rand_core::SeedableRng;
#[test]
fn recovering_partial_verification_keys() {
// START OF SETUP
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
derived_secrets.push(
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
)
}
let master_secret = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[2]), derived_secrets[2]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
// END OF SETUP
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
let g2 = G2Projective::generator();
assert_eq!(g2 * master_secret, recovered_master);
assert_eq!(g2 * derived_secrets[0], recovered_partials[0]);
assert_eq!(g2 * derived_secrets[1], recovered_partials[1]);
assert_eq!(g2 * derived_secrets[2], recovered_partials[2]);
}
#[test]
fn verifying_partial_verification_keys() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let threshold = 2;
let node_indices = vec![1, 4, 7];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
assert!(verify_verification_keys(
&recovered_master,
&recovered_partials,
&receivers,
threshold
)
.is_ok())
}
#[test]
fn dealing_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
let parties = 5;
let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold;
let node_indices = (1..=parties).collect::<Vec<_>>();
let epoch = Epoch::new(2);
let mut receivers = BTreeMap::new();
for index in &node_indices {
let (_, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
}
let (dealing, _) = Dealing::create(
&mut rng,
&params,
node_indices[0],
threshold,
epoch,
&receivers,
None,
);
let bytes = dealing.to_bytes();
let recovered = Dealing::try_from_bytes(&bytes).unwrap();
assert_eq!(dealing, recovered);
}
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DkgError {
#[error("Provided set of values contained duplicate coordinate")]
DuplicateCoordinate,
#[error("The public key is malformed")]
MalformedPublicKey,
#[error("The decryption key is malformed")]
MalformedDecryptionKey,
#[error("Could not solve the discrete log")]
UnsolvableDiscreteLog,
#[error("Received share is malformed")]
MalformedShare,
#[error("The share encrypted under index {0} doesn't exist")]
UnavailableCiphertext(usize),
#[error("The provided lookup table is mismatched")]
MismatchedLookupTable,
#[error("Failed to verify proof of discrete logarithm")]
InvalidProofOfDiscreteLog,
#[error("Tried to construct proof of sharing with an invalid instance")]
MalformedProofOfSharingInstance,
#[error("Tried to construct proof of chunking with an invalid instance")]
MalformedProofOfChunkingInstance,
#[error("Aborted construction of proof of chunking - could not complete it within specified number of attempts")]
AbortedProofOfChunking,
#[error("Tried to update the decryption key to an epoch in the past")]
TargetEpochUpdateInThePast,
#[error("Provided epoch is malformed")]
MalformedEpoch,
#[error("Provided node is not a valid parent")]
NotAValidParent,
#[error("Provided decryption key has expired")]
ExpiredKey,
#[error("Provided threshold value ({actual}) is either 0 or larger than the total number of the participating parties ({participating})")]
InvalidThreshold { actual: usize, participating: usize },
#[error(
"Provided ciphertext has been generated for a different number of participating parties (expected: {expected}, actual: {actual})"
)]
WrongCiphertextSize { actual: usize, expected: usize },
#[error(
"Provided public coefficients have been generated for a different number of participating parties (expected: {expected}, actual: {actual})"
)]
WrongPublicCoefficientsSize { actual: usize, expected: usize },
#[error("The provided ciphertexts failed integrity check")]
FailedCiphertextIntegrityCheck,
#[error("The provided proof of secret sharing was invalid")]
InvalidProofOfSharing,
#[error("The provided proof of chunking was invalid")]
InvalidProofOfChunking,
#[error("Failed to deserialize {name} - {reason}")]
DeserializationFailure { name: String, reason: String },
#[error("No dealings were provided")]
NoDealingsAvailable,
#[error("Provided dealings were created under different parameters")]
MismatchedDealings,
#[error(
"Not enough dealings are available. We have {available} while require at least {required}"
)]
NotEnoughDealingsAvailable { available: usize, required: usize },
#[error("Received different number of x and y coordinates for lagrangian interpolation (xs: {x}, ys: {y})")]
MismatchedLagrangianSamplesLengths { x: usize, y: usize },
#[error("Derived partial verification key is mismatched")]
MismatchedVerificationKey,
#[error("Insufficient number of receivers was provided")]
NotEnoughReceiversProvided,
#[error(
"The reshared dealing has different public constant coefficient than its prior variant"
)]
InvalidResharing,
}
impl DkgError {
pub fn new_deserialization_failure<S: Into<String>, T: Into<String>>(
name: S,
reason: T,
) -> DkgError {
DkgError::DeserializationFailure {
name: name.into(),
reason: reason.into(),
}
}
}
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use bls12_381::Scalar;
use core::iter::Sum;
use core::ops::Mul;
use std::collections::HashSet;
pub mod polynomial;
fn contains_duplicates(vals: &[Scalar]) -> bool {
let mut set = HashSet::new();
for x in vals {
if !set.insert(x.to_bytes()) {
return true;
}
}
false
}
#[inline]
fn generate_lagrangian_coefficients_at_x(
x: &Scalar,
points: &[Scalar],
) -> Result<Vec<Scalar>, DkgError> {
let num_points = points.len();
if num_points == 0 {
return Ok(Vec::new());
} else if num_points == 1 {
return Ok(vec![Scalar::one()]);
}
if contains_duplicates(points) {
return Err(DkgError::DuplicateCoordinate);
}
let mut res = Vec::with_capacity(points.len());
for (i, xi) in points.iter().enumerate() {
let mut numerator = Scalar::one();
let mut denominator = Scalar::one();
for (j, xj) in points.iter().enumerate() {
if j != i {
// 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;
}
}
// 1 / denominator
let inv: Scalar =
Option::from(denominator.invert()).ok_or(DkgError::DuplicateCoordinate)?;
// numerator / denominator
res.push(numerator * inv)
}
Ok(res)
}
/// Performs a Lagrange interpolation at specified x for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
pub fn perform_lagrangian_interpolation_at_x<T>(
x: &Scalar,
points: &[(Scalar, T)],
) -> Result<T, DkgError>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
let xs = points.iter().map(|p| p.0).collect::<Vec<_>>();
let coefficients = generate_lagrangian_coefficients_at_x(x, &xs)?;
Ok(coefficients
.into_iter()
.zip(points.iter().map(|p| &p.1))
.map(|(coeff, y)| y * coeff)
.sum())
}
/// Performs a Lagrange interpolation at the origin for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
pub fn perform_lagrangian_interpolation_at_origin<T>(points: &[(Scalar, T)]) -> Result<T, DkgError>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
perform_lagrangian_interpolation_at_x(&Scalar::zero(), points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn performing_lagrangian_scalar_interpolation_at_origin() {
// x^2 + 3
// x, f(x):
// 1, 4,
// 2, 7,
// 3, 12,
let points = vec![
(Scalar::from(1), Scalar::from(4)),
(Scalar::from(2), Scalar::from(7)),
(Scalar::from(3), Scalar::from(12)),
];
assert_eq!(
Scalar::from(3),
perform_lagrangian_interpolation_at_origin(&points).unwrap()
);
// x^3 + 3x^2 - 5x + 11
// x, f(x):
// 1, 10
// 2, 21
// 3, 50
// 4, 103
let points = vec![
(Scalar::from(1), Scalar::from(10)),
(Scalar::from(2), Scalar::from(21)),
(Scalar::from(3), Scalar::from(50)),
(Scalar::from(4), Scalar::from(103)),
];
assert_eq!(
Scalar::from(11),
perform_lagrangian_interpolation_at_origin(&points).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![
(Scalar::from(1), Scalar::from(12)),
(Scalar::from(2), Scalar::from(16)),
(Scalar::from(3), Scalar::from(22)),
(Scalar::from(4), Scalar::from(30)),
(Scalar::from(5), Scalar::from(40)),
];
assert_eq!(
Scalar::from(10),
perform_lagrangian_interpolation_at_origin(&points).unwrap()
);
}
}
@@ -0,0 +1,395 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::DkgError;
use crate::utils::deserialize_g2;
use bls12_381::{G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand_core::RngCore;
use std::ops::{Add, Index, IndexMut};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq)]
pub struct PublicCoefficients {
pub(crate) coefficients: Vec<G2Projective>,
}
impl PublicCoefficients {
pub(crate) fn size(&self) -> usize {
self.coefficients.len()
}
pub(crate) fn nth(&self, n: usize) -> &G2Projective {
&self.coefficients[n]
}
pub(crate) fn is_empty(&self) -> bool {
self.coefficients.is_empty()
}
pub(crate) fn inner(&self) -> &[G2Projective] {
&self.coefficients
}
pub(crate) fn evaluate_at(&self, x: &Scalar) -> G2Projective {
if self.coefficients.is_empty() {
G2Projective::identity()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().into() {
// we checked that coefficients are not empty so unwrap here is fine
*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()
}
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let coeffs = self.coefficients.len();
let mut bytes = Vec::with_capacity(4 + 96 * coeffs);
bytes.extend_from_slice(&((coeffs as u32).to_be_bytes()));
for coeff in &self.coefficients {
bytes.extend_from_slice(coeff.to_bytes().as_ref())
}
bytes
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, DkgError> {
if b.len() < 4 {
return Err(DkgError::new_deserialization_failure(
"PublicCoefficients",
"insufficient number of bytes provided",
));
}
let coeffs = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize;
let mut coefficients = Vec::with_capacity(coeffs);
if b.len() != 4 + coeffs * 96 {
return Err(DkgError::new_deserialization_failure(
"PublicCoefficients",
"insufficient number of bytes provided",
));
}
let mut i = 4;
for _ in 0..coeffs {
let coefficient = deserialize_g2(&b[i..i + 96]).ok_or_else(|| {
DkgError::new_deserialization_failure(
"PublicCoefficients.coefficient",
"invalid curve point",
)
})?;
coefficients.push(coefficient);
i += 96;
}
Ok(PublicCoefficients { coefficients })
}
}
impl Index<usize> for PublicCoefficients {
type Output = G2Projective;
fn index(&self, index: usize) -> &Self::Output {
self.coefficients.index(index)
}
}
impl IndexMut<usize> for PublicCoefficients {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
self.coefficients.index_mut(index)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Zeroize)]
#[zeroize(drop)]
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])
/// Creates new pseudorandom polynomial of specified degree.
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
Polynomial {
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
}
}
/// Creates new polynomial with provided coefficients.
pub fn new(coefficients: Vec<Scalar>) -> Self {
Polynomial { coefficients }
}
pub fn set_constant_coefficient(&mut self, value: Scalar) {
if self.coefficients.is_empty() {
self.coefficients = vec![value]
} else {
self.coefficients[0] = value
}
}
/// Creates a zero-polynomial, i.e. p(x) = 0
pub const fn zero() -> Self {
Polynomial {
coefficients: Vec::new(),
}
}
/// Returns public coefficients associated with this polynomial.
pub fn public_coefficients(&self) -> PublicCoefficients {
let g2 = G2Projective::generator();
let coefficients = self.coefficients.iter().map(|a_i| g2 * a_i).collect();
PublicCoefficients { coefficients }
}
/// Evaluates the polynomial at point x.
pub fn evaluate_at(&self, x: &Scalar) -> Scalar {
if self.coefficients.is_empty() {
Scalar::zero()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero().into() {
// we checked that coefficients are not empty so unwrap here is fine
*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()
}
}
}
impl Index<usize> for Polynomial {
type Output = Scalar;
fn index(&self, index: usize) -> &Self::Output {
self.coefficients.index(index)
}
}
impl IndexMut<usize> for Polynomial {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
self.coefficients.index_mut(index)
}
}
impl<'b> Add<&'b Polynomial> for Polynomial {
type Output = Polynomial;
fn add(self, rhs: &'b Polynomial) -> Polynomial {
&self + rhs
}
}
impl<'a> Add<Polynomial> for &'a Polynomial {
type Output = Polynomial;
fn add(self, rhs: Polynomial) -> Polynomial {
self + &rhs
}
}
impl Add<Polynomial> for Polynomial {
type Output = Polynomial;
fn add(self, rhs: Polynomial) -> Polynomial {
&self + &rhs
}
}
impl<'a, 'b> Add<&'b Polynomial> for &'a Polynomial {
type Output = Polynomial;
fn add(self, rhs: &'b Polynomial) -> Self::Output {
let len = self.coefficients.len();
let rhs_len = rhs.coefficients.len();
// to have easier bound checks
if rhs_len > len {
return rhs + self;
}
// we know len >= rhs_len and hence the output will also be of size len
let mut res = Vec::with_capacity(len);
for i in 0..len {
if let Some(rhs_coeff) = rhs.coefficients.get(i) {
res.push(self[i] + rhs_coeff)
} else {
res.push(self[i])
}
}
Polynomial { coefficients: res }
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand_core::SeedableRng;
#[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_at(&Scalar::from(1)));
assert_eq!(Scalar::from(42), poly.evaluate_at(&Scalar::from(0)));
assert_eq!(Scalar::from(42), poly.evaluate_at(&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_at(&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_at(&Scalar::from(3)));
// empty polynomial
let poly = Polynomial::zero();
// should always be 0
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(1)));
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(0)));
assert_eq!(Scalar::from(0), poly.evaluate_at(&Scalar::from(10)));
}
#[test]
fn polynomial_addition() {
let empty = Polynomial::zero();
let p1 = Polynomial {
coefficients: vec![Scalar::from(1), Scalar::from(2), Scalar::from(3)],
};
let p2 = Polynomial {
coefficients: vec![Scalar::from(4), Scalar::from(5)],
};
let expected_sum = Polynomial {
coefficients: vec![Scalar::from(5), Scalar::from(7), Scalar::from(3)],
};
assert_eq!(p1, &p1 + &empty);
assert_eq!(p1, empty + &p1);
assert_eq!(expected_sum, &p1 + &p2);
assert_eq!(expected_sum, &p2 + &p1);
}
#[test]
fn public_coefficients_evaluation() {
// we use the same values as in polynomial evaluation test
let g2 = G2Projective::generator();
// y = 42 (it should be 42 regardless of x)
let coeffs = PublicCoefficients {
coefficients: vec![g2 * Scalar::from(42)],
};
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(1)));
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(0)));
assert_eq!(g2 * Scalar::from(42), coeffs.evaluate_at(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = PublicCoefficients {
coefficients: vec![g2 * Scalar::from(10), g2 * Scalar::from(1)],
};
assert_eq!(g2 * Scalar::from(12), poly.evaluate_at(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let coeffs = PublicCoefficients {
coefficients: vec![
(-g2 * Scalar::from(3)),
g2 * Scalar::from(2),
(-g2 * Scalar::from(5)),
G2Projective::identity(),
g2 * Scalar::from(1),
],
};
assert_eq!(g2 * Scalar::from(39), coeffs.evaluate_at(&Scalar::from(3)));
// empty coefficients
let coeffs = PublicCoefficients {
coefficients: Vec::new(),
};
// should always be 0
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(1))
);
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(0))
);
assert_eq!(
G2Projective::identity(),
coeffs.evaluate_at(&Scalar::from(10))
);
}
#[test]
fn public_coefficients_roundtrip() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let good = vec![
Polynomial::zero().public_coefficients(),
Polynomial::new_random(&mut rng, 0).public_coefficients(),
Polynomial::new_random(&mut rng, 1).public_coefficients(),
Polynomial::new_random(&mut rng, 4).public_coefficients(),
Polynomial::new_random(&mut rng, 15).public_coefficients(),
];
for coefficient in good {
let bytes = coefficient.to_bytes();
let recovered = PublicCoefficients::try_from_bytes(&bytes).unwrap();
assert_eq!(coefficient, recovered);
}
assert!(PublicCoefficients::try_from_bytes(&[]).is_err());
assert!(PublicCoefficients::try_from_bytes(&[1]).is_err());
assert!(PublicCoefficients::try_from_bytes(&[1, 2, 3, 4]).is_err());
let g2 = G2Projective::generator().to_bytes();
let mut bad_length = Vec::new();
bad_length.extend_from_slice(&2u32.to_be_bytes());
bad_length.extend_from_slice(g2.as_ref());
assert!(PublicCoefficients::try_from_bytes(&bad_length).is_err());
let mut incomplete = Vec::new();
incomplete.extend_from_slice(&1u32.to_be_bytes());
incomplete.extend_from_slice(&g2.as_ref()[..95]);
assert!(PublicCoefficients::try_from_bytes(&incomplete).is_err());
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// forward-secure public key encryption scheme
pub mod bte;
pub mod error;
pub mod interpolation;
// this entire module is a big placeholder for whatever scheme we decide to use for the
// secure channel encryption scheme, but I would assume that the top-level API would
// remain more or less the same
pub mod dealing;
pub(crate) mod share;
pub(crate) mod utils;
pub use dealing::*;
pub use share::*;
// TODO: presumably this should live in a some different, common, crate?
pub type Threshold = u64;
pub type NodeIndex = u64;
#[cfg(test)]
mod tests {
use crate::interpolation::perform_lagrangian_interpolation_at_origin;
use crate::interpolation::polynomial::Polynomial;
use bls12_381::Scalar;
use rand_chacha::rand_core::SeedableRng;
#[test]
fn basic_dummy_secret_sharing() {
let degree = 2;
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let p1 = Polynomial::new_random(&mut rng, degree);
let p2 = Polynomial::new_random(&mut rng, degree);
let p3 = Polynomial::new_random(&mut rng, degree);
let p4 = Polynomial::new_random(&mut rng, degree);
let zero = Scalar::zero();
let one = Scalar::one();
let two = Scalar::from(2);
let three = Scalar::from(3);
let four = Scalar::from(4);
// i.e. given:
// p1 = a1 + x * b1 + ...
// p2 = a2 + x * b2 + ...
// ...
// expected = (a1 + a2 + ...) + x * (b1 + b2 + ...) + ...
// note: master polynomial is NEVER explicitly computed
let expected_master = &p1 + &p2 + &p3 + &p4;
let v1_secret = p1.evaluate_at(&one)
+ p2.evaluate_at(&one)
+ p3.evaluate_at(&one)
+ p4.evaluate_at(&one);
let v2_secret = p1.evaluate_at(&two)
+ p2.evaluate_at(&two)
+ p3.evaluate_at(&two)
+ p4.evaluate_at(&two);
let v3_secret = p1.evaluate_at(&three)
+ p2.evaluate_at(&three)
+ p3.evaluate_at(&three)
+ p4.evaluate_at(&three);
let v4_secret = p1.evaluate_at(&four)
+ p2.evaluate_at(&four)
+ p3.evaluate_at(&four)
+ p4.evaluate_at(&four);
// note that the following would have never happened in actual dkg setting, but it's
// used here mostly for a sanity check on the maths used
let samples = vec![
(one, v1_secret),
(two, v2_secret),
(three, v3_secret),
(four, v4_secret),
];
let master_secret = perform_lagrangian_interpolation_at_origin(&samples).unwrap();
assert_eq!(expected_master.evaluate_at(&zero), master_secret);
assert_eq!(expected_master.evaluate_at(&one), v1_secret);
assert_eq!(expected_master.evaluate_at(&two), v2_secret);
assert_eq!(expected_master.evaluate_at(&three), v3_secret);
assert_eq!(expected_master.evaluate_at(&four), v4_secret);
// since we have 4 parties, but polynomials used are of degree 2, we only need at least 3
// issuers to contribute
let samples2 = vec![(one, v1_secret), (three, v3_secret), (four, v4_secret)];
let master_secret2 = perform_lagrangian_interpolation_at_origin(&samples2).unwrap();
assert_eq!(master_secret, master_secret2)
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::{CHUNK_BYTES, NUM_CHUNKS, SCALAR_SIZE};
use crate::error::DkgError;
use crate::interpolation::perform_lagrangian_interpolation_at_origin;
use crate::NodeIndex;
use bls12_381::Scalar;
use zeroize::Zeroize;
// if this type is changed, one must ensure all values can fit in it
pub type Chunk = u16;
#[derive(PartialEq, Eq, Debug, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub struct Share(pub(crate) Scalar);
pub fn combine_shares(shares: Vec<Share>, node_indices: &[NodeIndex]) -> Result<Scalar, DkgError> {
if shares.len() != node_indices.len() {
return Err(DkgError::MismatchedLagrangianSamplesLengths {
x: node_indices.len(),
y: shares.len(),
});
}
let samples = shares
.into_iter()
.zip(node_indices.iter())
.map(|(share, index)| (Scalar::from(*index), share.0))
.collect::<Vec<_>>();
perform_lagrangian_interpolation_at_origin(&samples)
}
impl Share {
// not really used outside tests
#[cfg(test)]
pub(crate) fn random(mut rng: impl rand_core::RngCore) -> Self {
use ff::Field;
Share(Scalar::random(&mut rng))
}
pub(crate) fn to_chunks(&self) -> ChunkedShare {
let mut chunks = [0; NUM_CHUNKS];
let mut bytes = self.0.to_bytes();
for (chunk, chunk_bytes) in chunks.iter_mut().zip(bytes[..].chunks_exact(CHUNK_BYTES)) {
let mut tmp = [0u8; CHUNK_BYTES];
tmp.copy_from_slice(chunk_bytes);
*chunk = Chunk::from_le_bytes(tmp)
}
bytes.zeroize();
ChunkedShare { chunks }
}
pub(crate) fn inner(&self) -> &Scalar {
&self.0
}
}
impl From<Scalar> for Share {
fn from(s: Scalar) -> Self {
Share(s)
}
}
#[derive(Default, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub(crate) struct ChunkedShare {
pub(crate) chunks: [Chunk; NUM_CHUNKS],
}
impl From<Share> for ChunkedShare {
fn from(share: Share) -> ChunkedShare {
share.to_chunks()
}
}
impl TryFrom<ChunkedShare> for Share {
type Error = DkgError;
fn try_from(chunked: ChunkedShare) -> Result<Share, Self::Error> {
let mut bytes = [0u8; SCALAR_SIZE];
for (chunk, chunk_bytes) in chunked
.chunks
.iter()
.zip(bytes[..].chunks_exact_mut(CHUNK_BYTES))
{
let tmp = chunk.to_le_bytes();
chunk_bytes.copy_from_slice(&tmp[..]);
}
let recovered = Option::from(Scalar::from_bytes(&bytes))
.map(Share)
.ok_or(DkgError::MalformedShare)?;
bytes.zeroize();
Ok(recovered)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::combine_scalar_chunks;
use rand_core::SeedableRng;
#[test]
fn chunking_share() {
let dummy_seed = [1u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let share = Share::random(&mut rng);
let chunks: ChunkedShare = share.clone().into();
let scalar_chunks = chunks
.chunks
.iter()
.map(|c| Scalar::from(*c as u64))
.collect::<Vec<_>>();
let expected = combine_scalar_chunks(&scalar_chunks);
assert_eq!(expected, share.0);
let recombined: Share = chunks.try_into().unwrap();
assert_eq!(expected, recombined.0);
}
}
+4
View File
@@ -0,0 +1,4 @@
Just a todo file to keep track of what should be changed
- Deriving challenges and oracles should be more streamlined; perhaps some trait for hashing
- perhaps there could be additional intermediate types in proofs of knowledge (for moves / responses / etc)
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bte::CHUNK_SIZE;
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
use bls12_381::G1Projective;
use bls12_381::{G2Projective, Scalar};
use group::GroupEncoding;
use sha2::{Digest, Sha256};
#[macro_export]
macro_rules! ensure_len {
($a:expr, $b:expr) => {
if $a.len() != $b {
return false;
}
};
}
pub(crate) struct RandomOracleBuilder {
inner_state: Sha256,
}
impl RandomOracleBuilder {
pub(crate) fn new(domain: &[u8]) -> Self {
let mut inner_state = Sha256::new();
inner_state.update(domain);
RandomOracleBuilder { inner_state }
}
pub(crate) fn update(&mut self, data: impl AsRef<[u8]>) {
self.inner_state.update(data)
}
pub(crate) fn update_with_g1_elements<'a, I>(&mut self, items: I)
where
I: Iterator<Item = &'a G1Projective>,
{
items.for_each(|item| self.update(item.to_bytes()))
}
pub(crate) fn finalize(self) -> [u8; 32] {
self.inner_state.finalize().into()
}
}
// those will most likely need to somehow get re-combined with coconut (or maybe extracted to a completely different module)
pub(crate) fn hash_to_scalar<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> Scalar {
// the unwrap here is fine as the result vector will have 1 element (as specified) and will not be empty
hash_to_scalars(msg, domain, 1).pop().unwrap()
}
pub(crate) fn hash_to_scalars<M: AsRef<[u8]>>(msg: M, domain: &[u8], n: usize) -> Vec<Scalar> {
let mut output = vec![Scalar::zero(); n];
Scalar::hash_to_field::<ExpandMsgXmd<Sha256>>(msg.as_ref(), domain, &mut output);
output
}
pub(crate) fn hash_g2<M: AsRef<[u8]>>(msg: M, domain: &[u8]) -> G2Projective {
<G2Projective as HashToCurve<ExpandMsgXmd<Sha256>>>::hash_to_curve(msg, domain)
}
pub(crate) fn combine_scalar_chunks(chunks: &[Scalar]) -> Scalar {
let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64);
chunks.iter().rev().fold(Scalar::zero(), |mut acc, chunk| {
acc *= chunk_size_scalar;
acc += chunk;
acc
})
}
pub(crate) fn combine_g1_chunks(chunks: &[G1Projective]) -> G1Projective {
let chunk_size_scalar = Scalar::from(CHUNK_SIZE as u64);
chunks
.iter()
.rev()
.fold(G1Projective::identity(), |mut acc, chunk| {
acc *= chunk_size_scalar;
acc += chunk;
acc
})
}
pub(crate) fn deserialize_scalar(b: &[u8]) -> Option<Scalar> {
if b.len() != 32 {
None
} else {
let mut repr: [u8; 32] = Default::default();
repr.as_mut().copy_from_slice(b);
Scalar::from_bytes(&repr).into()
}
}
pub(crate) fn deserialize_g1(b: &[u8]) -> Option<G1Projective> {
if b.len() != 48 {
None
} else {
let mut encoding = <G1Projective as GroupEncoding>::Repr::default();
encoding.as_mut().copy_from_slice(b);
G1Projective::from_bytes(&encoding).into()
}
}
pub(crate) fn deserialize_g2(b: &[u8]) -> Option<G2Projective> {
if b.len() != 96 {
None
} else {
let mut encoding = <G2Projective as GroupEncoding>::Repr::default();
encoding.as_mut().copy_from_slice(b);
G2Projective::from_bytes(&encoding).into()
}
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G2Projective, Scalar};
use dkg::bte::{decrypt_share, keygen, setup, Epoch};
use dkg::interpolation::perform_lagrangian_interpolation_at_origin;
use dkg::{combine_shares, try_recover_verification_keys, Dealing};
use rand_core::SeedableRng;
use std::collections::BTreeMap;
#[test]
fn single_sender() {
// makes it easier to understand than `full_threshold_secret_sharing`
// and is a good stepping stone, because its everything each node will have to perform (from one point of view)
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
// TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET
// verify remote proofs of key possession
for key in full_keys.iter() {
assert!(key.1.verify());
}
let (dealing, dealer_share) = Dealing::create(
&mut rng,
&params,
node_indices[0],
threshold,
epoch,
&receivers,
None,
);
dealing
.verify(&params, epoch, threshold, &receivers, None)
.unwrap();
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap();
}
// and for good measure, check that the dealer's share matches decryption result
let recovered_dealer =
decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, epoch, None).unwrap();
assert_eq!(recovered_dealer, dealer_share.unwrap());
}
#[test]
fn full_threshold_secret_sharing() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
// TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET
// verify remote proofs of key possession
for key in full_keys.iter() {
assert!(key.1.verify());
}
let dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
for dealing in dealings.iter() {
dealing
.verify(&params, epoch, threshold, &receivers, None)
.unwrap();
}
// recover verification keys
let (recovered_master, recovered_partials) =
try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
let g2 = G2Projective::generator();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
// so for ease of use (IN A TEST SETTING), just decrypt one's own share
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
// make sure it matches the associated vk
assert_eq!(recovered_partials[i], g2 * recovered_secret);
derived_secrets.push(recovered_secret)
}
// sanity check that the shares were combined correctly and if we take threshold number of them,
// we end up with the same master secret, note: those are NEVER explicitly recovered in actual system
// (remember threshold was 2)
let master1 = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), derived_secrets[0]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
let master2 = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[1]), derived_secrets[1]),
(Scalar::from(node_indices[2]), derived_secrets[2]),
])
.unwrap();
assert_eq!(master1, master2);
assert_eq!(recovered_master, g2 * master1);
}
#[test]
fn full_threshold_secret_resharing() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let params = setup();
// the simplest possible case
let threshold = 2;
// the indices are going to get assigned externally, so for test sake, use non-consecutive ones
let node_indices = vec![15u64, 248, 33521];
let mut receivers = BTreeMap::new();
let mut full_keys = Vec::new();
for index in &node_indices {
let (dk, pk) = keygen(&params, &mut rng);
receivers.insert(*index, *pk.public_key());
full_keys.push((dk, pk))
}
// start off in a defined epoch (i.e. not root);
let epoch = Epoch::new(2);
let first_dealings = node_indices
.iter()
.map(|&dealer_index| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
epoch,
&receivers,
None,
)
.0
})
.collect::<Vec<_>>();
// recover verification keys
let (public_original_master, recovered_partials) =
try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
let mut derived_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(epoch, &params, &mut rng).unwrap();
let shares = first_dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap())
.collect();
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
derived_secrets.push(recovered_secret)
}
let original_master = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), derived_secrets[0]),
(Scalar::from(node_indices[1]), derived_secrets[1]),
])
.unwrap();
let next_epoch = Epoch::new(3);
// attempt to create resharing dealings!
let resharing_dealings = node_indices
.iter()
.zip(derived_secrets.iter())
.map(|(&dealer_index, prior_secret)| {
Dealing::create(
&mut rng,
&params,
dealer_index,
threshold,
next_epoch,
&receivers,
Some(*prior_secret),
)
.0
})
.collect::<Vec<_>>();
for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) {
reshared_dealing
.verify(&params, next_epoch, threshold, &receivers, Some(*prior_vk))
.unwrap();
}
// recover verification keys
let (public_reshared_master, reshared_partials) =
try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
let mut reshared_secrets = Vec::new();
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
dk.try_update_to(next_epoch, &params, &mut rng).unwrap();
let shares = resharing_dealings
.iter()
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, next_epoch, None).unwrap())
.collect();
let recovered_secret =
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap();
reshared_secrets.push(recovered_secret)
}
let reshared_master = perform_lagrangian_interpolation_at_origin(&[
(Scalar::from(node_indices[0]), reshared_secrets[0]),
(Scalar::from(node_indices[1]), reshared_secrets[1]),
])
.unwrap();
// the master secret and public values didn't change
assert_eq!(original_master, reshared_master);
assert_eq!(public_original_master, public_reshared_master);
// but partials did
assert_ne!(derived_secrets, reshared_secrets);
assert_ne!(recovered_partials, reshared_partials);
}
@@ -69,9 +69,10 @@ mod tests {
#[test]
fn wrong_prefix_fails() {
assert_eq!(
Err(Bech32Error::WrongPrefix(
"your bech32 address prefix should be nymt, not punk".to_string()
)),
Err(Bech32Error::WrongPrefix(format!(
"your bech32 address prefix should be {}, not punk",
DEFAULT_NETWORK.bech32_prefix()
))),
validate_bech32_prefix("punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0")
)
}
@@ -80,7 +81,9 @@ mod tests {
fn correct_prefix_works() {
assert_eq!(
Ok(()),
validate_bech32_prefix("nymt1z9egw0knv47nmur0p8vk4rcx59h9gg4zuxrrr9")
validate_bech32_prefix(
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"
)
)
}
}
+2 -2
View File
@@ -3,8 +3,8 @@
fn main() {
match option_env!("NETWORK") {
Some("mainnet") => println!("cargo:rustc-cfg=network=\"mainnet\"",),
None | Some("sandbox") => println!("cargo:rustc-cfg=network=\"sandbox\"",),
None | Some("mainnet") => println!("cargo:rustc-cfg=network=\"mainnet\"",),
Some("sandbox") => println!("cargo:rustc-cfg=network=\"sandbox\"",),
Some("qa") => println!("cargo:rustc-cfg=network=\"qa\""),
_ => panic!("No such network"),
}
+1 -1
View File
@@ -21,6 +21,6 @@ pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hh
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://rpc.nyx.nodes.guru/",
Some("https://api.nyx.nodes.guru/"),
Some("https://validator.nymtech.net/api"),
)]
}
+209 -18
View File
@@ -41,7 +41,7 @@ checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4"
[[package]]
name = "bandwidth-claim"
version = "1.0.0-rc.1"
version = "1.0.0"
dependencies = [
"bandwidth-claim-contract",
"config",
@@ -144,6 +144,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cc"
version = "1.0.72"
@@ -200,6 +206,9 @@ dependencies = [
"config",
"cosmwasm-std",
"cosmwasm-storage",
"cw-controllers",
"cw-multi-test",
"cw-storage-plus",
"schemars",
"serde",
"thiserror",
@@ -209,6 +218,7 @@ dependencies = [
name = "coconut-bandwidth-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
@@ -241,9 +251,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta7"
version = "1.0.0-beta8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88c2565b1e73a816fb659ef4838fc356143fbd35f43c48a51d2d7d4e5d6679d3"
checksum = "37e70111e9701c3ec43bfbff0e523cd4cb115876b4d3433813436dd0934ee962"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -254,18 +264,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta7"
version = "1.0.0-beta8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa89fcdf8dbbe0088e663d0a814aa7368e7ebe8fb045a3a150fb5fdc2ffe3b45"
checksum = "58bc2ad5d86be5f6068833f63e20786768db6890019c095dd7775232184fb7b3"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-schema"
version = "1.0.0-beta3"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "818b928263c09a3269c2bed22494a62107a43ef87900e273af8ad2cb9f7e4440"
checksum = "63f79866e7b2190b6b6cb06959e308183c8d9511a8530f7292073f3cddc963db"
dependencies = [
"schemars",
"serde_json",
@@ -273,9 +283,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta7"
version = "1.0.0-beta8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb8f99a61d0b9069e1afc80a4ffea87dcc3523edd992080923870b13a677da0"
checksum = "915ca82bd944f116f3a9717481f3fa657e4a73f28c4887288761ebb24e6fbe10"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -290,9 +300,9 @@ dependencies = [
[[package]]
name = "cosmwasm-storage"
version = "1.0.0-beta7"
version = "1.0.0-beta8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07f856099c824aa8f2488e62d1da3fc06383d3fdbc764573595f451be43441a2"
checksum = "1c4be9fd8c9d3ae7d0c32a925ecbc20707007ce0cba1f7538c0d78b7a2d3729b"
dependencies = [
"cosmwasm-std",
"serde",
@@ -382,16 +392,148 @@ dependencies = [
]
[[package]]
name = "cw-storage-plus"
version = "0.13.1"
name = "cw-controllers"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e8b7f9a758c030d375520df947323c052704f784561fc28dcaab4f988c50a30"
checksum = "1bc6d042b14823b0e9f33f5cdd67a1eb9b16a7d79f7547b1a73c8870b518b97b"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw-multi-test"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbea57e5be4a682268a5eca1a57efece57a54ff216bfd87603d5e864aad40e12"
dependencies = [
"anyhow",
"cosmwasm-std",
"cosmwasm-storage",
"cw-storage-plus",
"cw-utils",
"derivative",
"itertools",
"prost",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw-storage-plus"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9336ecef1e19d56cf6e3e932475fc6a3dee35eec5a386e07917a1d1ba6bb0e35"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "cw-utils"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw2"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686da2d2b3b646bea15d448dc25c3e132097a7c40ef9ba7b4db741375b6181f"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"serde",
]
[[package]]
name = "cw3"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7c7a87aed0637432c9ec39a691e533e18006dd400c67bce9f0ad643de7d52c"
dependencies = [
"cosmwasm-std",
"cw-utils",
"schemars",
"serde",
]
[[package]]
name = "cw3-fixed-multisig"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666afbb3bcaefc697047a0be8c4c5015be053a8456d27dd26e0b8ab114f6f5dd"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw3",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw3-flex-multisig"
version = "0.13.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw3",
"cw3-fixed-multisig",
"cw4",
"cw4-group",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw4"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d51b6e05094bfb91029f5baf5d1f39ee10f16fd61f8bf0e6f6632a5dbfb7f9"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"serde",
]
[[package]]
name = "cw4-group"
version = "0.13.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw4",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "der"
version = "0.4.5"
@@ -401,6 +543,17 @@ dependencies = [
"const-oid",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.8.1"
@@ -475,6 +628,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "elliptic-curve"
version = "0.10.6"
@@ -714,6 +873,15 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
@@ -812,9 +980,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.14"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
dependencies = [
"cfg-if",
]
@@ -1061,6 +1229,29 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "prost"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -1467,9 +1658,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "uint"
version = "0.9.1"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0"
dependencies = [
"byteorder",
"crunchy",
+1 -1
View File
@@ -1,5 +1,5 @@
[workspace]
members = ["bandwidth-claim", "coconut-bandwidth", "mixnet", "vesting"]
members = ["bandwidth-claim", "coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group"]
[profile.release]
opt-level = 3
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "bandwidth-claim"
version = "1.0.0-rc.1"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,8 +14,8 @@ config = { path = "../../common/config"}
[dependencies]
bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" }
cosmwasm-std = "1.0.0-beta6"
cosmwasm-storage = "1.0.0-beta6"
cosmwasm-std = "1.0.0-beta8"
cosmwasm-storage = "1.0.0-beta8"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+7 -2
View File
@@ -13,9 +13,14 @@ bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" }
coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta8"
cosmwasm-storage = "1.0.0-beta8"
cw-storage-plus = "0.13.2"
cw-controllers = "0.13.2"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = "1.0.23"
[dev-dependencies]
cw-multi-test = { version = "0.13.2" }
+163
View File
@@ -0,0 +1,163 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use coconut_bandwidth_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use crate::error::ContractError;
use crate::state::{Config, ADMIN, CONFIG};
use crate::transactions;
/// Instantiate the contract.
///
/// `deps` contains Storage, API and Querier
/// `msg` is the contract initialization message, sort of like a constructor call.
#[entry_point]
pub fn instantiate(
mut deps: DepsMut<'_>,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?;
let pool_addr = deps.api.addr_validate(&msg.pool_addr)?;
ADMIN.set(deps.branch(), Some(multisig_addr.clone()))?;
let cfg = Config {
multisig_addr,
pool_addr,
};
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::default())
}
/// Handle an incoming message
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::DepositFunds { data } => transactions::deposit_funds(deps, env, info, data),
ExecuteMsg::ReleaseFunds { funds } => transactions::release_funds(deps, env, info, funds),
}
}
#[entry_point]
pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult<Binary> {
unimplemented!();
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::helpers::*;
use coconut_bandwidth_contract_common::deposit::DepositData;
use config::defaults::DENOM;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, Addr};
use cw_multi_test::Executor;
use serde::de::Unexpected::Str;
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {
multisig_addr: String::from(MULTISIG_CONTRACT),
pool_addr: String::from(POOL_CONTRACT),
};
let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
// Contract balance should be 0
assert_eq!(
coins(0, DENOM),
vec![deps
.as_ref()
.querier
.query_balance(env.contract.address, DENOM)
.unwrap()]
);
}
#[test]
fn deposit_and_release() {
let init_funds = coins(10, DENOM);
let deposit_funds = coins(1, DENOM);
let release_funds = coins(2, DENOM);
let mut app = mock_app(&init_funds);
let multisig_addr = String::from(MULTISIG_CONTRACT);
let pool_addr = String::from(POOL_CONTRACT);
let code_id = app.store_code(contract_bandwidth());
let msg = InstantiateMsg {
multisig_addr: multisig_addr.clone(),
pool_addr: pool_addr.clone(),
};
let contract_addr = app
.instantiate_contract(
code_id,
Addr::unchecked(OWNER),
&msg,
&[],
"bandwidth",
None,
)
.unwrap();
let msg = ExecuteMsg::DepositFunds {
data: DepositData::new(
String::from("info"),
String::from("id"),
String::from("enc"),
),
};
app.execute_contract(
Addr::unchecked(OWNER),
contract_addr.clone(),
&msg,
&deposit_funds,
)
.unwrap();
// try to release more then it's in the contract
let msg = ExecuteMsg::ReleaseFunds {
funds: release_funds[0].clone(),
};
let err = app
.execute_contract(
Addr::unchecked(multisig_addr.clone()),
contract_addr.clone(),
&msg,
&[],
)
.unwrap_err();
assert_eq!(ContractError::NotEnoughFunds, err.downcast().unwrap());
let msg = ExecuteMsg::ReleaseFunds {
funds: deposit_funds[0].clone(),
};
app.execute_contract(
Addr::unchecked(multisig_addr),
contract_addr.clone(),
&msg,
&[],
)
.unwrap();
let pool_bal = app.wrap().query_balance(pool_addr, DENOM).unwrap();
assert_eq!(pool_bal, deposit_funds[0]);
}
}
+7
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use cw_controllers::AdminError;
use thiserror::Error;
use config::defaults::DENOM;
@@ -23,4 +24,10 @@ pub enum ContractError {
#[error("Wrong coin denomination, you must send {}", DENOM)]
WrongDenom,
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
#[error("{0}")]
Admin(#[from] AdminError),
}
+2 -67
View File
@@ -1,73 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
mod error;
mod state;
mod support;
mod transactions;
use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response};
use crate::error::ContractError;
use coconut_bandwidth_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg};
/// Instantiate the contract.
///
/// `deps` contains Storage, API and Querier
/// `env` contains block, message and contract info
/// `msg` is the contract initialization message, sort of like a constructor call.
#[entry_point]
pub fn instantiate(
_deps: DepsMut<'_>,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {
Ok(Response::default())
}
/// Handle an incoming message
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::DepositFunds { data } => transactions::deposit_funds(deps, env, info, data),
}
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use config::defaults::DENOM;
use cosmwasm_std::coins;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {};
let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
// Contract balance should be 0
assert_eq!(
coins(0, DENOM),
vec![deps
.as_ref()
.querier
.query_balance(env.contract.address, DENOM)
.unwrap()]
);
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_controllers::Admin;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const ADMIN: Admin = Admin::new("admin");
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Config {
pub multisig_addr: Addr,
pub pool_addr: Addr,
}
pub const CONFIG: Item<Config> = Item::new("config");
@@ -3,17 +3,44 @@
#[cfg(test)]
pub mod helpers {
use crate::instantiate;
pub const OWNER: &str = "admin0001";
pub const SOMEBODY: &str = "somebody";
pub const MULTISIG_CONTRACT: &str = "multisig contract address";
pub const POOL_CONTRACT: &str = "mix pool contract address";
use crate::contract::instantiate;
use coconut_bandwidth_contract_common::msg::InstantiateMsg;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps};
use cosmwasm_std::{Addr, Coin, Empty, MemoryStorage, OwnedDeps};
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper};
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {};
let msg = InstantiateMsg {
multisig_addr: String::from(MULTISIG_CONTRACT),
pool_addr: String::from(POOL_CONTRACT),
};
let env = mock_env();
let info = mock_info("creator", &[]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
return deps;
}
pub fn mock_app(init_funds: &[Coin]) -> App {
AppBuilder::new().build(|router, _, storage| {
router
.bank
.init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec())
.unwrap();
})
}
pub fn contract_bandwidth() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
crate::contract::execute,
crate::contract::instantiate,
crate::contract::query,
);
Box::new(contract)
}
}
@@ -1,9 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Response};
use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Response};
use crate::error::ContractError;
use crate::state::{ADMIN, CONFIG};
use coconut_bandwidth_contract_common::deposit::DepositData;
use coconut_bandwidth_contract_common::events::{
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
@@ -37,12 +39,40 @@ pub(crate) fn deposit_funds(
Ok(Response::new().add_event(event))
}
pub(crate) fn release_funds(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
funds: Coin,
) -> Result<Response, ContractError> {
if funds.denom != DENOM {
return Err(ContractError::WrongDenom);
}
let current_balance = deps.querier.query_balance(env.contract.address, DENOM)?;
if funds.amount > current_balance.amount {
return Err(ContractError::NotEnoughFunds);
}
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let cfg = CONFIG.load(deps.storage)?;
let return_tokens = BankMsg::Send {
to_address: cfg.pool_addr.into(),
amount: vec![funds],
};
let response = Response::new().add_message(return_tokens);
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::helpers;
use crate::support::tests::helpers::{MULTISIG_CONTRACT, POOL_CONTRACT};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::Coin;
use cosmwasm_std::{Coin, CosmosMsg};
use cw_controllers::AdminError;
#[test]
fn invalid_deposit() {
@@ -133,4 +163,65 @@ mod tests {
.unwrap();
assert_eq!(encryption_key_attr.value, encryption_key);
}
#[test]
fn invalid_release() {
let mut deps = helpers::init_contract();
let env = mock_env();
let invalid_admin = "invalid admin";
let funds = Coin::new(1, DENOM);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
Coin::new(1, "invalid denom"),
)
.unwrap_err();
assert_eq!(err, ContractError::WrongDenom);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
funds.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::NotEnoughFunds);
deps.querier
.update_balance(env.contract.address.clone(), vec![funds.clone()]);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
funds.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {}));
}
#[test]
fn valid_release() {
let mut deps = helpers::init_contract();
let env = mock_env();
let coin = Coin::new(1, DENOM);
deps.querier
.update_balance(env.contract.address.clone(), vec![coin.clone()]);
let res = release_funds(
deps.as_mut(),
env,
mock_info(MULTISIG_CONTRACT, &[]),
coin.clone(),
)
.unwrap();
assert_eq!(
res.messages[0].msg,
CosmosMsg::Bank(BankMsg::Send {
to_address: String::from(POOL_CONTRACT),
amount: vec![coin]
})
);
}
}
+3 -3
View File
@@ -20,9 +20,9 @@ mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta6"
cosmwasm-storage = "1.0.0-beta6"
cw-storage-plus = "0.13.1"
cosmwasm-std = "1.0.0-beta8"
cosmwasm-storage = "1.0.0-beta8"
cw-storage-plus = "0.13.2"
az = "1.2.0"
bs58 = "0.4.0"

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