Compare commits

...

215 Commits

Author SHA1 Message Date
Jędrzej Stuczyński d6a81d9213 initial validator API-networking related things
adapted from DKG impl
2022-05-23 15:34:48 +01:00
Jędrzej Stuczyński a6db5fe704 Added STATE_DENOM network specific constant 2022-05-23 13:37:53 +01:00
Jędrzej Stuczyński fe57d08f3e actually calling dotenv at validator API startup 2022-05-23 13:36:05 +01:00
Jędrzej Stuczyński ae29b2300c optional serde support for x25519 keys 2022-05-23 13:34:28 +01:00
Jędrzej Stuczyński 7b98d62f96 optional serde support for ed25519 keys 2022-05-23 12:17:58 +01:00
Jędrzej Stuczyński 6abe95ed61 Added abci::Data field to ExecuteResult 2022-05-23 12:12:03 +01:00
Raphaël Walther db743578a9 Add an extra clean step in nightly build 2022-05-23 11:13:07 +02:00
Bogdan-Ștefan Neacșu ba7f535cb7 Update stats provider mainnet client address 2022-05-20 13:23:58 +03:00
Bogdan-Ștefan Neacşu fd4f5b319c Add serviced client data to stats (#1278)
* Add serviced client data to stats

* Put stats service address in network defaults

* Update CHANGELOG.md
2022-05-19 15:54:51 +03:00
Mark Sinclair 6045f57612 Merge pull request #1279 from nymtech/change-text
change text
2022-05-19 12:50:20 +01:00
Mark Sinclair 878cb3f0e5 Merge pull request #1277 from nymtech/av-uptime-nodes-list
adding percentage symbol and make column wider
2022-05-19 12:50:06 +01:00
gala1234 686c0de5eb change text 2022-05-19 13:42:51 +02:00
Drazen Urch 5ce2e0c8bb Optimize fetch and loop (#1271)
* Optimize fetch and loop

* Better operator fetch

* cargo fmt
2022-05-19 11:33:11 +02:00
Drazen Urch 68e120dbf4 Replace checked with saturating sub (#1275)
* Replace checked with saturating sub

* CHANGELOG
2022-05-18 13:33:19 +02:00
gala1234 1362fcdbfa adding percentage symbol and make column wider 2022-05-18 09:41:56 +02:00
Jon Häggblad 3281f10443 wallet: multi account support in wallet-recovery-cli (#1276)
* wallet-recovery-cli: support multi accounts

* wallet-recovery-cli: simplify tests
2022-05-17 22:48:13 +02:00
Jon Häggblad 2409f85e31 Merge pull request #1273 from nymtech/fix/wallet-store-ids-in-state
wallet: store ids in state and require password to switch account
2022-05-17 22:21:01 +02:00
Jon Häggblad 37c06f338f wallet: just print account id in log statement 2022-05-17 21:56:19 +02:00
fmtabbara 8a37df64b5 update methods 2022-05-17 15:43:37 +01:00
fmtabbara 20828d0d28 rework account selection functions 2022-05-17 15:43:22 +01:00
fmtabbara 1e3c8ed3e0 confirm password modal 2022-05-17 15:41:25 +01:00
fmtabbara 830dbcecfc extend component props 2022-05-17 15:41:06 +01:00
fmtabbara a0d3144837 extend component props 2022-05-17 15:40:18 +01:00
fmtabbara a4988e3547 tidy up 2022-05-17 15:39:56 +01:00
fmtabbara 0b585a15b7 use confirm password modal on account selection 2022-05-17 15:39:45 +01:00
fmtabbara 86a93a71e9 relocate modals to their own directory 2022-05-17 15:39:17 +01:00
fmtabbara 967692bf88 create confirm password modal 2022-05-17 15:38:06 +01:00
Jędrzej Stuczyński 662b4d2fff Modified docker setup to start 2 validators in local consensus (#1270)
it was mostly for my own needs, but figured I might create separate PR for that
2022-05-17 09:52:32 +01:00
Jon Häggblad add4747e99 wallet: more test files and regression tests (#1274)
* wallet: add test file for 1.0.4

* wallet: add test file for 1.0.5

* wallet: add test for multi account wallet file

* rustfmt
2022-05-17 10:20:39 +02:00
Bogdan-Ștefan Neacșu 242733f144 Add release configuration for network requester rocket 2022-05-17 11:19:47 +03:00
Jon Häggblad 0cc1926636 Merge branch 'develop' into fix/wallet-store-ids-in-state 2022-05-17 07:22:30 +02:00
Jon Häggblad 0f54c073a7 wallet: simple cli tool for decrypting the wallet (#1272)
* wallet: simple cli tool for decrypting the wallet

* changelog: add note about wallet file recovery tool
2022-05-17 07:18:50 +02:00
Jon Häggblad 3bfce128a6 wallet: store id instead of mnemonic in state 2022-05-17 00:33:23 +02:00
Raphaël Walther 1b3e79e84d Add a way to manually trigger build 2022-05-16 16:43:42 +02:00
Gala e94f99211f Merge pull request #1269 from nymtech/av-uptime-nodes-list
adding average uptime on mix nodes table
2022-05-16 16:04:10 +02:00
gala1234 003ea095cc remove not needed variable, PR request 2022-05-16 14:12:31 +02:00
gala1234 74d93df74b adding av uptime on mix nodes table 2022-05-16 13:41:10 +02:00
Bogdan-Ștefan Neacşu 0c66cc7393 Feature/service stats (#1267)
* Send message from service provider to stats service

* Put some actual data in stats

* Put stats sender on its own thread and send response data too

* Use SQLite for storing stats

* Add the data interval and timestamp

* Fix clippy

* Set description at boot

* Guard stats service functionality under a feature for now

* Make stats service address data into consts

* Add README to network requester

* Retrieve sql data in interval

* Expose sql data via rocket rest api

* Add entry to changelog
2022-05-16 12:06:15 +03:00
Fouad 82abfa5c5c Merge pull request #1265 from nymtech/sprint/wallet-multiple-accounts
Support multiple accounts in the wallet
2022-05-13 12:46:01 +01:00
fmtabbara f7486f0490 add field validation for mnemonic and account name 2022-05-13 12:22:19 +01:00
Jon Häggblad ee5f0f5808 cargo: update lock files 2022-05-13 13:16:03 +02:00
Jon Häggblad 6cb25cf1fb wallet: dont use default wallet when removing 2022-05-13 11:53:27 +02:00
Jon Häggblad db01c245d9 wallet: update wallet error enum cases 2022-05-13 11:53:22 +02:00
Jon Häggblad 53347bec67 changelog: added note about multiple accounts 2022-05-13 11:23:50 +02:00
Jon Häggblad 252385688d wallet: remove unnecessary blank lines 2022-05-13 09:35:27 +02:00
Jon Häggblad cbcef9fbcd wallet: switch from PathBuf to Path 2022-05-13 09:18:21 +02:00
Jon Häggblad e27fc82524 wallet: use login_id and account_id instead of id and inner_id 2022-05-13 09:00:09 +02:00
Jon Häggblad f661bf0446 wallet: write_to_file function 2022-05-12 22:15:38 +02:00
Jon Häggblad 1d22f35c82 wallet: pass by ref 2022-05-12 22:05:40 +02:00
Jon Häggblad fa41fe62c4 wallet: simplify naming 2022-05-12 21:48:23 +02:00
fmtabbara 94a006a725 Merge branch 'develop' into sprint/wallet-multiple-accounts 2022-05-12 15:13:12 +01:00
Jon Häggblad 98cbd2509c wallet: more moving things around 2022-05-12 15:24:38 +02:00
Jon Häggblad d2d99ca5c9 wallet: additional tidy 2022-05-12 14:43:22 +02:00
Jon Häggblad 9e4904ff37 wallet: tweak names of wallet_storage functions 2022-05-12 14:43:22 +02:00
Jon Häggblad bf9db4128d wallet: qualify with wallet_storage 2022-05-12 14:43:22 +02:00
Jon Häggblad bce86235c7 wallet: create LoginId type to distinguish 2022-05-12 11:56:00 +02:00
Tommy Verrall 423f6bfb55 Merge pull request #1266 from nymtech/feature/update-qa
Update QA network defaults
2022-05-12 10:22:12 +01:00
tommy 38fcbb7f2e fmt 2022-05-12 10:21:37 +01:00
tommy 1785b10d91 clean up 2022-05-12 10:20:52 +01:00
Jon Häggblad 7771c5d3d9 wallet: create password with multi-account by default 2022-05-12 10:28:45 +02:00
Jon Häggblad d6206a04bd wallet: restore test in account 2022-05-12 08:37:54 +02:00
Jon Häggblad 83a7f6577b wallet: logout before re-connecting 2022-05-11 22:26:54 +02:00
Jon Häggblad 97c6567139 wallet: restore unit tests 2022-05-11 22:26:48 +02:00
tommy a1534d23af add correct bech32 address as the network-explorer-api was complaining 2022-05-11 18:17:49 +01:00
tommy bcbda85477 re-map coin type for qa 2022-05-11 16:01:04 +01:00
Jon Häggblad 54e95d795e wallet: revert back to old default account name for the time being 2022-05-11 16:48:10 +02:00
tommy 11c0e79725 fmt 2022-05-11 15:03:06 +01:00
tommy 4525be9871 Update QA vars 2022-05-11 14:56:07 +01:00
fmtabbara b83d4ca1a3 reset to initial step after add account 2022-05-11 13:47:41 +01:00
fmtabbara bfb868bfc7 move textfield components to global components 2022-05-11 13:39:01 +01:00
fmtabbara 7b00282b27 reset add account step on completion 2022-05-11 13:21:22 +01:00
Jon Häggblad 54194c03e1 wallet: fix default inner account name 2022-05-11 12:17:08 +02:00
fmtabbara 87c2a317d5 merge develop 2022-05-11 10:54:25 +01:00
Jon Häggblad be92171fec common/config: fix clippy (#1264)
* common/config: fix clippy

* common/config: use cfg_if
2022-05-10 13:32:18 +02:00
Jon Häggblad 04eef83c15 wallet: add backend support for validator name (#1262)
* wallet: add support for validator nymd name

* changelog: add entry for wallt validator name

* rustfmt

* wallet: keep nymd_name entirely on wallet side

* wallet: lint fixes
2022-05-10 11:26:49 +02:00
fmtabbara 25cc7dbebf content update 2022-05-10 10:23:08 +01:00
Jon Häggblad beeb67e9c2 wallet: change default name of first account 2022-05-10 10:25:40 +02:00
Fouad ec19de6fa3 Merge pull request #1227 from nymtech/feature/multi-accounts-integrate-rust
Multiple accounts
2022-05-09 14:40:59 +01:00
fmtabbara 575845af38 fix lint errors 2022-05-09 13:38:38 +01:00
fmtabbara b6b757436e remove edit button until feature delivered 2022-05-09 13:36:38 +01:00
fmtabbara eda69447de add how-to modal for creating multi accounts 2022-05-09 13:31:01 +01:00
Jędrzej Stuczyński 0f6f47c5ac Fixed overflow in determining reconnection backoff (#1260)
* Fixed overflow in determining connection backoff

* Updated changelog
2022-05-09 13:23:14 +01:00
fmtabbara b57c17e5af fix storybook 2022-05-09 10:34:14 +01:00
Drazen Urch c237b37ea6 Use saturating_sub for extra safety (#1258)
* Use saturating_sub for extra safety

* More saturating_sub

* Save some lines

* Add change log
2022-05-09 09:47:03 +02:00
fmtabbara f2fa221489 big tidy and some refactoring of high level component hierachy 2022-05-07 17:53:46 +01:00
Gala d7e82b075f Network Explorer: add economic dynamics (#1223)
* adding delegators number info on mixnode details

* add PM, Delegators and Avg. Uptime fields to the node list hardcoded

* make delegations number dynamic

* fixing bg color bug

* wip node info statistics

* adding basic tooltip new section and some ui

* tooltip customisation

* progress bar styles

* remove not used import

* fix info icons color

* remove discord icon

* Economic dynamics stats endpoint on the explorer API with dummy fixture data

* fetching economic-dynamics-stats

* Populating the endpoint with real data aggregated from validator api

* Introduced new cache functionalities

* using explorer-api data

* adding marging profit

* adding average update

* Update network-explorer.yml

* adding more info on mix nodes page

* display only part of wallet and node id

* typo

* remove log

* adding new values on node response and fix a typo

* remove delegators number column

* Endpoints for average mixnode uptime

* remove TODO

* Clippy

* some ui fixes for percentage linear progress

* GitHub Actions: build storybook for the Network Explorer and add to notification

* Fix file extension to `.ts`

* Fix up formatting and types

* Add storybook

* Add story for mix node details economics

* Fix unused warning

* adding percentage symbol on uptime in mix nodes

* Change eslint config

* some refactor

* progress bar story

* wip refactoring

* more refactor

* adding empty state to the story

* change default values for empty state

* refactor naming and progress bar contrast

* adding hardcoded selection chance and update the storybook

* adding selection chance stories

* adding the progress bar back

* tooltip button padding fix

* Endpoints for average mixnode uptime

* Fix unused warning

* Rustfmt

* moking selection chance response and new colors

* remove log

* fix camelCases issue

* remove hardcoded code

* remove avg_uptime at mixnodes table

* Add jsonchema to uptimeresponse struct

- add the route for avg_uptimes

* adding space between words

* update selection chance colours

* adding the 2 missing tooltips

* fix up uptimeresponse

* fix duplicate entry

* fmt

* validator-client: use statement

* explorer: PR requests

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Fouad <fmtabbara@hotmail.co.uk>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: tommy <tommyvez@protonmail.com>
2022-05-06 20:50:26 +01:00
fmtabbara 0e4787f078 minor refactors 2022-05-06 15:54:38 +01:00
Jędrzej Stuczyński f684664472 Removed an 'expect' in mixnet contract query (#1257)
* Removed an 'expect' in mixnet contract query

* Updated changelog
2022-05-06 14:28:57 +01:00
Jędrzej Stuczyński cd5fff92ad Added network information to --version (or --help) command (#1256)
* Added network information to --version (or --help) command

* Updated changelog
2022-05-06 10:46:17 +01:00
fmtabbara 9b0b961d43 content updates 2022-05-06 09:56:13 +01:00
Raphaël Walther 4b95e71adb Merge pull request #1254 from nymtech/cargo_manifest_path
Fixed nightly build mainifest path
2022-05-06 10:39:30 +02:00
Raphaël Walther cde5b66306 Fixed nightly build mainifest path 2022-05-06 09:44:55 +02:00
Jon Häggblad ac72e20447 wallet: additional test for decrypting stored wallet file 2022-05-06 09:41:36 +02:00
Jon Häggblad 571fd5cb93 wallet: removing an account in a login twice should fail 2022-05-06 09:01:48 +02:00
Jon Häggblad 7f3166d230 changelog: fix link 2022-05-05 17:06:27 +02:00
Jon Häggblad 68050d77df changelog: add note about swagger and validator-api 2022-05-05 16:44:38 +02:00
Jędrzej Stuczyński 943337db0d Running tests with the same opt-level in CI, but preserving debug assertions (#1253) 2022-05-05 15:13:04 +01:00
dependabot[bot] 8fbf84174d Bump ejs from 3.1.6 to 3.1.7 in /nym-wallet/webdriver (#1245)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:24 +01:00
dependabot[bot] d42a175289 Bump async from 2.6.3 to 2.6.4 (#1233)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:18 +01:00
dependabot[bot] ac01df0817 Bump async in /clients/native/examples/js-examples/websocket (#1232)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:12 +01:00
dependabot[bot] 7b27065608 Bump async from 2.6.3 to 2.6.4 in /clients/webassembly/js-example (#1231)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:07 +01:00
Jon Häggblad 0523ccdce8 workflow: add wallet rust ci job (#1251)
* workflow: add wallet rust ci job

* workflow: remove not used matrix file

* workflow: add wallet rust part to nightly build

* workflow: add nym-wallet to step names

* workflow: tweak wallet names

* workflow: run on ubuntu-latest

* workflow: skip sccache

* workflow: tweak name

* workflow: switch back to self-hosted tag
2022-05-05 13:58:07 +02:00
Jędrzej Stuczyński df8ae52d8c Added pull request template with changelog checklist 2022-05-05 11:21:25 +01:00
fmtabbara 7cfaf6fa1e Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-05 11:16:30 +01:00
fmtabbara 6e9eab4edb allow access to prev steps when creating account 2022-05-05 11:15:56 +01:00
Jon Häggblad 0812378fdd wallet: reset account state when adding and removing accounts 2022-05-05 12:13:31 +02:00
Jon Häggblad 188a7ec91d validator-api: reenable swagger openapi (#1249)
* validator-api: reenable swagger openapi

* rustfmt

* validator-api: rename openapi tag to status
2022-05-05 11:35:00 +02:00
Jon Häggblad c91bf3f8d1 wallet: fix rust unit tests (#1252) 2022-05-05 10:31:00 +02:00
fmtabbara ecb27e2cc2 import accounts wip 2022-05-04 22:36:09 +01:00
fmtabbara a1961dbc2f add accounts wip 2022-05-04 21:34:42 +01:00
fmtabbara ef4af0a1db display account errors 2022-05-04 20:49:19 +01:00
fmtabbara 5930ec1f18 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-04 20:46:09 +01:00
fmtabbara 72c7049fca set up loading page for account switch 2022-05-04 20:45:33 +01:00
Jon Häggblad 92cbe651de wallet: add account returns wallet entry 2022-05-04 20:14:01 +02:00
Jędrzej Stuczyński 777fcf8cb3 Release/nym wallet v1.0.4 (#1248)
* Bumped up version numbers to 1.0.1

* Updated changelog attempting to use new format

* Bumped up wallet version number

* Updated changelog attempting to use new format

* Updated tauri version
2022-05-04 17:03:08 +01:00
Jędrzej Stuczyński 945dda0c24 Release/1.0.1 (#1247)
* Bumped up version numbers to 1.0.1

* Updated changelog attempting to use new format
2022-05-04 16:57:36 +01:00
fmtabbara b44b074af7 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-04 16:22:31 +01:00
fmtabbara 7a0dff5f00 update main state to respond to account changes 2022-05-04 16:22:00 +01:00
fmtabbara 6685b129bb refactor to use accounts context 2022-05-04 16:21:29 +01:00
fmtabbara e0a80c777e stories broken until mock context added 2022-05-04 16:19:52 +01:00
Jędrzej Stuczyński 479d410d20 Feature/broadcast sync with polling (#1246)
* Broadcast tx in a sync mode and poll for its inclusion

* Adjusted internal type used in TauriTxResult

* Re-exported MsgSend

* Increased polling rate + removed print
2022-05-04 16:19:34 +01:00
fmtabbara ab019266cc derive color from name now address 2022-05-04 16:19:25 +01:00
fmtabbara 92fcae9a37 use context for accounts state 2022-05-04 16:18:56 +01:00
Jon Häggblad 413e2662ff wallet: rustfmt 2022-05-04 16:50:51 +02:00
Jon Häggblad e026a532dd wallet: store unlocked accounts in Vec to preserve order 2022-05-04 16:50:01 +02:00
Mark Sinclair a1ca330ce9 GitHub Actions: upload network requester to releases 2022-05-04 14:31:21 +01:00
Jon Häggblad 139e89643c Endpoints for average mixnode uptime (#1238) 2022-05-04 11:28:41 +02:00
fmtabbara a1e0087760 merge sprint branch updates£ 2022-05-04 10:01:39 +01:00
fmtabbara b15cc094ea small refactors 2022-05-04 10:00:17 +01:00
Jędrzej Stuczyński 8eb3f6f862 Changed opt-level for test code to speed up especially dkg tests 2022-05-04 09:54:18 +01:00
Jędrzej Stuczyński 9032d81d52 Fixed dkg benchmarking code to take into account resharing attributes 2022-05-04 09:44:10 +01:00
Jon Häggblad 7adee63ebe wallet: add show_mnemonic_for_account_in_password 2022-05-04 10:01:34 +02:00
Jon Häggblad 1a5580229b wallet: more unit tests 2022-05-04 01:35:57 +02:00
Jon Häggblad df4c6493d4 wallet: split and add more wallet_store tests 2022-05-03 23:20:16 +02:00
Jon Häggblad 30a41261ea wallet: add more log statements 2022-05-03 20:03:46 +02:00
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
Jon Häggblad 6d874cc34a wallet: remove list_accounts_for_password 2022-04-29 16:54:25 +02:00
Jon Häggblad 7e356ea3b3 wallet: simplify account handling 2022-04-29 16:49:57 +02:00
Jon Häggblad f7be9e7e6f wallet: remove DecryptedAccount and make Accounts clone 2022-04-29 16:27:24 +02:00
Jon Häggblad 05374393ef wallet: contructors for DecryptedAccount 2022-04-29 15:36:06 +02:00
fmtabbara 580656c002 refactor + wip 2022-04-29 14:12:47 +01:00
Jon Häggblad a7caf97b73 wallet: derive_address 2022-04-29 15:12:07 +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
fmtabbara 091507b6d8 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-04-29 11:10:21 +01:00
Jon Häggblad 71b5bc9e71 wallet: backend account switching 2022-04-29 11:48:07 +02:00
fmtabbara 8bbffb6a88 prevent bubbling 2022-04-29 10:45:43 +01:00
fmtabbara 31c4fc6807 update display mnemonic component 2022-04-29 10:45:23 +01:00
fmtabbara 66b9b13edc refactor and add more mocks 2022-04-29 10:05:18 +01:00
fmtabbara 1b945ae918 use account switch function 2022-04-27 13:43:39 +01:00
fmtabbara be9b83a87d set up account switching mechanism 2022-04-27 11:19:19 +01:00
Fouad ba1fb17908 Merge pull request #1209 from nymtech/feature/multi-address-wallet
Feature/multi address wallet
2022-04-26 20:51:50 +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
fmtabbara 5446874ebe align top bar with nav 2022-04-26 14:29:17 +01: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
fmtabbara afac630a77 add list accounts function 2022-04-25 11:27:07 +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
fmtabbara 3250d6982e refactor to separate logic from view 2022-04-14 14:31:55 +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
fmtabbara ecdbe1a6fb refactor 2022-04-13 18:08:57 +01:00
fmtabbara 6ba79ee924 use avatar component 2022-04-13 18:07:54 +01:00
fmtabbara 2bebb4b0c2 remove unused component 2022-04-13 18:07:31 +01:00
fmtabbara 81b7d49624 create avatar component 2022-04-13 18:07:06 +01:00
fmtabbara f118a0c854 move story files 2022-04-13 18:06:53 +01:00
Mark Sinclair 94be4c71a4 Update GitHub Action to build nym binaries 2022-04-13 16:22:35 +01:00
fmtabbara db6ecaaecb Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-address-wallet 2022-04-13 14:41:12 +01:00
fmtabbara bd13aa6f35 add new modals 2022-04-13 11:53:13 +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
fmtabbara 0cad7f635d new stories 2022-04-12 12:25:15 +01: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
Jon Häggblad a5e6032393 wallet: support multiple accounts per encrypted login (#1205)
* wallet: support multiple accounts per encrypted login

Rework wallet storage to allow grouping accounts under a single
encrypted entry, in a way that is backwards compatible.

* wallet: remove commented out lines
2022-04-12 11:53:38 +02:00
fmtabbara 4edc0700a1 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-address-wallet 2022-04-12 10:33:43 +01:00
fmtabbara 019a04c0fc code tidy and restructure 2022-04-12 10:33:16 +01: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
Fouad 2e7b8e911f Merge pull request #1201 from nymtech/feature/multi-address-wallet
Feature/multi address wallet
2022-04-08 13:13:09 +01:00
fmtabbara 50f4699c95 change button size 2022-04-07 22:04:19 +01:00
fmtabbara 3265df019a more multi account ui 2022-04-07 22:00:30 +01:00
fmtabbara 902721bda3 yarn lock updated 2022-04-07 16:28:57 +01:00
fmtabbara 585724fc79 more storybook work for multi accounts 2022-04-07 16:27:30 +01:00
fmtabbara c6578384a8 storybook ui for multi addresses 2022-04-07 14:01:58 +01:00
fmtabbara 3b245e16db more multi account UI work 2022-04-07 11:34:48 +01:00
fmtabbara 6d0e2cf491 start component work for multi accounts 2022-04-06 11:25:08 +01:00
408 changed files with 21776 additions and 2743 deletions
+9
View File
@@ -0,0 +1,9 @@
# Description
Closes: #XXXX
<!-- If appropriate, insert relevant description here -->
# Checklist:
- [ ] added a changelog entry to `CHANGELOG.md`
+72
View File
@@ -0,0 +1,72 @@
name: Continuous integration on dispatch
on: workflow_dispatch
jobs:
build:
runs-on: [ self-hosted, custom-linux-exoscale ]
# Enable sccache via environment variable
env:
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --all-features
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace -- -D warnings
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --features=coconut
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --features=coconut
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
with:
command: clippy
args: --features=coconut -- -D warnings
+14
View File
@@ -31,6 +31,8 @@ jobs:
# continue-on-error: true
- run: yarn && yarn build
continue-on-error: true
- run: yarn storybook:build
name: Build storybook
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -42,6 +44,17 @@ jobs:
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Deploy storybook to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "explorer/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
@@ -51,6 +64,7 @@ jobs:
NYM_PROJECT_NAME: "Network Explorer"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
+33
View File
@@ -94,12 +94,45 @@ 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' }}
with:
command: clippy
args: --workspace --all-targets --features=coconut -- -D warnings
# nym-wallet (the rust part)
- name: Build nym-wallet rust code
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Run nym-wallet tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Check nym-wallet formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
- name: Run clippy for nym-wallet
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings
notification:
needs: build
runs-on: ubuntu-latest
@@ -2,6 +2,9 @@ name: Publish Nym binaries
on:
release:
types: [created]
env:
NETWORK: mainnet
jobs:
publish-nym:
@@ -41,3 +44,4 @@ jobs:
target/release/nym-mixnode
target/release/nym-socks5-client
target/release/nym-validator-api
target/release/nym-network-requester
@@ -1,5 +1,6 @@
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION_STORYBOOK }}.{{ env.NYM_CI_WWW_BASE }}
> ✅ **SUCCESS**
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+57
View File
@@ -0,0 +1,57 @@
name: Nym Wallet (rust)
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
build:
runs-on: [ self-hosted, custom-linux-exoscale ]
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features -- -D warnings
+334
View File
@@ -1,5 +1,338 @@
# Changelog
## [Unreleased]
### Added
- wallet: require password to switch accounts
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
- wallet: added support for multiple accounts ([#1265])
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
- validator-api: add Swagger to document the REST API ([#1249]).
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
- network-requester: send traffic statistics from all network requesters and receive it in a special network-requester that aggregates the data and exposes it via a rest API ([#1267], [#1278]).
### Fixed
- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275])
- mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257])
- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258])
- mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260])
[#1258]: https://github.com/nymtech/nym/pull/1258
[#1249]: https://github.com/nymtech/nym/pull/1249
[#1256]: https://github.com/nymtech/nym/pull/1256
[#1257]: https://github.com/nymtech/nym/pull/1257
[#1260]: https://github.com/nymtech/nym/pull/1260
[#1265]: https://github.com/nymtech/nym/pull/1265
[#1267]: https://github.com/nymtech/nym/pull/1267
[#1275]: https://github.com/nymtech/nym/pull/1275
[#1278]: https://github.com/nymtech/nym/pull/1278
## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04)
### Changed
- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246])
## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04)
### Added
- validator-api: introduced endpoint for getting average mixnode uptime ([#1238])
### Changed
- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246])
### Fixed
- nym-network-requester: is included in the Github Actions for building release binaries
[#1238]: https://github.com/nymtech/nym/pull/1238
[#1246]: https://github.com/nymtech/nym/pull/1246
## [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 +391,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
+271 -156
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -9,6 +9,10 @@ overflow-checks = true
[profile.dev]
panic = "abort"
[profile.test]
# equivalent of running in `--release` (but since we're in test profile we're keeping overflow checks and all of those by default)
opt-level = 3
[workspace]
resolver = "2"
@@ -18,14 +22,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",
+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.1"
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" }
File diff suppressed because one or more lines are too long
+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.1"
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]
@@ -592,9 +592,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -4825,9 +4825,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
+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
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
pub mod commands;
@@ -67,6 +68,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -84,6 +86,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.0.0-rc.1"
version = "1.0.1"
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)
+4
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
mod commands;
@@ -67,6 +68,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -84,6 +86,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+5 -3
View File
@@ -16,7 +16,7 @@ use proxy_helpers::connection_controller::{
};
use proxy_helpers::proxy_runner::ProxyRunner;
use rand::RngCore;
use socks5_requests::{ConnectionId, RemoteAddress, Request};
use socks5_requests::{ConnectionId, Message, RemoteAddress, Request};
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
@@ -224,8 +224,9 @@ impl SocksClient {
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
let msg = Message::Request(req);
let input_message = InputMessage::new_fresh(self.service_provider, req.into_bytes(), false);
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
self.input_sender.unbounded_send(input_message).unwrap();
}
@@ -252,7 +253,8 @@ impl SocksClient {
)
.run(move |conn_id, read_data, socket_closed| {
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
InputMessage::new_fresh(recipient, provider_request.into_bytes(), false)
let provider_message = Message::Request(provider_request);
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
})
.await
.into_inner();
+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.1"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
+7 -8
View File
@@ -24,8 +24,7 @@
},
"../pkg": {
"name": "@nymproject/nym-client-wasm",
"version": "0.12.0",
"license": "Apache-2.0"
"version": "0.0.1"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
@@ -586,9 +585,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -4305,9 +4304,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
+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)
}
}
+48 -12
View File
@@ -133,20 +133,14 @@ impl Client {
if current_attempt == 0 {
None
} else {
// according to https://github.com/tokio-rs/tokio/issues/1953 there's an undocumented
// limit of tokio delay of about 2 years.
// let's ensure our delay is always on a sane side of being maximum 1 hour.
let maximum_sane_delay = Duration::from_secs(60 * 60);
let exp = 2_u32.checked_pow(current_attempt);
let backoff = exp
.and_then(|exp| self.config.initial_reconnection_backoff.checked_mul(exp))
.unwrap_or(self.config.maximum_reconnection_backoff);
Some(std::cmp::min(
maximum_sane_delay,
std::cmp::min(
self.config
.initial_reconnection_backoff
.checked_mul(2_u32.pow(current_attempt))
.unwrap_or(self.config.maximum_reconnection_backoff),
self.config.maximum_reconnection_backoff,
),
backoff,
self.config.maximum_reconnection_backoff,
))
}
}
@@ -254,3 +248,45 @@ impl SendWithoutResponse for Client {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_client() -> Client {
Client::new(Config {
initial_reconnection_backoff: Duration::from_millis(10_000),
maximum_reconnection_backoff: Duration::from_millis(300_000),
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
})
}
#[test]
fn determining_backoff_works_regardless_of_attempt() {
let client = dummy_client();
assert!(client.determine_backoff(0).is_none());
assert!(client.determine_backoff(1).is_some());
assert!(client.determine_backoff(2).is_some());
assert_eq!(
client.determine_backoff(16).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(32).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(1024).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(65536).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(u32::MAX).unwrap(),
client.config.maximum_reconnection_backoff
);
}
}
@@ -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"
@@ -5,6 +5,9 @@ use crate::{validator_api, ValidatorClientError};
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond};
use url::Url;
#[cfg(feature = "nymd-client")]
use validator_api_requests::models::UptimeResponse;
use validator_api_requests::models::{
CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
StakeSaturationResponse,
@@ -153,6 +156,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 +267,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 +282,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?)
}
@@ -576,6 +585,12 @@ impl<C> Client<C> {
Ok(delegations)
}
pub async fn get_mixnode_avg_uptimes(
&self,
) -> Result<Vec<UptimeResponse>, ValidatorClientError> {
Ok(self.validator_api.get_mixnode_avg_uptimes().await?)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -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,
@@ -32,12 +30,26 @@ use cosmwasm_std::Coin as CosmWasmCoin;
use prost::Message;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::time::Duration;
#[async_trait]
impl CosmWasmClient for HttpClient {}
impl CosmWasmClient for HttpClient {
fn broadcast_polling_rate(&self) -> Duration {
Duration::from_secs(4)
}
fn broadcast_timeout(&self) -> Duration {
Duration::from_secs(60)
}
}
#[async_trait]
pub trait CosmWasmClient: rpc::Client {
// this should probably get redesigned, but I'm leaving those like that temporarily to fix
// the underlying issue more quickly
fn broadcast_polling_rate(&self) -> Duration;
fn broadcast_timeout(&self) -> Duration;
// helper method to remove duplicate code involved in making abci requests with protobuf messages
// TODO: perhaps it should have an additional argument to determine whether the response should
// require proof?
@@ -83,21 +95,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,
@@ -260,6 +267,42 @@ pub trait CosmWasmClient: rpc::Client {
Ok(rpc::Client::broadcast_tx_commit(self, tx).await?)
}
async fn broadcast_tx(&self, tx: Transaction) -> Result<TxResponse, NymdError> {
let broadcasted = CosmWasmClient::broadcast_tx_sync(self, tx).await?;
if broadcasted.code.is_err() {
let code_val = broadcasted.code.value();
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: broadcasted.hash,
height: None,
code: code_val,
raw_log: broadcasted.log.to_string(),
});
}
let tx_hash = broadcasted.hash;
let start = tokio::time::Instant::now();
loop {
log::debug!(
"Polling for result of including {} in a block...",
broadcasted.hash
);
if tokio::time::Instant::now().duration_since(start) >= self.broadcast_timeout() {
return Err(NymdError::BroadcastTimeout {
hash: tx_hash,
timeout: self.broadcast_timeout(),
});
}
if let Ok(poll_res) = self.get_tx(tx_hash).await {
return Ok(poll_res);
}
tokio::time::sleep(self.broadcast_polling_rate()).await;
}
}
async fn get_codes(&self) -> Result<Vec<Code>, NymdError> {
let path = Some("/cosmwasm.wasm.v1.Query/Codes".parse().unwrap());
@@ -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;
@@ -17,7 +19,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
if self.check_tx.code.is_err() {
return Err(NymdError::BroadcastTxErrorCheckTx {
hash: self.hash,
height: self.height,
height: Some(self.height),
code: self.check_tx.code.value(),
raw_log: self.check_tx.log.value().to_owned(),
});
@@ -26,7 +28,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
if self.deliver_tx.code.is_err() {
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: self.hash,
height: self.height,
height: Some(self.height),
code: self.deliver_tx.code.value(),
raw_log: self.deliver_tx.log.value().to_owned(),
});
@@ -36,6 +38,21 @@ impl CheckResponse for broadcast::tx_commit::Response {
}
}
impl CheckResponse for crate::nymd::TxResponse {
fn check_response(self) -> Result<Self, NymdError> {
if self.tx_result.code.is_err() {
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: self.hash,
height: Some(self.height),
code: self.tx_result.code.value(),
raw_log: self.tx_result.log.value().to_owned(),
});
}
Ok(self)
}
}
pub(crate) fn compress_wasm_code(code: &[u8]) -> Result<Vec<u8>, NymdError> {
// using compression level 9, same as cosmjs, that optimises for size
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
@@ -65,3 +82,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()
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use std::convert::TryInto;
use std::time::Duration;
use async_trait::async_trait;
use cosmrs::bank::MsgSend;
@@ -24,7 +25,7 @@ use crate::nymd::cosmwasm_client::types::*;
use crate::nymd::error::NymdError;
use crate::nymd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER};
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use crate::nymd::{CosmosCoin, GasPrice};
use crate::nymd::{CosmosCoin, GasPrice, TxResponse};
// we need to have **a** valid secp256k1 signature for simulation purposes.
// it doesn't matter what it is as long as it parses correctly
@@ -35,6 +36,9 @@ const DUMMY_SECP256K1_SIGNATURE: &[u8] = &[
91,
];
const DEFAULT_BROADCAST_POLLING_RATE: Duration = Duration::from_secs(4);
const DEFAULT_BROADCAST_TIMEOUT: Duration = Duration::from_secs(60);
#[async_trait]
pub trait SigningCosmWasmClient: CosmWasmClient {
fn signer(&self) -> &DirectSecp256k1HdWallet;
@@ -111,12 +115,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgStoreCode".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![upload_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![upload_msg], fee, memo)
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
// TODO: should those strings be extracted into some constants?
// the reason I think unwrap here is fine is that if the transaction succeeded and those
@@ -172,12 +176,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgInstantiateContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![init_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![init_msg], fee, memo)
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
// TODO: should those strings be extracted into some constants?
// the reason I think unwrap here is fine is that if the transaction succeeded and those
@@ -214,14 +218,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgUpdateAdmin".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -242,14 +246,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgClearAdmin".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -277,14 +281,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgMigrateContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![migrate_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![migrate_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(MigrateResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -312,14 +316,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![execute_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![execute_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -352,14 +357,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.collect::<Result<_, _>>()?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, messages, fee, memo)
.sign_and_broadcast(sender_address, messages, fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -372,7 +378,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Vec<Coin>,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let send_msg = MsgSend {
from_address: sender_address.clone(),
to_address: recipient_address.clone(),
@@ -381,7 +387,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
self.sign_and_broadcast_commit(sender_address, vec![send_msg], fee, memo)
self.sign_and_broadcast(sender_address, vec![send_msg], fee, memo)
.await?
.check_response()
}
@@ -392,7 +398,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
msgs: I,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
I: IntoIterator<Item = (AccountId, Vec<Coin>)> + Send,
{
@@ -409,7 +415,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
})
.collect::<Result<_, _>>()?;
self.sign_and_broadcast_commit(sender_address, messages, fee, memo)
self.sign_and_broadcast(sender_address, messages, fee, memo)
.await?
.check_response()
}
@@ -421,7 +427,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Coin,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let delegate_msg = MsgDelegate {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -430,7 +436,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![delegate_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![delegate_msg], fee, memo)
.await?
.check_response()
}
@@ -442,7 +448,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Coin,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let undelegate_msg = MsgUndelegate {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -451,7 +457,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![undelegate_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![undelegate_msg], fee, memo)
.await?
.check_response()
}
@@ -462,7 +468,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
validator_address: &AccountId,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let withdraw_msg = MsgWithdrawDelegatorReward {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -470,7 +476,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgWithdrawDelegatorReward".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![withdraw_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![withdraw_msg], fee, memo)
.await?
.check_response()
}
@@ -573,6 +579,27 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
CosmWasmClient::broadcast_tx_commit(self, tx_bytes.into()).await
}
/// Broadcast a transaction to the network and monitors its inclusion in a block.
async fn sign_and_broadcast(
&self,
signer_address: &AccountId,
messages: Vec<Any>,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<TxResponse, NymdError> {
let memo = memo.into();
let fee = self
.determine_transaction_fee(signer_address, &messages, fee, &memo)
.await?;
let tx_raw = self.sign(signer_address, messages, fee, memo).await?;
let tx_bytes = tx_raw
.to_bytes()
.map_err(|_| NymdError::SerializationError("Tx".to_owned()))?;
self.broadcast_tx(tx_bytes.into()).await
}
fn sign_direct(
&self,
signer_address: &AccountId,
@@ -638,6 +665,9 @@ pub struct Client {
rpc_client: HttpClient,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
broadcast_polling_rate: Duration,
broadcast_timeout: Duration,
}
impl Client {
@@ -654,8 +684,18 @@ impl Client {
rpc_client,
signer,
gas_price,
broadcast_polling_rate: DEFAULT_BROADCAST_POLLING_RATE,
broadcast_timeout: DEFAULT_BROADCAST_TIMEOUT,
})
}
pub fn set_broadcast_polling_rate(&mut self, broadcast_polling_rate: Duration) {
self.broadcast_polling_rate = broadcast_polling_rate
}
pub fn set_broadcast_timeout(&mut self, broadcast_timeout: Duration) {
self.broadcast_timeout = broadcast_timeout
}
}
#[async_trait]
@@ -669,7 +709,15 @@ impl rpc::Client for Client {
}
#[async_trait]
impl CosmWasmClient for Client {}
impl CosmWasmClient for Client {
fn broadcast_polling_rate(&self) -> Duration {
self.broadcast_polling_rate
}
fn broadcast_timeout(&self) -> Duration {
self.broadcast_timeout
}
}
#[async_trait]
impl SigningCosmWasmClient for Client {
@@ -1,23 +1,35 @@
// 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::Data;
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 +44,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 +56,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 +71,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 +80,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,
@@ -403,6 +673,8 @@ pub struct MigrateResult {
pub struct ExecuteResult {
pub logs: Vec<Log>,
pub data: Data,
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
@@ -5,6 +5,7 @@ use crate::nymd::cosmwasm_client::types::ContractCodeId;
use cosmrs::tendermint::{abci, block};
use cosmrs::{bip32, tx, AccountId};
use std::io;
use std::time::Duration;
use thiserror::Error;
pub use cosmrs::rpc::error::{
@@ -81,21 +82,21 @@ pub enum NymdError {
MalformedLogString,
#[error(
"Error when broadcasting tx {hash} at height {height}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorCheckTx {
hash: tx::Hash,
height: block::Height,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
#[error(
"Error when broadcasting tx {hash} at height {height}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorDeliverTx {
hash: tx::Hash,
height: block::Height,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
@@ -108,6 +109,18 @@ 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,
#[error("Transaction with ID {hash} has been submitted but not yet found on the chain. You might want to check for it later. There was a total wait of {} seconds", .timeout.as_secs())]
BroadcastTimeout { hash: tx::Hash, timeout: Duration },
}
impl NymdError {
@@ -3,12 +3,11 @@
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;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::Error as TendermintRpcError;
use cosmrs::rpc::HttpClientUrl;
use cosmwasm_std::{Coin, Uint128};
@@ -23,12 +22,14 @@ use mixnet_contract_common::{
PagedRewardedSetResponse, QueryMsg, RewardedSetUpdateDetails,
};
use serde::Serialize;
use std::collections::HashMap;
use std::convert::TryInto;
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
pub use crate::nymd::fee::Fee;
use crate::nymd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER;
pub use cosmrs::bank::MsgSend;
pub use cosmrs::rpc::endpoint::tx::Response as TxResponse;
pub use cosmrs::rpc::endpoint::validators::Response as ValidatorResponse;
pub use cosmrs::rpc::HttpClient as QueryNymdClient;
@@ -43,7 +44,6 @@ pub use cosmrs::tx::{self, Gas};
pub use cosmrs::Coin as CosmosCoin;
pub use cosmrs::{AccountId, Decimal, Denom};
pub use signing_client::Client as SigningNymdClient;
use std::collections::HashMap;
pub use traits::{VestingQueryClient, VestingSigningClient};
pub mod cosmwasm_client;
@@ -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
@@ -620,7 +640,7 @@ impl<C> NymdClient<C> {
recipient: &AccountId,
amount: Vec<CosmosCoin>,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
@@ -635,7 +655,7 @@ impl<C> NymdClient<C> {
&self,
msgs: Vec<(AccountId, Vec<CosmosCoin>)>,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
@@ -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()
)
}
}
}
}
@@ -10,7 +10,7 @@ use std::collections::HashMap;
use url::Url;
use validator_api_requests::models::{
CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
};
pub mod error;
@@ -253,6 +253,36 @@ impl Client {
.await
}
pub async fn get_mixnode_avg_uptime(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<UptimeResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
identity,
routes::AVG_UPTIME,
],
NO_PARAMS,
)
.await
}
pub async fn get_mixnode_avg_uptimes(&self) -> Result<Vec<UptimeResponse>, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODES,
routes::AVG_UPTIME,
],
NO_PARAMS,
)
.await
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -26,5 +26,6 @@ pub const SINCE_ARG: &str = "since";
pub const STATUS: &str = "status";
pub const REWARD_ESTIMATION: &str = "reward-estimation";
pub const AVG_UPTIME: &str = "avg_uptime";
pub const STAKE_SATURATION: &str = "stake-saturation";
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
+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 {
+1
View File
@@ -7,6 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cfg-if = "1.0.0"
handlebars = "3.0.1"
humantime-serde = "1.0"
log = "0.4"
+17 -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,21 @@ 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()));
cfg_if::cfg_if! {
if #[cfg(unix)] {
fs::write(location.clone(), templated_config)?;
let mut perms = fs::metadata(location.clone())?.permissions();
perms.set_mode(0o600);
fs::set_permissions(location, perms)?;
} else {
fs::write(location, templated_config)?;
}
}
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()
}
@@ -47,20 +47,19 @@ impl NodeEpochRewards {
pub fn node_profit(&self) -> U128 {
let reward = U128::from_num(self.reward().u128());
if reward < self.operator_cost() {
U128::from_num(0u128)
} else {
reward - self.operator_cost()
}
// if operating cost is higher then the reward node profit is 0
reward.saturating_sub(self.operator_cost())
}
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 +81,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 +206,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),
+3
View File
@@ -19,6 +19,8 @@ cipher = { version = "0.4.3", optional = true }
x25519-dalek = { version = "1.1", optional = true }
ed25519-dalek = { version = "1.0", optional = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true }
serde_bytes = { version = "0.11.6", optional = true }
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
# internal
@@ -30,6 +32,7 @@ config = { path="../../common/config" }
rand_chacha = "0.2"
[features]
serde = ["serde_crate", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
symmetric = ["aes", "ctr", "cipher", "generic-array"]
+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
+547
View File
@@ -0,0 +1,547 @@
// 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,
None,
)
})
})
});
}
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,
None,
);
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, None)
.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,
None,
)
})
})
},
);
}
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,
None,
);
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, None)
.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,
None,
)
})
})
},
);
}
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,
None,
);
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, None)
.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)

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