Compare commits

...

148 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 60dc125735 Docker image for gateway 2021-12-15 16:11:05 +00:00
Jędrzej Stuczyński e6259c184c Mixnode dockerfile 2021-12-15 15:02:00 +00:00
Bogdan-Ștefan Neacşu c93f3cfc4f Fix topology log (#962) 2021-12-15 13:19:34 +02:00
Mark Sinclair 87d18bbc16 Merge pull request #960 from nymtech/feature/network-explorer-ui-config
Network Explorer: configure URLs with `.env` file
2021-12-15 10:37:47 +00:00
Mark Sinclair 45ed46afa0 Network Explorer: add prod config 2021-12-15 10:14:41 +00:00
Mark Sinclair 9619e794b2 Network Explorer: configure URLs with .env file 2021-12-14 16:06:02 +00:00
Drazen Urch 305864aa0f Feature/vesting to wallet (#954)
* Better code structure, proper error handling, VestingSigningClient for wallet

* Add vesting contract queries to wallet

* Address review comments

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
2021-12-14 12:25:48 +01:00
Jędrzej Stuczyński 8f152d42f0 Feature/simulate (#950)
* Raw scaffold for tx simulate

* Proper error handling in AbciResult parsing

* Simulate without actual signing operation

* Moved all-fee related functionalities to separate module

* Adding GasInfo to transaction results

* Automated gas estimation

* Slightly adjusted public API

* Using auto fees in eth events

* Removed old print statement

* Reorganised nymd client fee handling

* Put bandaid on wallet gas estimation

* Fixed operation re-export

* warning note on get_approximate_fee

* [ci skip] Generate TS types

* Refactored ProtoAbciResult parsing

* Explicit error on abci query failure

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-12-14 09:28:15 +00:00
Drazen Urch 791d051537 Different workshare calculations for rewarded vs active set (#951)
* Add Makefile to make running all checks easier locally

* Different workshare calculation for active vs rewarded set

* Rework omega calculation, update tests

* Remove ZERO const

* unym -> DENOM

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
2021-12-13 19:11:54 +01:00
Jędrzej Stuczyński f8099cb8c8 Bugfix/rewarding fixes (#953)
* Improved rewarding-related log messages

* Skipping empty mixnode chunks
2021-12-13 15:43:02 +00:00
Tommy Verrall f1e995b076 Merge pull request #947 from nymtech/feature/wallet-ui-updates-round-2
Desktop Wallet UI Updates
2021-12-13 11:11:30 +00:00
Bogdan-Ștefan Neacşu ed1fe22db2 Check the response for multiple sends (#955) 2021-12-10 09:22:37 +01:00
dependabot[bot] b1c45dc0f8 Bump next from 11.1.1 to 11.1.3 in /wallet-web (#952)
Bumps [next](https://github.com/vercel/next.js) from 11.1.1 to 11.1.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v11.1.1...v11.1.3)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-09 09:25:25 +00:00
Drazen Urch 0e0151f781 Feature/profit margin percent config (#949)
* Profit margin percent to config, fix vesting tests

* Add bonding bounds check

* Fix contract test
2021-12-08 20:17:31 +01:00
Drazen Urch 70e148b3ed Run CI for all contracts in one workflow (#948)
* Run CI for all contracts in one workflow

* Rename

* Fix vesting tests
2021-12-08 16:15:18 +01:00
fmtabbara 53138d6292 link to transaction hash 2021-12-07 22:39:13 +00:00
fmtabbara 39650de2bd style update 2021-12-07 21:46:02 +00:00
fmtabbara 7b04093cc5 style updates 2021-12-07 21:24:16 +00:00
fmtabbara a4c9a81399 refresh balance on balance page visit 2021-12-07 21:06:29 +00:00
Drazen Urch f42f76901a Add VestingExecute and VestingQuery client traits (#944)
* Add VestingExecute and VestingQuery client traits

* cover ed25519 verification

* cargo fmt

* `Unix` newline-style

* Remove newline force

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
2021-12-07 19:56:10 +01:00
fmtabbara 875bcb4e63 update bonding success UI 2021-12-07 13:42:23 +00:00
fmtabbara fa62de5bc6 undelegate page update 2021-12-07 13:17:46 +00:00
fmtabbara 5c3b08e1cd update delegate page 2021-12-07 12:36:21 +00:00
Tommy Verrall 745af89019 Merge pull request #946 from nymtech/feature/docker-updates
Docker updates
2021-12-07 11:53:37 +00:00
Tommy Verrall eb0fb90127 update arg for commit hash 2021-12-07 11:45:46 +00:00
Tommy Verrall 527d5a5d9b update 2021-12-07 11:25:03 +00:00
Tommy Verrall 2dfa0a9d2a update 2021-12-07 11:23:44 +00:00
Tommy Verrall d9bfa4562e Update docker-files
- updated the network explorer
- updated new vesting contract
- updated the version of comos-sdk to be used
2021-12-07 11:22:57 +00:00
Jędrzej Stuczyński aec0239d87 Removed reliance on cosmrs fork (#943)
* Removed reliance on cosmrs fork

* Removed the accidental optional flag on cosmrs import in the wallet
2021-12-07 11:04:32 +00:00
Jędrzej Stuczyński a2324f98f8 Feature/identity verification (#930)
* Base58 representation of ed25519 signature

* Helper for verifying ed25519 signature on sender address

* Signature verification for gateway bonding

* Signature verification for mixnode bonding

* Added owner signatures for bonding in vesting contract

* Fixed choosing mixnode layer test

* Added owner signature fields to nymd client for bonding

* 'Updated' tauri wallet with new bond requirements

* Mixnode sign command with extra address validation

* Sign command for the gateway

* Signing own gateway's address derived with known mnemonic in not(coconut) case

* Fixed imports post-merge
2021-12-07 10:28:44 +00:00
fmtabbara 001d166477 update bond page 2021-12-07 10:23:37 +00:00
fmtabbara bb14e95e61 update send page 2021-12-06 17:32:29 +00:00
Jędrzej Stuczyński 5aa1c29409 Feature/terminology update (#941)
* Corrected used bond/pledge terminology

* ibid for the wallet and explorer

* [ci skip] Generate TS types

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-12-06 16:30:55 +00:00
Bogdan-Ștefan Neacşu db111490b6 Check the response for other transactions as well (#937) 2021-12-06 17:21:11 +01:00
Jędrzej Stuczyński fe0bb007c9 Don't reset total delegation on mixnode rebond (#940) 2021-12-06 17:20:41 +01:00
Tommy Verrall ddad6d73db Merge pull request #934 from nymtech/feature/client_on_behalf
Feature/client on behalf
2021-12-06 15:33:24 +00:00
fmtabbara 35c04014e5 undo hard-coded data 2021-12-06 15:32:09 +00:00
fmtabbara 138daddfed add custom nav icons 2021-12-06 15:21:34 +00:00
fmtabbara ab3cfe79bc use svgs as React components with svgr 2021-12-06 12:57:46 +00:00
fmtabbara 9fcf3105e0 balance page updates 2021-12-06 10:41:41 +00:00
fmtabbara 51bf117007 fix memory leak 2021-12-06 10:24:44 +00:00
Jędrzej Stuczyński a04d4503b5 Feature/pre cosmrs updates (#935)
* Updated nymd client

* Fixed contract upload and initialisation

* Increased default contract upload fee

* [ci skip] Generate TS types

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-12-06 09:25:53 +00:00
neacsu 9fb44f9672 [ci skip] Generate TS types 2021-12-03 18:16:55 +00:00
Bogdan-Ștefan Neacșu f542c28e89 Batching for sends 2021-12-03 19:05:21 +01:00
Bogdan-Ștefan Neacșu b809eb6c5f Batch delegations on behalf as well 2021-12-03 16:06:23 +01:00
Bogdan-Ștefan Neacșu 51e82fe930 Add gateway bond on behalf functions 2021-12-03 15:25:47 +01:00
Bogdan-Ștefan Neacșu 67f847e674 Add batched delegated mixnode bonds 2021-12-03 15:02:39 +01:00
Bogdan-Ștefan Neacșu d4736bac27 Fix gas for operations 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu 6417feaaed Fix clippy 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu 718170c651 Update tauri client 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu c10038e688 Add client-side Operations as well, in case fees need to be modified 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu 2f68439916 Unbond/undelegate mixnode with rust client 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu b175480ba5 Bond/delegate mixnode with rust client 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu 34c9726ac9 Add vesting contract address to validator client library 2021-12-03 15:02:19 +01:00
Bogdan-Ștefan Neacșu 11b2a7ad3d Rename contract_address to mixnet_contract_address 2021-12-03 15:02:19 +01:00
Drazen Urch 18978c7599 Merge branch 'develop' of https://github.com/nymtech/nym into develop 2021-12-03 10:14:59 +01:00
Drazen Urch 8e52a70685 Update vesting image 2021-12-03 10:14:47 +01:00
Drazen Urch 1f42ce57e3 Allow proxy gateway bonding (#936)
* Allow proxy gateway bonding

* Update msg text

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
2021-12-02 18:16:26 +01:00
Dave Hrycyszyn 231fba34bc Spelling fix in variable name 2021-12-02 13:15:55 +00:00
Dave Hrycyszyn 0e05fc46c9 Merge remote-tracking branch 'origin/develop' into develop 2021-12-02 13:13:24 +00:00
Dave Hrycyszyn 52777efc53 Tiny spellcheck fix in comment 2021-12-02 13:13:03 +00:00
Jędrzej Stuczyński 54bc198885 Feature/mixnet contract further adjustments (#928)
* Upgraded code to be cosmwasm 1.0-beta.2 compatible (#923)

* Upgraded code to be cosmwasm 1.0-beta.2 compatible

* [ci skip] Generate TS types

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>

* Feature/cosmwasm plus storage (#924)

* Upgraded code to be cosmwasm 1.0-beta.2 compatible

* Added cw-storage-plus dependency

* Experimentally replaced storage for config and layers with cw plus Item

* The same for main mixnode storage

* Usingn IndexedMap for mixnodes

* Split delegations from mixnodes into separate module

* MixnodeIndex on Addr directly

* Moved namespace values to constants

* Outdated comment

* [ci skip] Generate TS types

* Removed redundant identity index on mixnodes

* IndexMap for gateways storage

* Moved total delegation into a Map

* Compiling contract code after delegation storage upgrades

Tests dont compile yet and neither, I would assume, the client code

* Delegation type cleanup

* Client fixes

* Migrated delegation tests + fixed them

* Moved Rewarding Status to rewards

* Reward pool

* Rewarding status migrated

* Made clippy happier

* Added explorer API to default workspace members

* Updated delegation types in explorer-api

* Fixed tauri wallet

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>

* Missing license notices

* Dead code removal

* Changed RewardMixnodeV2 to RewardMixnode

* Adjusted module visibility

* Setting rewarding validator address in init msg

* ContractSettings => ContractState

* Transaction-related cleanup

* Changed ownership queries to return full bond information instead of just a bool

* Function for updating post rewarding storage

* Changed the order of arguments in decrementing reward pool

* Helpers for updating storage after rewarding

* Removed redundant turbofish

* [ci skip] Generate TS types

* Changed bond/delegation validation

* Made clippy happier

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-12-02 12:16:16 +00:00
Tommy Verrall 577c5dc1ee Merge pull request #933 from nymtech/test/webpack-wallet
Webpack wallet prod configuration
2021-12-02 10:16:51 +00:00
Dave Hrycyszyn 7927846390 Merge branch 'develop' of github.com:nymtech/nym into develop 2021-12-02 10:12:09 +00:00
Dave Hrycyszyn e41f02ad0d Tiny readme fixup 2021-12-02 10:12:01 +00:00
tommyv1987 e57a5b73a2 [ci skip] Generate TS types 2021-12-02 09:54:58 +00:00
Tommy Verrall 608de11377 Merge pull request #932 from nymtech/feature/add-tx-hash-to-wallet-result
Adding tx_hash to wallet response
2021-12-02 09:44:54 +00:00
futurechimp b88153a2bd [ci skip] Generate TS types 2021-12-01 18:30:05 +00:00
Tommy Verrall 0fd2f946dc dev 2021-12-01 18:23:46 +00:00
Dave Hrycyszyn c5cdbd5bfe Params formatting 2021-12-01 18:23:11 +00:00
Tommy Verrall 3c05cf29e6 fix duplication 2021-12-01 18:22:07 +00:00
futurechimp 6cb58d1f1c Adding tx_hash to wallet response 2021-12-01 18:19:14 +00:00
Tommy Verrall 5727ac5161 minor changes, due to windows complaining on builds. 2021-12-01 18:00:47 +00:00
Tommy Verrall 804f254e16 Compiler complains...
The compiler complains, however, these references are needed to enabled menu actions on mac builds.
2021-12-01 17:30:54 +00:00
Tommy Verrall f03ce6e07b fix typo 2021-12-01 17:11:23 +00:00
Tommy Verrall 2631ed4d0c Add prod config for the wallet
When supplying `production` as the mode in the config for webpack, it complains about an unmet dependency issue with fav-icons. Then trying to supply a favicon.ico, returns a mimetype error. By using a png in alignment to the .ico. It builds. Tested on Linux.
2021-12-01 17:06:47 +00:00
Tommy Verrall 0f93dde8fc Merge pull request #925 from nymtech/feature/ui-enhancements
Feature/UI enhancements for Desktop Wallet
2021-12-01 14:47:21 +00:00
Drazen Urch eb93b428cf Release/1.0.0 pre1 (#931)
* Upgraded code to be cosmwasm 1.0-beta.2 compatible (#923)

* Upgraded code to be cosmwasm 1.0-beta.2 compatible

* [ci skip] Generate TS types

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>

* Feature/cosmwasm plus storage (#924)

* Upgraded code to be cosmwasm 1.0-beta.2 compatible

* Added cw-storage-plus dependency

* Experimentally replaced storage for config and layers with cw plus Item

* The same for main mixnode storage

* Usingn IndexedMap for mixnodes

* Split delegations from mixnodes into separate module

* MixnodeIndex on Addr directly

* Moved namespace values to constants

* Outdated comment

* [ci skip] Generate TS types

* Removed redundant identity index on mixnodes

* IndexMap for gateways storage

* Moved total delegation into a Map

* Compiling contract code after delegation storage upgrades

Tests dont compile yet and neither, I would assume, the client code

* Delegation type cleanup

* Client fixes

* Migrated delegation tests + fixed them

* Moved Rewarding Status to rewards

* Reward pool

* Rewarding status migrated

* Made clippy happier

* Added explorer API to default workspace members

* Updated delegation types in explorer-api

* Fixed tauri wallet

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>

* Vesting contract (#900)

* Initial interface spec

* .gitignore

* Finalize implementation

* Correct assumptions, use wasm_execute

* Cleanup

* Track delegation balance

* Add delegation flow img

* Proper messaging from the vesting side

* Add proxy_address to RawDelegationData

* Wrap up (un)delegation

* Add proxy: Addr to MixNodeBond

* Stub in bonding/unbonding

* Migrate vesting to cosmwasm 1.0

* Rebase on top of 1.0.0-pre1

* Reimplement delegations tracking with a Map

* Migrate to cw-storage-plus

* Restructure code, add tests

* Streamline contract code, as per review

* Address review comments

* Pre-merge rebase

* Few more nits

* Few more nits

* Fix test

* cargo fmt

* Fix beta CI

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
2021-12-01 15:42:34 +01:00
fmtabbara 04b6f83d99 pr updates 2021-12-01 12:35:41 +00:00
fmtabbara f5612cc64b update large button height 2021-11-30 13:39:51 +00:00
fmtabbara d525563a46 link to block explorer 2021-11-30 13:11:51 +00:00
fmtabbara 560b306d32 update admin nav item style 2021-11-30 10:08:29 +00:00
fmtabbara a4a4da1121 style updates 2021-11-29 22:31:11 +00:00
fmtabbara d69cb47c6c style updates 2021-11-29 22:11:28 +00:00
Aid19801 3e93b4ffd5 Merge pull request #926 from nymtech/copy-change-nodemap
typo copy change for nodemap
2021-11-29 13:15:22 +00:00
Aid Thompson 6da660623b typo copy change for nodemap 2021-11-29 13:09:13 +00:00
fmtabbara a85a67199a theme tweaks 2021-11-29 09:52:00 +00:00
fmtabbara 09b6d37ca6 [ci skip] Generate TS types 2021-11-27 22:59:48 +00:00
fmtabbara 4029b1b830 resolve conflict 2021-11-27 22:50:13 +00:00
fmtabbara e075759461 add tooltip hide delay 2021-11-27 22:36:16 +00:00
fmtabbara 1069032cd7 remove unecessary fullscreen property 2021-11-27 22:29:17 +00:00
fmtabbara b97b7410da update balance page 2021-11-27 22:15:57 +00:00
fmtabbara f259012faf update copy component 2021-11-27 22:15:42 +00:00
fmtabbara c00cdafc8c update getBalance hook name 2021-11-27 22:15:13 +00:00
fmtabbara 82c56351de add appbar component 2021-11-27 22:14:04 +00:00
Dave Hrycyszyn 5eeb55aa78 Fixing some clippy warnings (#922)
Co-authored-by: dave <dave@nym-mbp.lan>
2021-11-26 12:51:41 +00:00
Dave Hrycyszyn 94357c6132 Fixing go warning re unused btc lib (#921)
* Fixing go warning re unused btc lib

* Removed commented code

Co-authored-by: dave <dave@nym-mbp.lan>
2021-11-26 12:51:01 +00:00
Mark Sinclair 77c5296c39 testnet-faucet: fix line break in destructuring 2021-11-26 11:57:37 +00:00
Aid19801 6ee5b68b46 Merge pull request #920 from nymtech/nodemap-responsiveness
quick fix adding dimensions to nodemap page for consistency
2021-11-26 10:47:40 +00:00
Aid Thompson 56fa48215b changed country names from official to their aliases to fit in cells 2021-11-26 10:15:59 +00:00
Aid Thompson 26b0d9555f quick fix adding dimensions to nodemap page for consistency 2021-11-26 07:41:58 +00:00
fmtabbara 61a67ac334 add new sign in and create account pages 2021-11-25 23:22:14 +00:00
fmtabbara c2e268a1c9 update app layout to use mui v5 system 2021-11-25 23:21:19 +00:00
fmtabbara cfd0b7868a refine copytoclipboard component 2021-11-25 17:41:35 +00:00
fmtabbara 69b52ae629 new styling + new signin and create account pages 2021-11-25 17:41:15 +00:00
Jędrzej Stuczyński c361de62a5 Introduces query for contract build information (#919)
* Introduces query for contract build information

* Removed temporary test garbage

* Removed unused imports in validator-client if built without nymd-client feature
2021-11-25 15:17:56 +00:00
fmtabbara 7d3d0d8874 upgrade to mui v5 2021-11-25 14:35:26 +00:00
Jędrzej Stuczyński 39c7b131b6 Bugfix/remove mixnode bonding overwrite (#917)
* Disallow double bonding

* Adjusted test assertions
2021-11-25 10:27:31 +00:00
dependabot[bot] 4071a20bb5 Bump nth-check from 2.0.0 to 2.0.1 in /nym-wallet (#918)
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-25 10:16:14 +00:00
Tommy Verrall b4519cd77e Merge pull request #911 from nymtech/feature/faucet-page-react
Feature/faucet page react
2021-11-25 09:59:54 +00:00
Dave Hrycyszyn 35748e07c4 Feature/mixnet contract refactor (#910)
* Starting a refactor to cut the huge files into chunks

* Fixing some lints

* ibid

* Mixnode and gateway bonding tests moved

* All transaction test moved into submodules

* Finished splitting out transactions.rs from root into submodules.

* Moved mixnet params state into submodule

* Recombined modules for few top-level actions

* Moving mixnode bonding queries into their own file

* Removed some unused imports

* Got tests running again. Max limit tests not right.

* Fixed tests

* Started moving delegation queries into own module

* Finished moving delegation queries into their own module

* Cleanup

* Moving query limits into relevant modules

* Putting query limits back at top-level

* Using prefix to make storage usage a little more explicit

* Separating storage into smaller chunks

* More storage refactoring

* Finished moving all storage into modules

* Moved all storage prefixes into relevant modules and made them not-public

* Renamed the mostly-empty queries module to query_support

* ibid

* Fixed query support rename problems

* Started to move rewards-related helpers into their own module

* Started moving delegations-related helpers into their own module

* Moved more code from global helpers into delegations helpers

* Moved all remaining test helper code from main helpers file into test helpers

* Made use of test_helpers explicit via a module rename.

Also got rid of non-explicit usages

* Moved mixnode storage retrieval limits into mixnodes storage module

* Moved bond retrieval max limit into storage moduel

* Moved more storage limits into mixnodes storage file; fixed a gateways limit test.

* Added a note on gateways limits constants

I'll re-use the mixnodes values, but it doesn't have to be this way.

We could easily make a specific constant for gateways instaed.

* Renamed "state" to GlobalContractParams

* Pulled bit of test helper code up a level

* Small cleanup of zero spacing in constants

* Made a local helper method private

* Renaming GlobalContractParams to ContractSettings and StateParams to ContractSettingsParams

* ibid

* Renamed contract settings storage methods from "config" (which is a bit vage to "contract_settings"

* Indulging a fullword as a personal protest vs the Go programming language

* Renaming mixnet settings to mixnet contract settings

* Making validate_mixnode_bond private and moving it downwards in the file

* Moving gateway bond validation to the bottom of the tests file

* Getting the wallet compiling again.

* Updated TypeScript client with new types and contract method names

* Updating rust validator client with new contract method names and types

* Fixed type error in mixnet-contract shared msg.

* Used new contract method names and types

* Fixed warnings in non-test code

* All tests compiling

But not passing yet

* Fixed test compilation warnings

* Fixed tests

* Test-locked Delegations struct

Co-authored-by: dave <dave@nym-mbp.lan>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2021-11-24 22:14:38 +00:00
Aid19801 c8f2548090 Merge pull request #915 from nymtech/fix-mob-datagrids
Fix Mobile View for MUI data-grid (CARD 108)
2021-11-24 13:28:36 +00:00
Aid Thompson 62f20a905e Alignment on Overview page 2021-11-24 10:45:54 +00:00
Jędrzej Stuczyński a3dd94507a Feature/total delegation bucket (#913)
* Preliminary attempt at creating StoredMixnodeBond

* Fixed tests

* Removed dead code

* Moved StoredMixnodeBond to storage.rs

* Removed redundant clone
2021-11-23 16:36:48 +00:00
Aid Thompson dd36b329d5 renamed back to universal-datagrid 2021-11-23 16:03:39 +00:00
Aid Thompson 581af4a295 gateways page performant 2021-11-23 15:49:05 +00:00
Jędrzej Stuczyński 085761a9fb Feature/batch delegator rewarding (#898)
* Unnrolled the loop into separate function

* Ugly way of saving rewarding status

* Initial way of rewarding next page of delegators

* Attribute passing

* Promoted transactions to directory

* Moved rewarding-related functionalities into separate file

* [ci skip] Generate TS types

* Better errors on double rewarding attempt

* Removed old rewarding call

* Test fixes

* Some cleanup

* Paged mixnode rewarding test + serde fixes

* Tests for delegator rewarding

* ExecuteMsg for MixDelegatorRewarding

* Made validator-api code compliable

with bunch of todo!() macros

* Removed Option wrapper from params in MixnodeToReward

* Calculating uptime for entire epoch

* Created shared MIXNODE_DELEGATORS_PAGE_LIMIT constant

* Using new rewarding messages in validator API

* cargo fmt

* Updated wallet state types

* Additional test for correct rewarding information

* Query for rewarding status

* Additional test regarding delegator rewarding

* Client methods for obtaining rewarding status

* Validator API checking for full rewarding

* Removed unused field from validator api config template

* Waiting for MINIMUM number of test routes

* Waiting initialisation_backoff in the early return case

* Fixes crash condition in validator API when calculating last day uptime

* Fixed typo

* Dealing with the case of rewarding mixnode with 0 uptime

* Removed temporary unwrap

* Guarding against 0-size rewarded/active sets

Co-authored-by: jstuczyn <jstuczyn@users.noreply.github.com>
2021-11-23 15:36:20 +00:00
Aid Thompson 58537224df mixnode datagrid performant 2021-11-23 15:20:23 +00:00
fmtabbara 54e217ccea update logic 2021-11-23 15:01:05 +00:00
Jędrzej Stuczyński 78a5dbbf05 Fixes crash condition in validator API when calculating last day uptime (#909) 2021-11-23 14:47:49 +00:00
fmtabbara c8f81d118b fix bug with successful response ui 2021-11-23 12:16:07 +00:00
fmtabbara a193e948e6 link address to block explorer + refresh balance after tokens sent 2021-11-23 11:34:05 +00:00
AniaPiotrowska ab7f24dc1f Merge pull request #837 from nymtech/feature/vouchers
Feature/vouchers
2021-11-22 19:34:58 +00:00
fmtabbara cc77f6e392 add favicon 2021-11-22 14:48:37 +00:00
Tommy Verrall 58f02109be Add data-testids 2021-11-22 11:00:20 +00:00
Fouad 724e0f78c6 Update README.md 2021-11-20 21:27:07 +00:00
fmtabbara d764b11122 responsive tweaks 2021-11-20 21:24:30 +00:00
fmtabbara 4953b40a42 add readme file 2021-11-20 21:12:06 +00:00
fmtabbara 66e5afe485 add form validation and error handling 2021-11-20 21:04:44 +00:00
fmtabbara 9b17a1ca77 add responses 2021-11-19 22:46:32 +00:00
fmtabbara 5ce6147839 set up validator connection 2021-11-19 18:49:52 +00:00
fmtabbara dd3643a1bb set up form ui 2021-11-19 17:41:21 +00:00
fmtabbara 52fedd9866 create base project 2021-11-19 16:27:45 +00:00
Jędrzej Stuczyński 289605f36b Bugfix/monitor initial values wait (#907)
* Waiting for MINIMUM number of test routes

* Waiting initialisation_backoff in the early return case
2021-11-18 17:16:15 +00:00
Tommy Verrall 4f6bf3423b Merge pull request #905 from nymtech/update/readme
Update README.md
2021-11-18 09:59:55 +00:00
Tommy Verrall 04a32156d4 Merge pull request #891 from nymtech/update/remove-gateway-delegations
remove delegation and undelegation from gateways
2021-11-18 09:42:34 +00:00
Tommy Verrall 427880d23f Merge pull request #896 from nymtech/bug-fix/macos-keyboard-shortcuts
Bug fix/macos keyboard shortcuts
2021-11-18 09:22:33 +00:00
Tommy Verrall a5e47dead8 Update readme
. If `Webview2` is not installed on Windows, that app will not launch correctly. There's two ways to update if you're running on an older version of windows. 
- Update the Edge browser in your current OS
- Update via the installer now provided in the README documentation.
2021-11-18 09:18:04 +00:00
Tommy Verrall 297b7b2355 Merge pull request #904 from nymtech/bug-bond-denom
BUG: Bond cell denom
2021-11-17 12:30:45 +00:00
Tommy Verrall d7f3abfe7d Merge pull request #903 from nymtech/test/fix-detail-tables
Explorer UI tests missing data-testid
2021-11-17 12:29:34 +00:00
Aid Thompson 234952a6bb removed typo 2021-11-17 11:50:03 +00:00
Tommy Verrall 930561faf5 Explorer UI tests
. This `data-testid` was missing and is needed after changes to DataGrid into MUI-Table
2021-11-17 11:20:43 +00:00
Tommy e5051f98c5 Adding a few more menu options 2021-11-12 11:23:36 +00:00
fmtabbara 2f69958e21 macos keyboard shortcut fix 2021-11-12 10:29:49 +00:00
fmtabbara d427591a66 dont commite dist folder contents 2021-11-12 10:29:33 +00:00
fmtabbara 0257c42ee9 update tauri bundle identifier 2021-11-12 10:29:04 +00:00
fmtabbara 8ebfc72da8 remove contents of dist folder 2021-11-12 10:28:39 +00:00
fmtabbara b1178b7ad5 remove delegation and undelegation from gateways 2021-11-10 10:42:45 +00:00
333 changed files with 34200 additions and 17761 deletions
@@ -1,4 +1,4 @@
name: Mixnet Contract
name: Contracts
on:
push:
@@ -21,7 +21,7 @@ jobs:
with:
inputFile: '.github/workflows/contract_matrix_includes.json'
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
mixnet-contract:
contracts:
# since it's going to be compiled into wasm, there's absolutely
# no point in running CI on different OS-es
runs-on: ubuntu-latest
@@ -45,20 +45,20 @@ jobs:
RUSTFLAGS: '-C link-arg=-s'
with:
command: build
args: --manifest-path contracts/mixnet/Cargo.toml --target wasm32-unknown-unknown
args: --manifest-path contracts/Cargo.toml --all --target wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path contracts/mixnet/Cargo.toml
args: --manifest-path contracts/Cargo.toml
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path contracts/mixnet/Cargo.toml -- --check
args: --manifest-path contracts/Cargo.toml --all -- --check
- uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --manifest-path contracts/mixnet/Cargo.toml -- -D warnings
args: --manifest-path contracts/Cargo.toml --all -- -D warnings
@@ -1,58 +0,0 @@
name: ERC20 Bridge Contract
on: [ push, pull_request ]
jobs:
matrix_prep:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
# creates the matrix strategy from build_matrix_includes.json
- uses: actions/checkout@v2
- id: set-matrix
uses: JoshuaTheMiller/conditional-build-matrix@main
with:
inputFile: '.github/workflows/contract_matrix_includes.json'
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
erc20-bridge-contract:
needs: matrix_prep
strategy:
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
# since it's going to be compiled into wasm, there's absolutely
# no point in running CI on different OS-es
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.rust == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: wasm32-unknown-unknown
override: true
components: rustfmt, clippy
- uses: actions-rs/cargo@v1
env:
RUSTFLAGS: '-C link-arg=-s'
with:
command: build
args: --manifest-path contracts/erc20-bridge/Cargo.toml --target wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path contracts/erc20-bridge/Cargo.toml
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path contracts/erc20-bridge/Cargo.toml -- --check
- uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --manifest-path contracts/erc20-bridge/Cargo.toml -- -D warnings
+2
View File
@@ -22,6 +22,8 @@ jobs:
node-version: '14'
- run: npm install
continue-on-error: true
- name: Set environment from the example
run: cp .env.prod .env
- run: npm run test
continue-on-error: true
- run: npm run build
@@ -1,4 +1,4 @@
name: Publish Tauri Wallet
name: Publish Nym Wallet
on:
push:
tags:
+3 -1
View File
@@ -24,6 +24,7 @@ v6-topology.json
/explorer/downloads/topology.json
/explorer/public/downloads/mixmining.json
/explorer/public/downloads/topology.json
/nym-wallet/dist/*
/clients/validator/examples/nym-driver-example/current-contract.txt
validator-api/v4.json
validator-api/v6.json
@@ -33,4 +34,5 @@ contracts/mixnet/code_id
contracts/mixnet/Justfile
contracts/mixnet/Makefile
validator-config
*.patch
*.patch
validator-api-config.toml
Generated
+204 -30
View File
@@ -420,7 +420,10 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
@@ -504,6 +507,15 @@ dependencies = [
"system-deps 3.2.0",
]
[[package]]
name = "cast"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
dependencies = [
"rustc_version 0.4.0",
]
[[package]]
name = "cc"
version = "1.0.70"
@@ -667,30 +679,11 @@ dependencies = [
name = "coconut-interface"
version = "0.1.0"
dependencies = [
"coconut-rs",
"getset",
"nymcoconut",
"serde",
]
[[package]]
name = "coconut-rs"
version = "0.5.0"
source = "git+https://github.com/nymtech/coconut.git?branch=0.5.0#a1b72d51aa2a67b73b9f58d707030ae6dc70af7f"
dependencies = [
"bls12_381",
"bs58",
"digest 0.9.0",
"ff",
"getrandom 0.2.3",
"group",
"itertools",
"rand 0.8.4",
"serde",
"serde_derive",
"sha2",
"thiserror",
]
[[package]]
name = "colored"
version = "2.0.0"
@@ -875,8 +868,7 @@ dependencies = [
[[package]]
name = "cosmos-sdk-proto"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb5204c6ddc4352c74297638b5561f2929d6334866c156e5f3c75e1e1a1436a"
source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58"
dependencies = [
"prost",
"prost-types",
@@ -886,8 +878,7 @@ dependencies = [
[[package]]
name = "cosmrs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d31147fe89e547e74e2692e4bec35387e1ea406fe8cfb14c0ea177b58fd2a8a9"
source = "git+https://github.com/cosmos/cosmos-rust?rev=e5a1872083abb3d88fa62dda966e7f5408deba58#e5a1872083abb3d88fa62dda966e7f5408deba58"
dependencies = [
"bip32",
"cosmos-sdk-proto",
@@ -908,8 +899,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c16b255449b3f5cd7fa4b79acd5225b5185655261087a3d8aaac44f88a0e23e9"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -920,16 +912,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abad1a6ff427a2f66890a4dce6354b4563cd07cee91a942300e011c921c09ed2"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-std"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1660ee3d5734672e1eb4f0ceda403e2d83345e15143a48845f340f3252ce99a6"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -986,6 +980,42 @@ dependencies = [
"validator-client",
]
[[package]]
name = "criterion"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10"
dependencies = [
"atty",
"cast",
"clap",
"criterion-plot",
"csv",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
@@ -1125,6 +1155,28 @@ dependencies = [
"syn",
]
[[package]]
name = "csv"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "ct-logs"
version = "0.8.0"
@@ -1156,6 +1208,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "cw-storage-plus"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b8b840947313c1a1cccf056836cd79a60b4526bdcd6582995be37dc97be4ae"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "darling"
version = "0.10.2"
@@ -1740,6 +1803,7 @@ dependencies = [
"az",
"bytemuck",
"half",
"serde",
"typenum",
]
@@ -3464,6 +3528,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"network-defaults",
"nymsphinx",
"pemstore",
"pretty_env_logger",
@@ -3509,8 +3574,10 @@ name = "nym-gateway"
version = "0.11.0"
dependencies = [
"bip39",
"bs58",
"clap",
"coconut-interface",
"colored",
"config",
"credentials",
"crypto",
@@ -3532,6 +3599,7 @@ dependencies = [
"rand 0.7.3",
"serde",
"sqlx",
"subtle-encoding",
"thiserror",
"tokio",
"tokio-stream",
@@ -3567,6 +3635,7 @@ dependencies = [
"rocket",
"serde",
"serial_test",
"subtle-encoding",
"tokio",
"tokio-util",
"toml",
@@ -3613,6 +3682,7 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"network-defaults",
"nymsphinx",
"ordered-buffer",
"pemstore",
@@ -3670,6 +3740,27 @@ dependencies = [
"version-checker",
]
[[package]]
name = "nymcoconut"
version = "0.5.0"
dependencies = [
"bincode",
"bls12_381",
"bs58",
"criterion",
"digest 0.9.0",
"doc-comment",
"ff",
"getrandom 0.2.3",
"group",
"itertools",
"rand 0.8.4",
"serde",
"serde_derive",
"sha2",
"thiserror",
]
[[package]]
name = "nymsphinx"
version = "0.1.0"
@@ -3834,6 +3925,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opaque-debug"
version = "0.2.3"
@@ -4264,6 +4361,34 @@ version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]]
name = "plotters"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
[[package]]
name = "plotters-svg"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
dependencies = [
"plotters-backend",
]
[[package]]
name = "pmutil"
version = "0.5.3"
@@ -4810,6 +4935,12 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.25"
@@ -5091,6 +5222,15 @@ dependencies = [
"semver 0.11.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.4",
]
[[package]]
name = "rustls"
version = "0.19.1"
@@ -5330,6 +5470,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.130"
@@ -6608,6 +6758,16 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tokio"
version = "1.12.0"
@@ -6985,6 +7145,7 @@ dependencies = [
"thiserror",
"ts-rs",
"url",
"vesting-contract",
]
[[package]]
@@ -7024,6 +7185,19 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "vesting-contract"
version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"mixnet-contract",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
+2
View File
@@ -30,6 +30,7 @@ members = [
"common/mixnode-common",
"common/network-defaults",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -61,6 +62,7 @@ default-members = [
"service-providers/network-requester",
"mixnode",
"validator-api",
"explorer-api",
]
exclude = ["explorer", "contracts", "tokenomics-py"]
+31
View File
@@ -0,0 +1,31 @@
all: clippy test fmt
clippy: clippy-main clippy-contracts clippy-wallet
test: test-main test-contracts test-wallet
fmt: fmt-main fmt-contracts fmt-wallet
clippy-main:
cargo clippy
clippy-contracts:
cargo clippy --manifest-path contracts/Cargo.toml
clippy-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml
test-main:
cargo test
test-contracts:
cargo test --manifest-path contracts/Cargo.toml
test-wallet:
cargo test --manifest-path nym-wallet/Cargo.toml
fmt-main:
cargo fmt --all
fmt-contracts:
cargo fmt --manifest-path contracts/Cargo.toml --all
fmt-wallet:
cargo fmt --manifest-path nym-wallet/Cargo.toml --all
+1 -1
View File
@@ -13,7 +13,7 @@ The platform is composed of multiple Rust crates. Top-level executable binary cr
* nym-gateway - acts sort of like a mailbox for mixnet messages, removing the need for directly delivery to potentially offline or firewalled devices.
* nym-network-monitor - sends packets through the full system to check that they are working as expected, and stores node uptime histories as the basis of a rewards system ("mixmining" or "proof-of-mixing").
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
* nym-wallet (currently in development)- a desktop wallet implemented using the [Tauri](https://tauri.studio/en/docs/about/intro) framework.
* nym-wallet - a desktop wallet implemented using the [Tauri](https://tauri.studio/en/docs/about/intro) framework.
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge)](https://opensource.org/licenses/Apache-2.0)
[![Build Status](https://img.shields.io/github/workflow/status/nymtech/nym/Continuous%20integration/develop?style=for-the-badge&logo=github-actions)](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
+2 -1
View File
@@ -24,7 +24,7 @@ dirs = "3.0" # for determining default store directories in config
dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being)
log = "0.4" # self explanatory
pretty_env_logger = "0.4" # for formatting log messages
rand = {version = "0.7.3", features = ["wasm-bindgen"]} # rng-related traits + some rng implementation to use
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
sled = "0.34" # for storage of replySURB decryption keys
tokio = { version = "1.4", features = ["rt-multi-thread", "net", "signal"] } # async runtime
@@ -44,6 +44,7 @@ topology = { path = "../../common/topology" }
websocket-requests = { path = "websocket-requests" }
validator-client = { path = "../../common/client-libs/validator-client" }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut"]
@@ -3,6 +3,5 @@ module github.com/nymtech/nym/clients/native/examples/go
go 1.14
require (
github.com/btcsuite/btcutil v1.0.2 // indirect
github.com/gorilla/websocket v1.4.2
)
+3 -3
View File
@@ -1,8 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, SocketType};
use crate::websocket;
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
@@ -24,6 +22,7 @@ use client_core::client::topology_control::{
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::{
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
MixnetMessageSender,
@@ -35,7 +34,8 @@ use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::receiver::ReconstructedMessage;
use tokio::runtime::Runtime;
use gateway_client::bandwidth::BandwidthController;
use crate::client::config::{Config, SocketType};
use crate::websocket;
pub(crate) mod config;
+46 -5
View File
@@ -1,15 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use crate::commands::override_config;
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::{hash_to_scalar, Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::bandwidth::{
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[cfg(feature = "coconut")]
use network_defaults::BANDWIDTH_VALUE;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use rand::rngs::OsRng;
@@ -21,6 +29,9 @@ use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
use crate::client::config::Config;
use crate::commands::override_config;
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("init")
.about("Initialise a Nym client. Do this first!")
@@ -36,9 +47,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
.takes_value(true)
)
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("disable-socket")
.long("disable-socket")
@@ -71,6 +82,36 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
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 bandwidth_credential_attributes = BandwidthVoucherAttributes {
serial_number: params.random_scalar(),
binding_number: params.random_scalar(),
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
};
let bandwidth_credential =
obtain_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
@@ -1,8 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod handler;
pub(crate) mod listener;
pub(crate) use handler::Handler;
pub(crate) use listener::Listener;
pub(crate) mod handler;
pub(crate) mod listener;
+2 -1
View File
@@ -32,13 +32,14 @@ crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
gateway-requests = { path = "../../gateway/gateway-requests" }
nymsphinx = { path = "../../common/nymsphinx" }
ordered-buffer = {path = "../../common/socks5/ordered-buffer"}
ordered-buffer = { path = "../../common/socks5/ordered-buffer" }
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" }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut"]
+6 -6
View File
@@ -1,11 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
@@ -25,6 +20,7 @@ use client_core::client::topology_control::{
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::{
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
MixnetMessageSender,
@@ -34,7 +30,11 @@ use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use tokio::runtime::Runtime;
use gateway_client::bandwidth::BandwidthController;
use crate::client::config::Config;
use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
pub(crate) mod config;
+46 -5
View File
@@ -1,15 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use crate::commands::override_config;
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::{hash_to_scalar, Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::bandwidth::{
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[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};
@@ -19,6 +27,9 @@ use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
use crate::client::config::Config;
use crate::commands::override_config;
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
let app = App::new("init")
.about("Initialise a Nym client. Do this first!")
@@ -40,9 +51,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
.takes_value(true)
)
.arg(Arg::with_name("validators")
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
.long("validators")
.help("Comma separated list of rest endpoints of the validators")
.takes_value(true),
)
.arg(Arg::with_name("port")
.short("p")
@@ -71,6 +82,36 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
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 bandwidth_credential_attributes = BandwidthVoucherAttributes {
serial_number: params.random_scalar(),
binding_number: params.random_scalar(),
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
voucher_info: hash_to_scalar("BandwidthVoucher"),
};
let bandwidth_credential =
obtain_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>,
+29 -17
View File
@@ -3,21 +3,24 @@
windows_subsystem = "windows"
)]
use std::sync::Arc;
use tokio::sync::RwLock;
use url::Url;
use coconut_interface::{
self, hash_to_scalar, Attribute, Credential, Parameters, Signature, Theta, VerificationKey,
};
use credentials::{obtain_aggregate_signature, obtain_aggregate_verification_key};
use std::sync::Arc;
use tokio::sync::RwLock;
use url::Url;
struct State {
signatures: Vec<Signature>,
n_attributes: u32,
params: Parameters,
public_attributes_bytes: Vec<Vec<u8>>,
public_attributes: Vec<Attribute>,
private_attributes: Vec<Attribute>,
serial_number: Attribute,
binding_number: Attribute,
voucher_value: Attribute,
voucher_info: Attribute,
aggregated_verification_key: Option<VerificationKey>,
}
@@ -37,9 +40,10 @@ impl State {
signatures: Vec::new(),
n_attributes,
params,
public_attributes_bytes,
public_attributes,
private_attributes,
serial_number: private_attributes[0],
binding_number: private_attributes[1],
voucher_value: public_attributes[0],
voucher_info: public_attributes[1],
aggregated_verification_key: None,
}
}
@@ -63,8 +67,8 @@ async fn randomise_credential(
) -> Result<Vec<Signature>, String> {
let mut state = state.write().await;
let signature = state.signatures.remove(idx);
let new = signature.randomise(&state.params);
state.signatures.insert(idx, new);
let (new_signature, _) = signature.randomise(&state.params);
state.signatures.insert(idx, new_signature);
Ok(state.signatures.clone())
}
@@ -117,14 +121,15 @@ async fn prove_credential(
let state = state.read().await;
if let Some(signature) = state.signatures.get(idx) {
match coconut_interface::prove_credential(
match coconut_interface::prove_bandwidth_credential(
&state.params,
&verification_key,
signature,
&state.private_attributes,
state.serial_number,
state.binding_number,
) {
Ok(theta) => Ok(theta),
Err(e) => Err(format!("{}", e)),
Err(e) => Err(format!("{:?}", e)),
}
} else {
Err("Got invalid Signature idx".to_string())
@@ -144,10 +149,15 @@ async fn verify_credential(
let state = state.read().await;
let public_attributes_bytes = vec![
state.voucher_value.to_bytes().to_vec(),
state.voucher_info.to_bytes().to_vec(),
];
let credential = Credential::new(
state.n_attributes,
theta,
state.public_attributes_bytes.clone(),
public_attributes_bytes,
state
.signatures
.get(idx)
@@ -164,11 +174,13 @@ async fn get_credential(
) -> Result<Vec<Signature>, String> {
let guard = state.read().await;
let parsed_urls = parse_url_validators(&validator_urls)?;
let public_attributes = vec![guard.voucher_value, guard.voucher_info];
let private_attributes = vec![guard.serial_number, guard.binding_number];
let signature = obtain_aggregate_signature(
&guard.params,
&guard.public_attributes,
&guard.private_attributes,
&public_attributes,
&private_attributes,
&parsed_urls,
)
.await
+26 -26
View File
@@ -1,6 +1,6 @@
import NetClient, {INetClient} from "./net-client";
import NetClient, { INetClient } from "./net-client";
import {
StateParams,
ContractSettingsParams,
Delegation,
PagedMixDelegationsResponse,
PagedGatewayDelegationsResponse,
@@ -10,10 +10,10 @@ import {
Gateway,
SendRequest
} from "./types";
import {Bip39, Random} from "@cosmjs/crypto";
import {DirectSecp256k1HdWallet, EncodeObject} from "@cosmjs/proto-signing";
import { Bip39, Random } from "@cosmjs/crypto";
import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing";
import MixnodesCache from "./caches/mixnodes";
import {buildFeeTable, coin, Coin, coins, StdFee} from "@cosmjs/stargate";
import { buildFeeTable, coin, Coin, coins, StdFee } from "@cosmjs/stargate";
import {
ExecuteResult,
InstantiateOptions,
@@ -32,17 +32,17 @@ import {
nativeToPrintable
} from "./currency";
import GatewaysCache from "./caches/gateways";
import QueryClient, {IQueryClient} from "./query-client";
import {nymGasLimits, nymGasPrice} from "./stargate-helper";
import {BroadcastTxSuccess, isBroadcastTxFailure} from "@cosmjs/stargate";
import {makeBankMsgSend} from "./utils";
import QueryClient, { IQueryClient } from "./query-client";
import { nymGasLimits, nymGasPrice } from "./stargate-helper";
import { BroadcastTxSuccess, isBroadcastTxFailure } from "@cosmjs/stargate";
import { makeBankMsgSend } from "./utils";
export const VALIDATOR_API_PORT = "8080";
export const VALIDATOR_API_GATEWAYS = "v1/gateways";
export const VALIDATOR_API_MIXNODES = "v1/mixnodes";
export {coins, coin};
export {Coin};
export { coins, coin };
export { Coin };
export {
displayAmountToNative,
nativeCoinToDisplay,
@@ -52,7 +52,7 @@ export {
MappedCoin,
CoinMap
}
export {nymGasLimits, nymGasPrice}
export { nymGasLimits, nymGasPrice }
export default class ValidatorClient {
private readonly client: INetClient | IQueryClient
@@ -226,12 +226,12 @@ export default class ValidatorClient {
*/
static async mnemonicToAddress(mnemonic: string, prefix: string): Promise<string> {
const wallet = await ValidatorClient.buildWallet(mnemonic, prefix);
const [{address}] = await wallet.getAccounts()
const [{ address }] = await wallet.getAccounts()
return address
}
static async buildWallet(mnemonic: string, prefix: string): Promise<DirectSecp256k1HdWallet> {
const signerOptions = {prefix: prefix};
const signerOptions = { prefix: prefix };
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, signerOptions);
}
@@ -239,7 +239,7 @@ export default class ValidatorClient {
return this.client.getBalance(address, this.denom).catch((err) => this.handleRequestFailure(err));
}
async getStateParams(): Promise<StateParams> {
async getStateParams(): Promise<ContractSettingsParams> {
return this.client.getStateParams(this.contractAddress).catch((err) => this.handleRequestFailure(err))
}
@@ -293,7 +293,7 @@ export default class ValidatorClient {
*/
async bondMixnode(mixNode: MixNode, bond: Coin): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_mixnode: {mix_node: mixNode}}, "adding mixnode", [bond]).catch((err) => this.handleRequestFailure(err));
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_mixnode: { mix_node: mixNode } }, "adding mixnode", [bond]).catch((err) => this.handleRequestFailure(err));
console.log(`account ${this.client.clientAddress} added mixnode with ${mixNode.host}`);
return result;
} else {
@@ -307,7 +307,7 @@ export default class ValidatorClient {
*/
async unbondMixnode(): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_mixnode: {}}).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_mixnode: {} }).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} unbonded mixnode`);
return result;
} else {
@@ -324,7 +324,7 @@ export default class ValidatorClient {
// requires coin type to ensure correct denomination (
async delegateToMixnode(mixIdentity: string, amount: Coin): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {delegate_to_mixnode: {mix_identity: mixIdentity}}, `delegating to ${mixIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_mixnode: { mix_identity: mixIdentity } }, `delegating to ${mixIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} delegated ${amount} to mixnode ${mixIdentity}`);
return result;
} else {
@@ -339,7 +339,7 @@ export default class ValidatorClient {
*/
async removeMixnodeDelegation(mixIdentity: string): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {undelegate_from_mixnode: {mix_identity: mixIdentity}}).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_mixnode: { mix_identity: mixIdentity } }).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} removed delegation from mixnode ${mixIdentity}`);
return result;
} else {
@@ -356,7 +356,7 @@ export default class ValidatorClient {
// requires coin type to ensure correct denomination (
async delegateToGateway(gatewayIdentity: string, amount: Coin): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {delegate_to_gateway: {gateway_identity: gatewayIdentity}}, `delegating to ${gatewayIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_gateway: { gateway_identity: gatewayIdentity } }, `delegating to ${gatewayIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} delegated ${amount} to gateway ${gatewayIdentity}`);
return result;
} else {
@@ -371,7 +371,7 @@ export default class ValidatorClient {
*/
async removeGatewayDelegation(gatewayIdentity: string): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {undelegate_from_gateway: {gateway_identity: gatewayIdentity}}).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_gateway: { gateway_identity: gatewayIdentity } }).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} removed delegation from gateway ${gatewayIdentity}`);
return result;
} else {
@@ -450,7 +450,7 @@ export default class ValidatorClient {
*/
async bondGateway(gateway: Gateway, bond: Coin): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_gateway: {gateway: gateway}}, "adding gateway", [bond]).catch((err) => this.handleRequestFailure(err));
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_gateway: { gateway: gateway } }, "adding gateway", [bond]).catch((err) => this.handleRequestFailure(err));
console.log(`account ${this.client.clientAddress} added gateway with ${gateway.host}`);
return result;
} else {
@@ -463,7 +463,7 @@ export default class ValidatorClient {
*/
async unbondGateway(): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_gateway: {}}).catch((err) => this.handleRequestFailure(err))
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_gateway: {} }).catch((err) => this.handleRequestFailure(err))
console.log(`account ${this.client.clientAddress} unbonded gateway`);
return result;
} else {
@@ -471,9 +471,9 @@ export default class ValidatorClient {
}
}
async updateStateParams(newParams: StateParams): Promise<ExecuteResult> {
async updateStateParams(newParams: ContractSettingsParams): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, {update_state_params: newParams}, "updating contract state").catch((err) => this.handleRequestFailure(err));
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, { update_contract_settings: newParams }, "updating contract settings").catch((err) => this.handleRequestFailure(err));
} else {
throw new Error("Tried to update state params with a query client")
}
@@ -580,7 +580,7 @@ export default class ValidatorClient {
// the function to calculate fee for a single entry is not exposed...
console.log(`this.denom is ${this.denom}`);
const table = buildFeeTable(nymGasPrice(this.prefix), {sendMultiple: nymGasLimits.send * data.length}, {sendMultiple: nymGasLimits.send * data.length})
const table = buildFeeTable(nymGasPrice(this.prefix), { sendMultiple: nymGasLimits.send * data.length }, { sendMultiple: nymGasLimits.send * data.length })
const fee = table.sendMultiple
const result = await this.client.signAndBroadcast(senderAddress, encoded, fee, memo)
if (isBroadcastTxFailure(result)) {
+8 -8
View File
@@ -80,7 +80,7 @@ export default class NetClient implements INetClient {
}
public static async connect(wallet: DirectSecp256k1HdWallet, url: string, prefix: string): Promise<INetClient> {
const [{address}] = await wallet.getAccounts();
const [{ address }] = await wallet.getAccounts();
const signerOptions: SigningCosmWasmClientOptions = {
gasPrice: nymGasPrice(prefix),
gasLimits: nymGasLimits,
@@ -95,17 +95,17 @@ export default class NetClient implements INetClient {
public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedMixnodeResponse> {
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit}});
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } });
} else {
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit, start_after}});
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit, start_after } });
}
}
public getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse> {
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit}});
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit } });
} else {
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit, start_after}});
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit, start_after } });
}
}
@@ -166,11 +166,11 @@ export default class NetClient implements INetClient {
}
public ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse> {
return this.cosmClient.queryContractSmart(contractAddress, {owns_mixnode: {address}});
return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } });
}
public ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse> {
return this.cosmClient.queryContractSmart(contractAddress, {owns_gateway: {address}});
return this.cosmClient.queryContractSmart(contractAddress, { owns_gateway: { address } });
}
public getBalance(address: string, denom: string): Promise<Coin | null> {
@@ -178,7 +178,7 @@ export default class NetClient implements INetClient {
}
public getStateParams(contractAddress: string): Promise<StateParams> {
return this.cosmClient.queryContractSmart(contractAddress, {state_params: {}});
return this.cosmClient.queryContractSmart(contractAddress, { contract_settings_params: {} });
}
public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult> {
+10 -10
View File
@@ -6,7 +6,7 @@ import {
MixOwnershipResponse, PagedGatewayDelegationsResponse,
PagedGatewayResponse, PagedMixDelegationsResponse,
PagedMixnodeResponse,
StateParams
ContractSettingsParams
} from "./types";
export interface IQueryClient {
@@ -28,7 +28,7 @@ export interface IQueryClient {
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
getStateParams(contractAddress: string): Promise<StateParams>;
getStateParams(contractAddress: string): Promise<ContractSettingsParams>;
changeValidator(newUrl: string): Promise<void>
}
@@ -59,17 +59,17 @@ export default class QueryClient implements IQueryClient {
public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedMixnodeResponse> {
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit}});
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } });
} else {
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit, start_after}});
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit, start_after } });
}
}
public getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse> {
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit}});
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit } });
} else {
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit, start_after}});
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit, start_after } });
}
}
@@ -130,18 +130,18 @@ export default class QueryClient implements IQueryClient {
}
public ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse> {
return this.cosmClient.queryContractSmart(contractAddress, {owns_mixnode: {address}});
return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } });
}
public ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse> {
return this.cosmClient.queryContractSmart(contractAddress, {owns_gateway: {address}});
return this.cosmClient.queryContractSmart(contractAddress, { owns_gateway: { address } });
}
public getBalance(address: string, stakeDenom: string): Promise<Coin | null> {
return this.cosmClient.getBalance(address, stakeDenom);
}
public getStateParams(contractAddress: string): Promise<StateParams> {
return this.cosmClient.queryContractSmart(contractAddress, {state_params: {}});
public getStateParams(contractAddress: string): Promise<ContractSettingsParams> {
return this.cosmClient.queryContractSmart(contractAddress, { contract_settings_params: {} });
}
}
+2 -2
View File
@@ -42,7 +42,7 @@ export type GatewayOwnershipResponse = {
has_gateway: boolean,
}
export type StateParams = {
export type ContractSettingsParams = {
epoch_length: number,
// ideally I'd want to define those as `number` rather than `string`, but
// rust-side they are defined as Uint128 and Decimal that don't have
@@ -103,7 +103,7 @@ export type MixNode = {
export type GatewayBond = {
owner: string
gateway: Gateway,
bond_amount: Coin,
total_delegation: Coin,
}
+2 -3
View File
@@ -3,6 +3,7 @@
use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::GatewayClient;
use nymsphinx::acknowledgements::AckKey;
use nymsphinx::addressing::clients::Recipient;
@@ -17,8 +18,6 @@ use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use wasm_utils::{console_log, console_warn};
use gateway_client::bandwidth::BandwidthController;
pub(crate) mod received_processor;
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
@@ -250,7 +249,7 @@ impl NymClient {
let validator_client = validator_client::ApiClient::new(self.validator_server.clone());
let mixnodes = match validator_client.get_cached_active_mixnodes().await {
Err(err) => panic!("{}", err),
Err(err) => panic!("{:?}", err),
Ok(mixes) => mixes,
};
@@ -1,10 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::{obtain_signature, prepare_for_spending},
bandwidth::{
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
},
utils::obtain_aggregate_verification_key,
};
#[cfg(not(feature = "coconut"))]
@@ -12,10 +13,11 @@ use credentials::token::bandwidth::TokenCredential;
#[cfg(not(feature = "coconut"))]
use crypto::asymmetric::identity;
use crypto::asymmetric::identity::PublicKey;
use network_defaults::BANDWIDTH_VALUE;
#[cfg(not(feature = "coconut"))]
use network_defaults::{
eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS,
ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN,
eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH,
TOKENS_TO_BURN,
};
#[cfg(not(feature = "coconut"))]
use rand::rngs::OsRng;
@@ -33,6 +35,8 @@ use web3::{
Web3,
};
use crate::error::GatewayClientError;
#[cfg(not(feature = "coconut"))]
pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
Contract::from_json(
@@ -108,15 +112,31 @@ 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 bandwidth_credential =
obtain_signature(&self.identity.to_bytes(), &self.validator_endpoints).await?;
// TODO: Decide what is the value and additional info associated with the bandwidth voucher
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
serial_number: params.random_scalar(),
binding_number: params.random_scalar(),
voucher_value: coconut_interface::hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
voucher_info: coconut_interface::hash_to_scalar(
String::from("BandwidthVoucher").as_bytes(),
),
};
let bandwidth_credential = obtain_signature(
&params,
&bandwidth_credential_attributes,
&self.validator_endpoints,
)
.await?;
// the above would presumably be loaded from a file
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
Ok(prepare_for_spending(
&self.identity.to_bytes(),
&bandwidth_credential,
&bandwidth_credential_attributes,
&verification_key,
)?)
}
@@ -203,9 +223,10 @@ impl BandwidthController {
#[cfg(not(feature = "coconut"))]
#[cfg(test)]
mod tests {
use super::*;
use network_defaults::ETH_EVENT_NAME;
use super::*;
#[test]
fn parse_contract() {
let transport =
@@ -10,6 +10,7 @@ rust-version = "1.56"
[dependencies]
base64 = "0.13"
mixnet-contract = { path="../../../common/mixnet-contract" }
vesting-contract = { path="../../../contracts/vesting" }
serde = { version="1", features=["derive"] }
serde_json = "1"
reqwest = { version="0.11", features=["json"] }
@@ -26,12 +27,13 @@ network-defaults = { path = "../../network-defaults" }
async-trait = { version = "0.1.51", optional = true }
bip39 = { version = "1", features = ["rand"], optional = true }
config = { path = "../../config", optional = true }
cosmrs = { version = "0.3", features = ["rpc", "bip32", "cosmwasm"], optional = true }
#cosmrs = { version = "0.3", features = ["rpc", "bip32", "cosmwasm"], optional = true }
cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev="e5a1872083abb3d88fa62dda966e7f5408deba58", features = ["rpc", "bip32", "cosmwasm"], optional = true }
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 = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", optional = true }
cosmwasm-std = { version = "1.0.0-beta2", optional = true }
ts-rs = {version = "5.1", optional = true}
[features]
@@ -6,13 +6,18 @@ use crate::nymd::{
error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient,
};
#[cfg(feature = "nymd-client")]
use mixnet_contract::StateParams;
use mixnet_contract::ContractStateParams;
use crate::{validator_api, ValidatorClientError};
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
use mixnet_contract::{GatewayBond, MixNodeBond};
#[cfg(feature = "nymd-client")]
use mixnet_contract::{RawDelegationData, RewardingIntervalResponse};
use mixnet_contract::{
Delegation, MixnetContractVersion, MixnodeRewardingStatusResponse, RewardingIntervalResponse,
};
use mixnet_contract::{GatewayBond, MixNodeBond};
#[cfg(feature = "nymd-client")]
use std::str::FromStr;
use url::Url;
#[cfg(feature = "nymd-client")]
@@ -20,6 +25,7 @@ pub struct Config {
api_url: Url,
nymd_url: Url,
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
mixnode_page_limit: Option<u32>,
gateway_page_limit: Option<u32>,
@@ -32,10 +38,12 @@ impl Config {
nymd_url: Url,
api_url: Url,
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
) -> Self {
Config {
nymd_url,
mixnet_contract_address,
vesting_contract_address,
api_url,
mixnode_page_limit: None,
gateway_page_limit: None,
@@ -62,6 +70,7 @@ impl Config {
#[cfg(feature = "nymd-client")]
pub struct Client<C> {
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
mnemonic: Option<bip39::Mnemonic>,
mixnode_page_limit: Option<u32>,
@@ -83,11 +92,14 @@ impl Client<SigningNymdClient> {
let nymd_client = NymdClient::connect_with_mnemonic(
config.nymd_url.as_str(),
config.mixnet_contract_address.clone(),
config.vesting_contract_address.clone(),
mnemonic.clone(),
None,
)?;
Ok(Client {
mixnet_contract_address: config.mixnet_contract_address,
vesting_contract_address: config.vesting_contract_address,
mnemonic: Some(mnemonic),
mixnode_page_limit: config.mixnode_page_limit,
gateway_page_limit: config.gateway_page_limit,
@@ -101,7 +113,9 @@ impl Client<SigningNymdClient> {
self.nymd = NymdClient::connect_with_mnemonic(
new_endpoint.as_ref(),
self.mixnet_contract_address.clone(),
self.vesting_contract_address.clone(),
self.mnemonic.clone().unwrap(),
None,
)?;
Ok(())
}
@@ -113,16 +127,19 @@ impl Client<QueryNymdClient> {
let validator_api_client = validator_api::Client::new(config.api_url.clone());
let nymd_client = NymdClient::connect(
config.nymd_url.as_str(),
config
.mixnet_contract_address
.clone()
.ok_or(ValidatorClientError::NymdError(
NymdError::NoContractAddressAvailable,
))?,
config.mixnet_contract_address.clone().unwrap_or_else(|| {
cosmrs::AccountId::from_str(network_defaults::DEFAULT_MIXNET_CONTRACT_ADDRESS)
.unwrap()
}),
config.vesting_contract_address.clone().unwrap_or_else(|| {
cosmrs::AccountId::from_str(network_defaults::DEFAULT_VESTING_CONTRACT_ADDRESS)
.unwrap()
}),
)?;
Ok(Client {
mixnet_contract_address: config.mixnet_contract_address,
vesting_contract_address: config.vesting_contract_address,
mnemonic: None,
mixnode_page_limit: config.mixnode_page_limit,
gateway_page_limit: config.gateway_page_limit,
@@ -136,6 +153,7 @@ impl Client<QueryNymdClient> {
self.nymd = NymdClient::connect(
new_endpoint.as_ref(),
self.mixnet_contract_address.clone().unwrap(),
self.vesting_contract_address.clone().unwrap(),
)?;
Ok(())
}
@@ -165,11 +183,18 @@ impl<C> Client<C> {
Ok(self.validator_api.get_gateways().await?)
}
pub async fn get_state_params(&self) -> Result<StateParams, ValidatorClientError>
pub async fn get_contract_settings(&self) -> Result<ContractStateParams, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self.nymd.get_state_params().await?)
Ok(self.nymd.get_contract_settings().await?)
}
pub async fn get_mixnet_contract_version(&self) -> Result<MixnetContractVersion, NymdError>
where
C: CosmWasmClient + Sync,
{
Ok(self.nymd.get_mixnet_contract_version().await?)
}
pub async fn get_current_rewarding_interval(
@@ -181,6 +206,20 @@ impl<C> Client<C> {
Ok(self.nymd.get_current_rewarding_interval().await?)
}
pub async fn get_rewarding_status(
&self,
mix_identity: mixnet_contract::IdentityKey,
rewarding_interval_nonce: u32,
) -> Result<MixnodeRewardingStatusResponse, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self
.nymd
.get_rewarding_status(mix_identity, rewarding_interval_nonce)
.await?)
}
pub async fn get_reward_pool(&self) -> Result<u128, ValidatorClientError>
where
C: CosmWasmClient + Sync,
@@ -286,9 +325,7 @@ impl<C> Client<C> {
Ok(delegations)
}
pub async fn get_all_nymd_mixnode_delegations(
&self,
) -> Result<Vec<mixnet_contract::UnpackedDelegation<RawDelegationData>>, ValidatorClientError>
pub async fn get_all_network_delegations(&self) -> Result<Vec<Delegation>, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
@@ -297,7 +334,7 @@ impl<C> Client<C> {
loop {
let mut paged_response = self
.nymd
.get_all_mix_delegations_paged(
.get_all_network_delegations_paged(
start_after.take(),
self.mixnode_delegations_page_limit,
)
@@ -314,10 +351,10 @@ impl<C> Client<C> {
Ok(delegations)
}
pub async fn get_all_nymd_reverse_mixnode_delegations(
pub async fn get_all_delegator_delegations(
&self,
delegation_owner: &cosmrs::AccountId,
) -> Result<Vec<mixnet_contract::IdentityKey>, ValidatorClientError>
) -> Result<Vec<Delegation>, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
@@ -326,13 +363,13 @@ impl<C> Client<C> {
loop {
let mut paged_response = self
.nymd
.get_reverse_mix_delegations_paged(
mixnet_contract::Addr::unchecked(delegation_owner.as_ref()),
.get_delegator_delegations_paged(
delegation_owner.to_string(),
start_after.take(),
self.mixnode_delegations_page_limit,
)
.await?;
delegations.append(&mut paged_response.delegated_nodes);
delegations.append(&mut paged_response.delegations);
if let Some(start_after_res) = paged_response.start_next_after {
start_after = Some(start_after_res)
@@ -344,28 +381,6 @@ impl<C> Client<C> {
Ok(delegations)
}
pub async fn get_all_nymd_mixnode_delegations_of_owner(
&self,
delegation_owner: &cosmrs::AccountId,
) -> Result<Vec<mixnet_contract::Delegation>, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
let mut delegations = Vec::new();
for node_identity in self
.get_all_nymd_reverse_mixnode_delegations(delegation_owner)
.await?
{
let delegation = self
.nymd
.get_mix_delegation(node_identity, delegation_owner)
.await?;
delegations.push(delegation);
}
Ok(delegations)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -4,7 +4,7 @@
use crate::nymd::cosmwasm_client::helpers::create_pagination;
use crate::nymd::cosmwasm_client::types::{
Account, Code, CodeDetails, Contract, ContractCodeHistoryEntry, ContractCodeId,
SequenceResponse,
SequenceResponse, SimulateResponse,
};
use crate::nymd::error::NymdError;
use async_trait::async_trait;
@@ -14,15 +14,19 @@ use cosmrs::proto::cosmos::auth::v1beta1::{
use cosmrs::proto::cosmos::bank::v1beta1::{
QueryAllBalancesRequest, QueryAllBalancesResponse, QueryBalanceRequest, QueryBalanceResponse,
};
use cosmrs::proto::cosmwasm::wasm::v1beta1::*;
use cosmrs::proto::cosmos::tx::v1beta1::{
SimulateRequest, SimulateResponse as ProtoSimulateResponse,
};
use cosmrs::proto::cosmwasm::wasm::v1::*;
use cosmrs::rpc::endpoint::block::Response as BlockResponse;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::endpoint::tx::Response as TxResponse;
use cosmrs::rpc::query::Query;
use cosmrs::rpc::{self, HttpClient, Order};
use cosmrs::tendermint::abci::Code as AbciCode;
use cosmrs::tendermint::abci::Transaction;
use cosmrs::tendermint::{abci, block, chain};
use cosmrs::{tx, AccountId, Coin, Denom};
use cosmrs::{tx, AccountId, Coin, Denom, Tx};
use prost::Message;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
@@ -49,6 +53,11 @@ pub trait CosmWasmClient: rpc::Client {
let res = self.abci_query(path, buf, None, false).await?;
match res.code {
AbciCode::Err(code) => return Err(NymdError::AbciError(code, res.log)),
AbciCode::Ok => (),
}
Ok(Res::decode(res.value.as_ref())?)
}
@@ -213,7 +222,7 @@ pub trait CosmWasmClient: rpc::Client {
}
async fn get_codes(&self) -> Result<Vec<Code>, NymdError> {
let path = Some("/cosmwasm.wasm.v1beta1.Query/Codes".parse().unwrap());
let path = Some("/cosmwasm.wasm.v1.Query/Codes".parse().unwrap());
let mut raw_codes = Vec::new();
let mut pagination = None;
@@ -240,7 +249,7 @@ pub trait CosmWasmClient: rpc::Client {
}
async fn get_code_details(&self, code_id: ContractCodeId) -> Result<CodeDetails, NymdError> {
let path = Some("/cosmwasm.wasm.v1beta1.Query/Code".parse().unwrap());
let path = Some("/cosmwasm.wasm.v1.Query/Code".parse().unwrap());
let req = QueryCodeRequest { code_id };
@@ -255,11 +264,7 @@ pub trait CosmWasmClient: rpc::Client {
}
}
async fn get_contracts(&self, code_id: ContractCodeId) -> Result<Vec<AccountId>, NymdError> {
let path = Some(
"/cosmwasm.wasm.v1beta1.Query/ContractsByCode"
.parse()
.unwrap(),
);
let path = Some("/cosmwasm.wasm.v1.Query/ContractsByCode".parse().unwrap());
let mut raw_contracts = Vec::new();
let mut pagination = None;
@@ -290,7 +295,7 @@ pub trait CosmWasmClient: rpc::Client {
}
async fn get_contract(&self, address: &AccountId) -> Result<Contract, NymdError> {
let path = Some("/cosmwasm.wasm.v1beta1.Query/ContractInfo".parse().unwrap());
let path = Some("/cosmwasm.wasm.v1.Query/ContractInfo".parse().unwrap());
let req = QueryContractInfoRequest {
address: address.to_string(),
@@ -315,11 +320,7 @@ pub trait CosmWasmClient: rpc::Client {
&self,
address: &AccountId,
) -> Result<Vec<ContractCodeHistoryEntry>, NymdError> {
let path = Some(
"/cosmwasm.wasm.v1beta1.Query/ContractHistory"
.parse()
.unwrap(),
);
let path = Some("/cosmwasm.wasm.v1.Query/ContractHistory".parse().unwrap());
let mut raw_entries = Vec::new();
let mut pagination = None;
@@ -353,11 +354,7 @@ pub trait CosmWasmClient: rpc::Client {
address: &AccountId,
query_data: Vec<u8>,
) -> Result<Vec<u8>, NymdError> {
let path = Some(
"/cosmwasm.wasm.v1beta1.Query/RawContractState"
.parse()
.unwrap(),
);
let path = Some("/cosmwasm.wasm.v1.Query/RawContractState".parse().unwrap());
let req = QueryRawContractStateRequest {
address: address.to_string(),
@@ -381,7 +378,7 @@ pub trait CosmWasmClient: rpc::Client {
for<'a> T: Deserialize<'a>,
{
let path = Some(
"/cosmwasm.wasm.v1beta1.Query/SmartContractState"
"/cosmwasm.wasm.v1.Query/SmartContractState"
.parse()
.unwrap(),
);
@@ -400,4 +397,27 @@ pub trait CosmWasmClient: rpc::Client {
Ok(serde_json::from_slice(&res.data)?)
}
// deprecation warning is due to the fact the protobuf files built were based on cosmos-sdk 0.44,
// where they prefer using tx_bytes directly. However, in 0.42, which we are using at the time
// of writing this, the option does not work
#[allow(deprecated)]
async fn query_simulate(
&self,
tx: Option<Tx>,
tx_bytes: Vec<u8>,
) -> Result<SimulateResponse, NymdError> {
let path = Some("/cosmos.tx.v1beta1.Service/Simulate".parse().unwrap());
let req = SimulateRequest {
tx: tx.map(Into::into),
tx_bytes,
};
let res = self
.make_abci_query::<_, ProtoSimulateResponse>(path, req)
.await?;
res.try_into()
}
}
@@ -29,7 +29,7 @@ pub(crate) fn find_attribute<'a>(
) -> Option<&'a cosmwasm_std::Attribute> {
logs.iter()
.flat_map(|log| log.events.iter())
.find(|event| event.kind == event_type)?
.find(|event| event.ty == event_type)?
.attributes
.iter()
.find(|attr| attr.key == attribute_key)
@@ -61,7 +61,7 @@ mod tests {
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].msg_index, 0);
assert_eq!(parsed[0].events.len(), 1);
assert_eq!(parsed[0].events[0].kind, "message");
assert_eq!(parsed[0].events[0].ty, "message");
assert_eq!(parsed[0].events[0].attributes[3].key, "code_id");
assert_eq!(parsed[0].events[0].attributes[3].value, "1");
}
@@ -76,12 +76,12 @@ mod tests {
assert_eq!(parsed[2].msg_index, 2);
assert_eq!(parsed[0].events.len(), 1);
assert_eq!(parsed[0].events[0].kind, "message");
assert_eq!(parsed[0].events[0].ty, "message");
assert_eq!(parsed[0].events[0].attributes[3].key, "code_id");
assert_eq!(parsed[0].events[0].attributes[3].value, "9");
assert_eq!(parsed[2].events.len(), 1);
assert_eq!(parsed[2].events[0].kind, "message");
assert_eq!(parsed[2].events[0].ty, "message");
assert_eq!(parsed[2].events[0].attributes[2].key, "signer");
assert_eq!(
parsed[2].events[0].attributes[2].value,
@@ -3,6 +3,7 @@
use crate::nymd::error::NymdError;
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use crate::nymd::GasPrice;
use cosmrs::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl};
use std::convert::TryInto;
@@ -23,9 +24,10 @@ where
pub fn connect_with_signer<U>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: Option<GasPrice>,
) -> Result<signing_client::Client, NymdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
signing_client::Client::connect_with_signer(endpoint, signer)
signing_client::Client::connect_with_signer(endpoint, signer, gas_price)
}
@@ -1,37 +1,100 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::convert::TryInto;
use async_trait::async_trait;
use cosmrs::bank::MsgSend;
use cosmrs::distribution::MsgWithdrawDelegatorReward;
use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl, SimpleRequest};
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
use cosmrs::tx::{self, Msg, SignDoc, SignerInfo};
use cosmrs::{cosmwasm, rpc, AccountId, Any, Coin, Tx};
use log::debug;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use crate::nymd::cosmwasm_client::client::CosmWasmClient;
use crate::nymd::cosmwasm_client::helpers::{compress_wasm_code, CheckResponse};
use crate::nymd::cosmwasm_client::logs::{self, parse_raw_logs};
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 async_trait::async_trait;
use cosmrs::bank::MsgSend;
use cosmrs::distribution::MsgWithdrawDelegatorReward;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::{Error as TendermintRpcError, HttpClient, HttpClientUrl, SimpleRequest};
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
use cosmrs::tx::{Fee, Msg, SignDoc, SignerInfo};
use cosmrs::{cosmwasm, rpc, tx, AccountId, Any, Coin};
use log::debug;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use std::convert::TryInto;
use crate::nymd::{CosmosCoin, GasPrice};
// we need to have **a** valid secp256k1 signature for simulation purposes.
// it doesn't matter what it is as long as it parses correctly
const DUMMY_SECP256K1_SIGNATURE: &[u8] = &[
54, 167, 169, 61, 100, 173, 231, 87, 1, 113, 179, 49, 102, 141, 67, 22, 170, 153, 52, 88, 178,
159, 200, 11, 37, 138, 76, 221, 187, 70, 104, 123, 98, 216, 190, 249, 149, 81, 1, 158, 0, 220,
32, 147, 101, 60, 64, 77, 44, 83, 221, 119, 170, 124, 109, 177, 73, 116, 46, 57, 102, 181, 98,
91,
];
#[async_trait]
pub trait SigningCosmWasmClient: CosmWasmClient {
fn signer(&self) -> &DirectSecp256k1HdWallet;
fn gas_price(&self) -> &GasPrice;
fn signer_public_key(&self, signer_address: &AccountId) -> Option<tx::SignerPublicKey> {
let signer_accounts = self.signer().try_derive_accounts().ok()?;
let account_from_signer = signer_accounts
.iter()
.find(|account| &account.address == signer_address)?;
let public_key = account_from_signer.public_key;
Some(public_key.into())
}
async fn simulate(
&self,
signer_address: &AccountId,
messages: Vec<Any>,
memo: impl Into<String> + Send + 'static,
) -> Result<SimulateResponse, NymdError> {
let public_key = self.signer_public_key(signer_address);
let sequence_response = self.get_sequence(signer_address).await?;
let partial_tx = Tx {
body: tx::Body {
messages,
memo: memo.into(),
timeout_height: 0u32.into(),
extension_options: vec![],
non_critical_extension_options: vec![],
},
auth_info: tx::AuthInfo {
signer_infos: vec![tx::SignerInfo {
public_key,
mode_info: tx::ModeInfo::Single(tx::mode_info::Single {
mode: SignMode::Unspecified,
}),
sequence: sequence_response.sequence,
}],
fee: tx::Fee::from_amount_and_gas(
CosmosCoin {
denom: "".parse().unwrap(),
amount: 0u64.into(),
},
0,
),
},
signatures: vec![DUMMY_SECP256K1_SIGNATURE.try_into().unwrap()],
};
self.query_simulate(Some(partial_tx), Vec::new()).await
}
async fn upload(
&self,
sender_address: &AccountId,
wasm_code: Vec<u8>,
fee: Fee,
memo: impl Into<String> + Send + 'static,
mut meta: Option<UploadMeta>,
) -> Result<UploadResult, NymdError> {
let compressed = compress_wasm_code(&wasm_code)?;
let compressed_size = compressed.len();
@@ -42,14 +105,6 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
let upload_msg = cosmwasm::MsgStoreCode {
sender: sender_address.clone(),
wasm_byte_code: compressed,
source: meta
.as_mut()
.map(|meta| meta.source.take())
.unwrap_or_default(),
builder: meta
.as_mut()
.map(|meta| meta.builder.take())
.unwrap_or_default(),
instantiate_permission: Default::default(),
}
.to_any()
@@ -61,12 +116,13 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.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);
// 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
// fields do not exist or code_id is not a number, there's no way we can recover, we're probably connected
// to wrong validator or something
let code_id = logs::find_attribute(&logs, "message", "code_id")
let code_id = logs::find_attribute(&logs, "store_code", "code_id")
.unwrap()
.value
.parse()
@@ -80,6 +136,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
code_id,
logs,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -111,7 +168,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
// now this is a weird one. the protobuf files say this field is optional,
// but if you omit it, the initialisation will fail CheckTx
label: Some(label),
init_msg: serde_json::to_vec(msg)?,
msg: serde_json::to_vec(msg)?,
funds: options.map(|options| options.funds).unwrap_or_default(),
}
.to_any()
@@ -123,12 +180,13 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.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);
// 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
// fields do not exist or address is malformed, there's no way we can recover, we're probably connected
// to wrong validator or something
let contract_address = logs::find_attribute(&logs, "message", "contract_address")
let contract_address = logs::find_attribute(&logs, "instantiate", "_contract_address")
.unwrap()
.value
.parse()
@@ -138,6 +196,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
contract_address,
logs,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -162,9 +221,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -187,9 +249,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -209,7 +274,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
sender: sender_address.clone(),
contract: contract_address.clone(),
code_id,
migrate_msg: serde_json::to_vec(msg)?,
msg: serde_json::to_vec(msg)?,
}
.to_any()
.map_err(|_| NymdError::SerializationError("MsgMigrateContract".to_owned()))?;
@@ -219,9 +284,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
Ok(MigrateResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -251,9 +319,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -288,14 +359,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.await?
.check_response()?;
debug!(
"gas wanted: {:?}, gas used: {:?}",
tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used
);
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
}
@@ -316,7 +385,36 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
self.sign_and_broadcast_commit(sender_address, vec![send_msg], fee, memo)
.await
.await?
.check_response()
}
async fn send_tokens_multiple<I>(
&self,
sender_address: &AccountId,
msgs: I,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
where
I: IntoIterator<Item = (AccountId, Vec<Coin>)> + Send,
{
let messages = msgs
.into_iter()
.map(|(to_address, amount)| {
MsgSend {
from_address: sender_address.clone(),
to_address,
amount,
}
.to_any()
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))
})
.collect::<Result<_, _>>()?;
self.sign_and_broadcast_commit(sender_address, messages, fee, memo)
.await?
.check_response()
}
async fn delegate_tokens(
@@ -336,7 +434,8 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![delegate_msg], fee, memo)
.await
.await?
.check_response()
}
async fn undelegate_tokens(
@@ -356,7 +455,8 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![undelegate_msg], fee, memo)
.await
.await?
.check_response()
}
async fn withdraw_rewards(
@@ -374,7 +474,45 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgWithdrawDelegatorReward".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![withdraw_msg], fee, memo)
.await
.await?
.check_response()
}
// in this particular case we cannot generalise the argument to `&str` due to lifetime constraints
#[allow(clippy::ptr_arg)]
async fn determine_transaction_fee(
&self,
signer_address: &AccountId,
messages: &[Any],
fee: Fee,
memo: &String,
) -> Result<tx::Fee, NymdError> {
let fee = match fee {
Fee::Manual(fee) => fee,
Fee::Auto(multiplier) => {
debug!("Trying to simulate gas costs...");
// from what I've seen in manual testing, gas estimation does not exist if transaction
// fails to get executed (for example if you send 'BondMixnode" with invalid signature)
let gas_estimation = self
.simulate(signer_address, messages.to_vec(), memo.clone())
.await?
.gas_info
.ok_or(NymdError::GasEstimationFailure)?
.gas_used;
let multiplier = multiplier.unwrap_or(DEFAULT_SIMULATED_GAS_MULTIPLIER);
let gas = ((gas_estimation.value() as f32 * multiplier) as u64).into();
debug!("Gas estimation: {}", gas_estimation);
debug!("Multiplying the estimation by {}", multiplier);
debug!("Final gas limit used: {}", gas);
let fee = self.gas_price() * gas;
tx::Fee::from_amount_and_gas(fee, gas)
}
};
debug!("Fee used for the transaction: {:?}", fee);
Ok(fee)
}
/// Broadcast a transaction, returning immediately.
@@ -385,6 +523,10 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_async::Response, 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()
@@ -401,6 +543,10 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_sync::Response, 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()
@@ -417,6 +563,11 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, 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()
@@ -429,7 +580,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
&self,
signer_address: &AccountId,
messages: Vec<Any>,
fee: Fee,
fee: tx::Fee,
memo: impl Into<String> + Send + 'static,
signer_data: SignerData,
) -> Result<tx::Raw, NymdError> {
@@ -467,7 +618,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
&self,
signer_address: &AccountId,
messages: Vec<Any>,
fee: Fee,
fee: tx::Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<tx::Raw, NymdError> {
// TODO: Future optimisation: rather than grabbing current account_number and sequence
@@ -485,22 +636,28 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Client {
rpc_client: HttpClient,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
}
impl Client {
pub fn connect_with_signer<U>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: Option<GasPrice>,
) -> Result<Self, NymdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let rpc_client = HttpClient::new(endpoint)?;
Ok(Client { rpc_client, signer })
Ok(Client {
rpc_client,
signer,
gas_price: gas_price.unwrap_or_default(),
})
}
}
@@ -522,4 +679,8 @@ impl SigningCosmWasmClient for Client {
fn signer(&self) -> &DirectSecp256k1HdWallet {
&self.signer
}
fn gas_price(&self) -> &GasPrice {
&self.gas_price
}
}
@@ -7,15 +7,19 @@ 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::cosmwasm::wasm::v1beta1::{
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::cosmwasm::wasm::v1::{
CodeInfoResponse, ContractCodeHistoryEntry as ProtoContractCodeHistoryEntry,
ContractCodeHistoryOperationType, ContractInfo as ProtoContractInfo,
};
use cosmrs::tendermint::chain;
use cosmrs::tx::{AccountNumber, SequenceNumber};
use cosmrs::tendermint::{abci, chain};
use cosmrs::tx::{AccountNumber, Gas, SequenceNumber};
use cosmrs::{tx, AccountId, Coin};
use serde::Serialize;
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
pub type ContractCodeId = u64;
@@ -70,18 +74,6 @@ pub struct Code {
/// sha256 hash of the code stored
pub data_hash: Vec<u8>,
/// An URL to a .tar.gz archive of the source code of the contract,
/// which can be used to reproducibly build the Wasm bytecode.
///
/// @see https://github.com/CosmWasm/cosmwasm-verify
pub source: Option<String>,
/// A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
///
/// @example ```cosmwasm/rust-optimizer:0.8.0```
/// @see https://github.com/CosmWasm/cosmwasm-verify
pub builder: Option<String>,
}
impl TryFrom<CodeInfoResponse> for Code {
@@ -92,31 +84,16 @@ impl TryFrom<CodeInfoResponse> for Code {
code_id,
creator,
data_hash,
source,
builder,
} = value;
let creator = creator
.parse()
.map_err(|_| NymdError::MalformedAccountAddress(creator))?;
let source = if source.is_empty() {
None
} else {
Some(source)
};
let builder = if builder.is_empty() {
None
} else {
Some(builder)
};
Ok(Code {
code_id,
creator,
data_hash,
source,
builder,
})
}
}
@@ -242,6 +219,101 @@ impl TryFrom<ProtoContractCodeHistoryEntry> for ContractCodeHistoryEntry {
}
}
#[derive(Debug)]
pub struct GasInfo {
/// GasWanted is the maximum units of work we allow this tx to perform.
pub gas_wanted: Gas,
/// GasUsed is the amount of gas actually consumed.
pub gas_used: Gas,
}
impl From<ProtoGasInfo> for GasInfo {
fn from(value: ProtoGasInfo) -> Self {
GasInfo {
gas_wanted: value.gas_wanted.into(),
gas_used: value.gas_used.into(),
}
}
}
impl GasInfo {
pub fn new(gas_wanted: Gas, gas_used: Gas) -> Self {
GasInfo {
gas_wanted,
gas_used,
}
}
}
#[derive(Debug)]
pub struct AbciResult {
/// Data is any data returned from message or handler execution. It MUST be
/// length prefixed in order to separate data from multiple message executions.
pub data: Vec<u8>,
/// Log contains the log information from message or handler execution.
// todo: try to parse into Log?
pub log: String,
/// Events contains a slice of Event objects that were emitted during message
/// or handler execution.
pub events: Vec<abci::Event>,
}
impl TryFrom<ProtoAbciResult> for AbciResult {
type Error = NymdError;
fn try_from(value: ProtoAbciResult) -> Result<Self, Self::Error> {
let mut events = Vec::with_capacity(value.events.len());
for proto_event in value.events.into_iter() {
let type_str = proto_event.r#type;
let mut attributes = Vec::with_capacity(proto_event.attributes.len());
for proto_attribute in proto_event.attributes.into_iter() {
let stringified_ked = String::from_utf8(proto_attribute.key)
.map_err(|_| NymdError::DeserializationError("EventAttributeKey".to_owned()))?;
let stringified_value = String::from_utf8(proto_attribute.value)
.map_err(|_| NymdError::DeserializationError("EventAttributeKey".to_owned()))?;
attributes.push(abci::tag::Tag {
key: stringified_ked.parse().unwrap(),
value: stringified_value.parse().unwrap(),
})
}
events.push(abci::Event {
type_str,
attributes,
})
}
Ok(AbciResult {
data: value.data,
log: value.log,
events,
})
}
}
#[derive(Debug)]
pub struct SimulateResponse {
pub gas_info: Option<GasInfo>,
pub result: Option<AbciResult>,
}
impl TryFrom<ProtoSimulateResponse> for SimulateResponse {
type Error = NymdError;
fn try_from(value: ProtoSimulateResponse) -> Result<Self, Self::Error> {
Ok(SimulateResponse {
gas_info: value.gas_info.map(|gas_info| gas_info.into()),
result: value.result.map(|result| result.try_into()).transpose()?,
})
}
}
// ##############################################################################
// types specific to the signing client (perhaps they should go to separate file)
// ##############################################################################
@@ -254,21 +326,6 @@ pub struct SignerData {
pub chain_id: chain::Id,
}
#[derive(Debug)]
pub struct UploadMeta {
/// An URL to a .tar.gz archive of the source code of the contract,
/// which can be used to reproducibly build the Wasm bytecode.
///
/// @see https://github.com/CosmWasm/cosmwasm-verify
pub source: Option<String>,
/// A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
///
/// @example ```cosmwasm/rust-optimizer:0.8.0```
/// @see https://github.com/CosmWasm/cosmwasm-verify
pub builder: Option<String>,
}
#[derive(Debug)]
pub struct UploadResult {
/// Size of the original wasm code in bytes
@@ -290,6 +347,8 @@ pub struct UploadResult {
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
pub gas_info: GasInfo,
}
#[derive(Debug)]
@@ -316,6 +375,8 @@ pub struct InstantiateResult {
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
pub gas_info: GasInfo,
}
#[derive(Debug)]
@@ -324,6 +385,8 @@ pub struct ChangeAdminResult {
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
pub gas_info: GasInfo,
}
#[derive(Debug)]
@@ -332,6 +395,8 @@ pub struct MigrateResult {
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
pub gas_info: GasInfo,
}
#[derive(Debug)]
@@ -340,4 +405,6 @@ pub struct ExecuteResult {
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
pub gas_info: GasInfo,
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::nymd::cosmwasm_client::types::ContractCodeId;
use cosmrs::tendermint::block;
use cosmrs::tendermint::{abci, block};
use cosmrs::{bip32, tx, AccountId};
use std::io;
use thiserror::Error;
@@ -102,6 +102,12 @@ pub enum NymdError {
#[error("The provided gas price is malformed")]
MalformedGasPrice,
#[error("Failed to estimate gas price for the transaction")]
GasEstimationFailure,
#[error("Abci query failed with code {0} - {1}")]
AbciError(u32, abci::Log),
}
impl NymdError {
@@ -38,7 +38,7 @@ impl<'a> Mul<Gas> for &'a GasPrice {
// however, realistically that is impossible to happen as the resultant value
// would have to be way higher than our token limit of 10^15 (1 billion of tokens * 1 million for denomination)
// and max value of u128 is approximately 10^38
if limit_uint128.u128() * gas_price_numerator > amount.u128() * gas_price_denominator {
if limit_uint128 * gas_price_numerator > amount * gas_price_denominator {
amount += Uint128::new(1);
}
@@ -17,17 +17,29 @@ pub enum Operation {
Send,
BondMixnode,
BondMixnodeOnBehalf,
UnbondMixnode,
UnbondMixnodeOnBehalf,
DelegateToMixnode,
DelegateToMixnodeOnBehalf,
UndelegateFromMixnode,
UndelegateFromMixnodeOnBehalf,
BondGateway,
BondGatewayOnBehalf,
UnbondGateway,
UnbondGatewayOnBehalf,
UpdateStateParams,
UpdateContractSettings,
BeginMixnodeRewarding,
FinishMixnodeRewarding,
TrackUnbondGateway,
TrackUnbondMixnode,
WithdrawVestedCoins,
TrackUndelegation,
CreatePeriodicVestingAccount,
}
pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin {
@@ -43,14 +55,27 @@ impl fmt::Display for Operation {
Operation::ChangeAdmin => f.write_str("ChangeAdmin"),
Operation::Send => f.write_str("Send"),
Operation::BondMixnode => f.write_str("BondMixnode"),
Operation::BondMixnodeOnBehalf => f.write_str("BondMixnodeOnBehalf"),
Operation::UnbondMixnode => f.write_str("UnbondMixnode"),
Operation::UnbondMixnodeOnBehalf => f.write_str("UnbondMixnodeOnBehalf"),
Operation::BondGateway => f.write_str("BondGateway"),
Operation::BondGatewayOnBehalf => f.write_str("BondGatewayOnBehalf"),
Operation::UnbondGateway => f.write_str("UnbondGateway"),
Operation::UnbondGatewayOnBehalf => f.write_str("UnbondGatewayOnBehalf"),
Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"),
Operation::DelegateToMixnodeOnBehalf => f.write_str("DelegateToMixnodeOnBehalf"),
Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"),
Operation::UpdateStateParams => f.write_str("UpdateStateParams"),
Operation::UndelegateFromMixnodeOnBehalf => {
f.write_str("UndelegateFromMixnodeOnBehalf")
}
Operation::UpdateContractSettings => f.write_str("UpdateContractSettings"),
Operation::BeginMixnodeRewarding => f.write_str("BeginMixnodeRewarding"),
Operation::FinishMixnodeRewarding => f.write_str("FinishMixnodeRewarding"),
Operation::TrackUnbondGateway => f.write_str("TrackUnbondGateway"),
Operation::TrackUnbondMixnode => f.write_str("TrackUnbondMixnode"),
Operation::WithdrawVestedCoins => f.write_str("WithdrawVestedCoins"),
Operation::TrackUndelegation => f.write_str("TrackUndelegation"),
Operation::CreatePeriodicVestingAccount => f.write_str("CreatePeriodicVestingAccount"),
}
}
}
@@ -59,23 +84,34 @@ impl Operation {
// TODO: some value tweaking
pub fn default_gas_limit(&self) -> Gas {
match self {
Operation::Upload => 2_500_000u64.into(),
Operation::Upload => 3_000_000u64.into(),
Operation::Init => 500_000u64.into(),
Operation::Migrate => 200_000u64.into(),
Operation::ChangeAdmin => 80_000u64.into(),
Operation::Send => 80_000u64.into(),
Operation::BondMixnode => 175_000u64.into(),
Operation::BondMixnodeOnBehalf => 200_000u64.into(),
Operation::UnbondMixnode => 175_000u64.into(),
Operation::UnbondMixnodeOnBehalf => 175_000u64.into(),
Operation::DelegateToMixnode => 175_000u64.into(),
Operation::DelegateToMixnodeOnBehalf => 175_000u64.into(),
Operation::UndelegateFromMixnode => 175_000u64.into(),
Operation::UndelegateFromMixnodeOnBehalf => 175_000u64.into(),
Operation::BondGateway => 175_000u64.into(),
Operation::BondGatewayOnBehalf => 200_000u64.into(),
Operation::UnbondGateway => 175_000u64.into(),
Operation::UnbondGatewayOnBehalf => 200_000u64.into(),
Operation::UpdateStateParams => 175_000u64.into(),
Operation::UpdateContractSettings => 175_000u64.into(),
Operation::BeginMixnodeRewarding => 175_000u64.into(),
Operation::FinishMixnodeRewarding => 175_000u64.into(),
Operation::TrackUnbondGateway => 175_000u64.into(),
Operation::TrackUnbondMixnode => 175_000u64.into(),
Operation::WithdrawVestedCoins => 175_000u64.into(),
Operation::TrackUndelegation => 175_000u64.into(),
Operation::CreatePeriodicVestingAccount => 175_000u64.into(),
}
}
@@ -89,9 +125,8 @@ impl Operation {
Fee::from_amount_and_gas(fee, gas_limit)
}
pub(crate) fn determine_fee(&self, gas_price: &GasPrice, gas_limit: Option<Gas>) -> Fee {
let gas_limit = gas_limit.unwrap_or_else(|| self.default_gas_limit());
Self::determine_custom_fee(gas_price, gas_limit)
pub fn default_fee(&self, gas_price: &GasPrice) -> Fee {
Self::determine_custom_fee(gas_price, self.default_gas_limit())
}
}
@@ -0,0 +1,33 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmrs::tx;
pub mod gas_price;
pub mod helpers;
pub const DEFAULT_SIMULATED_GAS_MULTIPLIER: f32 = 1.3;
#[derive(Debug, Clone)]
pub enum Fee {
Manual(tx::Fee),
Auto(Option<f32>),
}
impl From<tx::Fee> for Fee {
fn from(fee: tx::Fee) -> Self {
Fee::Manual(fee)
}
}
impl From<f32> for Fee {
fn from(multiplier: f32) -> Self {
Fee::Auto(Some(multiplier))
}
}
impl Default for Fee {
fn default() -> Self {
Fee::Auto(Some(DEFAULT_SIMULATED_GAS_MULTIPLIER))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod vesting_query_client;
mod vesting_signing_client;
pub use vesting_query_client::VestingQueryClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -0,0 +1,176 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
use crate::nymd::error::NymdError;
use crate::nymd::NymdClient;
use async_trait::async_trait;
use cosmwasm_std::{Coin, Timestamp};
use vesting_contract::messages::QueryMsg as VestingQueryMsg;
#[async_trait]
pub trait VestingQueryClient {
async fn locked_coins(
&self,
address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn spendable_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn vested_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn vesting_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn vesting_start_time(
&self,
vesting_account_address: &str,
) -> Result<Timestamp, NymdError>;
async fn vesting_end_time(&self, vesting_account_address: &str)
-> Result<Timestamp, NymdError>;
async fn original_vesting(&self, vesting_account_address: &str) -> Result<Coin, NymdError>;
async fn delegated_free(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
async fn delegated_vesting(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError>;
}
#[async_trait]
impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
async fn locked_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::LockedCoins {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn spendable_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::SpendableCoins {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn vested_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::GetVestedCoins {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn vesting_coins(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::GetVestingCoins {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn vesting_start_time(
&self,
vesting_account_address: &str,
) -> Result<Timestamp, NymdError> {
let request = VestingQueryMsg::GetStartTime {
vesting_account_address: vesting_account_address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn vesting_end_time(
&self,
vesting_account_address: &str,
) -> Result<Timestamp, NymdError> {
let request = VestingQueryMsg::GetEndTime {
vesting_account_address: vesting_account_address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn original_vesting(&self, vesting_account_address: &str) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::GetOriginalVesting {
vesting_account_address: vesting_account_address.to_string(),
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn delegated_free(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::GetDelegatedFree {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
async fn delegated_vesting(
&self,
vesting_account_address: &str,
block_time: Option<Timestamp>,
) -> Result<Coin, NymdError> {
let request = VestingQueryMsg::GetDelegatedVesting {
vesting_account_address: vesting_account_address.to_string(),
block_time,
};
self.client
.query_contract_smart(self.vesting_contract_address()?, &request)
.await
}
}
@@ -0,0 +1,294 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
use crate::nymd::cosmwasm_client::types::ExecuteResult;
use crate::nymd::error::NymdError;
use crate::nymd::fee::helpers::Operation;
use crate::nymd::{cosmwasm_coin_to_cosmos_coin, NymdClient};
use async_trait::async_trait;
use cosmwasm_std::Coin;
use mixnet_contract::{Gateway, IdentityKey, IdentityKeyRef, MixNode};
use vesting_contract::messages::ExecuteMsg as VestingExecuteMsg;
#[async_trait]
pub trait VestingSigningClient {
async fn vesting_bond_gateway(
&self,
gateway: Gateway,
owner_signature: &str,
pledge: Coin,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_unbond_gateway(&self) -> Result<ExecuteResult, NymdError>;
async fn vesting_track_unbond_gateway(
&self,
owner: &str,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_bond_mixnode(
&self,
mix_node: MixNode,
owner_signature: &str,
pledge: Coin,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_unbond_mixnode(&self) -> Result<ExecuteResult, NymdError>;
async fn vesting_track_unbond_mixnode(
&self,
owner: &str,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
async fn withdraw_vested_coins(&self, amount: Coin) -> Result<ExecuteResult, NymdError>;
async fn vesting_track_undelegation(
&self,
address: &str,
mix_identity: IdentityKey,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_delegate_to_mixnode<'a>(
&self,
mix_identity: IdentityKeyRef<'a>,
amount: &Coin,
) -> Result<ExecuteResult, NymdError>;
async fn vesting_undelegate_from_mixnode<'a>(
&self,
mix_identity: IdentityKeyRef<'a>,
) -> Result<ExecuteResult, NymdError>;
async fn create_periodic_vesting_account(
&self,
address: &str,
start_time: Option<u64>,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
}
#[async_trait]
impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient<C> {
async fn vesting_bond_gateway(
&self,
gateway: Gateway,
owner_signature: &str,
pledge: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::BondGateway);
let req = VestingExecuteMsg::BondGateway {
gateway,
owner_signature: owner_signature.to_string(),
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::BondGateway",
vec![cosmwasm_coin_to_cosmos_coin(pledge)],
)
.await
}
async fn vesting_unbond_gateway(&self) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::UnbondGateway);
let req = VestingExecuteMsg::UnbondGateway {};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::UnbondGateway",
vec![],
)
.await
}
async fn vesting_track_unbond_gateway(
&self,
owner: &str,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::TrackUnbondGateway);
let req = VestingExecuteMsg::TrackUnbondGateway {
owner: owner.to_string(),
amount,
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::TrackUnbondGateway",
vec![],
)
.await
}
async fn vesting_bond_mixnode(
&self,
mix_node: MixNode,
owner_signature: &str,
pledge: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::BondMixnode);
let req = VestingExecuteMsg::BondMixnode {
mix_node,
owner_signature: owner_signature.to_string(),
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::BondMixnode",
vec![cosmwasm_coin_to_cosmos_coin(pledge)],
)
.await
}
async fn vesting_unbond_mixnode(&self) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::UnbondMixnode);
let req = VestingExecuteMsg::UnbondMixnode {};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::UnbondMixnode",
vec![],
)
.await
}
async fn vesting_track_unbond_mixnode(
&self,
owner: &str,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::TrackUnbondMixnode);
let req = VestingExecuteMsg::TrackUnbondMixnode {
owner: owner.to_string(),
amount,
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::TrackUnbondMixnode",
vec![],
)
.await
}
async fn withdraw_vested_coins(&self, amount: Coin) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::WithdrawVestedCoins);
let req = VestingExecuteMsg::WithdrawVestedCoins { amount };
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::WithdrawVested",
vec![],
)
.await
}
async fn vesting_track_undelegation(
&self,
address: &str,
mix_identity: IdentityKey,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::TrackUndelegation);
let req = VestingExecuteMsg::TrackUndelegation {
owner: address.to_string(),
mix_identity,
amount,
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::TrackUndelegation",
vec![],
)
.await
}
async fn vesting_delegate_to_mixnode<'a>(
&self,
mix_identity: IdentityKeyRef<'a>,
amount: &Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::DelegateToMixnode);
let req = VestingExecuteMsg::DelegateToMixnode {
mix_identity: mix_identity.into(),
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::DeledateToMixnode",
vec![cosmwasm_coin_to_cosmos_coin(amount.to_owned())],
)
.await
}
async fn vesting_undelegate_from_mixnode<'a>(
&self,
mix_identity: IdentityKeyRef<'a>,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::UndelegateFromMixnode);
let req = VestingExecuteMsg::UndelegateFromMixnode {
mix_identity: mix_identity.into(),
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::UndelegateFromMixnode",
vec![],
)
.await
}
async fn create_periodic_vesting_account(
&self,
address: &str,
start_time: Option<u64>,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::CreatePeriodicVestingAccount);
let req = VestingExecuteMsg::CreateAccount {
address: address.to_string(),
start_time,
};
self.client
.execute(
self.address(),
self.vesting_contract_address()?,
&req,
fee,
"VestingContract::CreatePeriodicVestingAccount",
vec![cosmwasm_coin_to_cosmos_coin(amount)],
)
.await
}
}
@@ -10,7 +10,7 @@ use cosmrs::tx::SignDoc;
use cosmrs::{tx, AccountId};
/// Derivation information required to derive a keypair and an address from a mnemonic.
#[derive(Debug)]
#[derive(Debug, Clone)]
struct Secp256k1Derivation {
hd_path: DerivationPath,
prefix: String,
@@ -40,7 +40,7 @@ impl AccountData {
type Secp256k1Keypair = (SigningKey, PublicKey);
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DirectSecp256k1HdWallet {
/// Base secret
secret: bip39::Mnemonic,
+1 -1
View File
@@ -8,4 +8,4 @@ description = "Crutch library until there is proper SerDe support for coconut st
serde = { version = "1.0", features = ["derive"] }
getset = "0.1.1"
coconut-rs = { git = "https://github.com/nymtech/coconut.git", branch = "0.5.0" }
nymcoconut = {path = "../nymcoconut" }
+6 -6
View File
@@ -4,7 +4,7 @@
use getset::{CopyGetters, Getters};
use serde::{Deserialize, Serialize};
pub use coconut_rs::*;
pub use nymcoconut::*;
#[derive(Serialize, Deserialize, Getters, CopyGetters, Clone)]
pub struct Credential {
@@ -42,7 +42,7 @@ impl Credential {
.iter()
.map(hash_to_scalar)
.collect::<Vec<Attribute>>();
coconut_rs::verify_credential(&params, verification_key, &self.theta, &public_attributes)
nymcoconut::verify_credential(&params, verification_key, &self.theta, &public_attributes)
}
}
@@ -84,7 +84,7 @@ pub struct BlindSignRequestBody {
#[getset(get = "pub")]
blind_sign_request: BlindSignRequest,
#[getset(get = "pub")]
public_key: coconut_rs::PublicKey,
public_key: nymcoconut::PublicKey,
public_attributes: Vec<String>,
#[getset(get = "pub")]
total_params: u32,
@@ -92,13 +92,13 @@ pub struct BlindSignRequestBody {
impl BlindSignRequestBody {
pub fn new(
blind_sign_request: BlindSignRequest,
public_key: &coconut_rs::PublicKey,
blind_sign_request: &BlindSignRequest,
public_key: &nymcoconut::PublicKey,
public_attributes: &[Attribute],
total_params: u32,
) -> BlindSignRequestBody {
BlindSignRequestBody {
blind_sign_request,
blind_sign_request: blind_sign_request.clone(),
public_key: public_key.clone(),
public_attributes: public_attributes
.iter()
+44 -14
View File
@@ -6,41 +6,71 @@
// right now this has no double-spending protection, spender binding, etc
// it's the simplest possible case
use coconut_interface::{
Credential, Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
};
use network_defaults::BANDWIDTH_VALUE;
use url::Url;
use super::utils::{obtain_aggregate_signature, prepare_credential_for_spending};
use crate::error::Error;
use coconut_interface::{hash_to_scalar, Credential, Parameters, Signature, VerificationKey};
use network_defaults::BANDWIDTH_VALUE;
pub const PUBLIC_ATTRIBUTES: u32 = 1;
pub const PRIVATE_ATTRIBUTES: u32 = 1;
use super::utils::{obtain_aggregate_signature, prepare_credential_for_spending};
pub const PUBLIC_ATTRIBUTES: u32 = 2;
pub const PRIVATE_ATTRIBUTES: u32 = 2;
pub const TOTAL_ATTRIBUTES: u32 = PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES;
pub struct BandwidthVoucherAttributes {
// a random secret value generated by the client used for double-spending detection
pub serial_number: PrivateAttribute,
// a random secret value generated by the client used to bind multiple credentials together
pub binding_number: PrivateAttribute,
// the value (e.g., bandwidth) encoded in this voucher
pub voucher_value: PublicAttribute,
// a field with public information, e.g., type of voucher, epoch etc.
pub voucher_info: PublicAttribute,
}
impl BandwidthVoucherAttributes {
pub fn get_public_attributes(&self) -> Vec<PublicAttribute> {
vec![self.voucher_value, self.voucher_info]
}
pub fn get_private_attributes(&self) -> Vec<PrivateAttribute> {
vec![self.serial_number, self.binding_number]
}
}
// TODO: this definitely has to be moved somewhere else. It's just a temporary solution
pub async fn obtain_signature(raw_identity: &[u8], validators: &[Url]) -> Result<Signature, Error> {
let public_attributes = vec![hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes())];
let private_attributes = vec![hash_to_scalar(raw_identity)];
pub async fn obtain_signature(
params: &Parameters,
attributes: &BandwidthVoucherAttributes,
validators: &[Url],
) -> Result<Signature, Error> {
let public_attributes = attributes.get_public_attributes();
let private_attributes = attributes.get_private_attributes();
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
obtain_aggregate_signature(&params, &public_attributes, &private_attributes, validators).await
obtain_aggregate_signature(params, &public_attributes, &private_attributes, validators).await
}
pub fn prepare_for_spending(
raw_identity: &[u8],
signature: &Signature,
attributes: &BandwidthVoucherAttributes,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let public_attributes = vec![BANDWIDTH_VALUE.to_be_bytes().to_vec()];
let private_attributes = vec![raw_identity.to_vec()];
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,
private_attributes,
attributes.serial_number,
attributes.binding_number,
signature,
verification_key,
)
+68 -19
View File
@@ -1,14 +1,16 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use coconut_interface::{
aggregate_signature_shares, aggregate_verification_keys, hash_to_scalar, prepare_blind_sign,
prove_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
aggregate_signature_shares, aggregate_verification_keys, prepare_blind_sign,
prove_bandwidth_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
SignatureShare, VerificationKey,
};
use url::Url;
use crate::coconut::bandwidth::PRIVATE_ATTRIBUTES;
use crate::error::Error;
/// Contacts all provided validators and then aggregate their verification keys.
///
/// # Arguments
@@ -63,17 +65,18 @@ async fn obtain_partial_credential(
public_attributes: &[Attribute],
private_attributes: &[Attribute],
client: &validator_client::ApiClient,
validator_vk: &VerificationKey,
) -> Result<Signature, Error> {
let elgamal_keypair = coconut_interface::elgamal_keygen(params);
let blind_sign_request = prepare_blind_sign(
params,
elgamal_keypair.public_key(),
&elgamal_keypair,
private_attributes,
public_attributes,
)?;
let blind_sign_request_body = BlindSignRequestBody::new(
blind_sign_request,
&blind_sign_request,
elgamal_keypair.public_key(),
public_attributes,
(public_attributes.len() + private_attributes.len()) as u32,
@@ -83,7 +86,17 @@ async fn obtain_partial_credential(
.blind_sign(&blind_sign_request_body)
.await?
.blinded_signature;
Ok(blinded_signature.unblind(elgamal_keypair.private_key()))
let unblinded_signature = blinded_signature.unblind(
params,
elgamal_keypair.private_key(),
validator_vk,
private_attributes,
public_attributes,
&blind_sign_request.get_commitment_hash(),
)?;
Ok(unblinded_signature)
}
pub async fn obtain_aggregate_signature(
@@ -97,40 +110,76 @@ pub async fn obtain_aggregate_signature(
}
let mut shares = Vec::with_capacity(validators.len());
let mut validators_partial_vks: Vec<VerificationKey> = Vec::with_capacity(validators.len());
let mut client = validator_client::ApiClient::new(validators[0].clone());
let first =
obtain_partial_credential(params, public_attributes, private_attributes, &client).await?;
let validator_partial_vk = client.get_coconut_verification_key().await?;
validators_partial_vks.push(validator_partial_vk.key.clone());
let first = obtain_partial_credential(
params,
public_attributes,
private_attributes,
&client,
&validator_partial_vk.key,
)
.await?;
shares.push(SignatureShare::new(first, 0));
for (id, validator_url) in validators.iter().enumerate().skip(1) {
client.change_validator_api(validator_url.clone());
let signature =
obtain_partial_credential(params, public_attributes, private_attributes, &client)
.await?;
let validator_partial_vk = client.get_coconut_verification_key().await?;
validators_partial_vks.push(validator_partial_vk.key.clone());
let signature = obtain_partial_credential(
params,
public_attributes,
private_attributes,
&client,
&validator_partial_vk.key,
)
.await?;
let share = SignatureShare::new(signature, id as u64);
shares.push(share)
}
Ok(aggregate_signature_shares(&shares)?)
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(private_attributes);
attributes.extend_from_slice(public_attributes);
let mut indices: Vec<u64> = Vec::with_capacity(validators_partial_vks.len());
for i in 1..validators_partial_vks.len() {
indices.push(i as u64);
}
let verification_key =
aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?;
Ok(aggregate_signature_shares(
params,
&verification_key,
&attributes,
&shares,
)?)
}
// TODO: better type flow
pub fn prepare_credential_for_spending(
params: &Parameters,
public_attributes: Vec<Vec<u8>>,
private_attributes: Vec<Vec<u8>>,
serial_number: Attribute,
binding_number: Attribute,
signature: &Signature,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let private_attributes = private_attributes
.iter()
.map(hash_to_scalar)
.collect::<Vec<Attribute>>();
let theta = prove_credential(params, verification_key, signature, &private_attributes)?;
let theta = prove_bandwidth_credential(
params,
verification_key,
signature,
serial_number,
binding_number,
)?;
Ok(Credential::new(
(public_attributes.len() + private_attributes.len()) as u32,
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
theta,
public_attributes,
signature,
+27 -18
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ed25519_dalek::ed25519::signature::Signature as SignatureTrait;
pub use ed25519_dalek::ed25519::signature::Signature as SignatureTrait;
pub use ed25519_dalek::SignatureError;
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nymsphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
@@ -10,33 +10,33 @@ use rand::{CryptoRng, RngCore};
use std::fmt::{self, Display, Formatter};
#[derive(Debug)]
pub enum KeyRecoveryError {
pub enum Ed25519RecoveryError {
MalformedBytes(SignatureError),
MalformedString(bs58::decode::Error),
}
impl From<SignatureError> for KeyRecoveryError {
impl From<SignatureError> for Ed25519RecoveryError {
fn from(err: SignatureError) -> Self {
KeyRecoveryError::MalformedBytes(err)
Ed25519RecoveryError::MalformedBytes(err)
}
}
impl From<bs58::decode::Error> for KeyRecoveryError {
impl From<bs58::decode::Error> for Ed25519RecoveryError {
fn from(err: bs58::decode::Error) -> Self {
KeyRecoveryError::MalformedString(err)
Ed25519RecoveryError::MalformedString(err)
}
}
impl fmt::Display for KeyRecoveryError {
impl fmt::Display for Ed25519RecoveryError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
KeyRecoveryError::MalformedBytes(err) => write!(f, "malformed bytes - {}", err),
KeyRecoveryError::MalformedString(err) => write!(f, "malformed string - {}", err),
Ed25519RecoveryError::MalformedBytes(err) => write!(f, "malformed bytes - {}", err),
Ed25519RecoveryError::MalformedString(err) => write!(f, "malformed string - {}", err),
}
}
}
impl std::error::Error for KeyRecoveryError {}
impl std::error::Error for Ed25519RecoveryError {}
/// Keypair for usage in ed25519 EdDSA.
pub struct KeyPair {
@@ -62,7 +62,7 @@ impl KeyPair {
&self.public_key
}
pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result<Self, KeyRecoveryError> {
pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(KeyPair {
private_key: PrivateKey::from_bytes(priv_bytes)?,
public_key: PublicKey::from_bytes(pub_bytes)?,
@@ -115,7 +115,7 @@ impl PublicKey {
self.0.to_bytes()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, KeyRecoveryError> {
pub fn from_bytes(b: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(PublicKey(ed25519_dalek::PublicKey::from_bytes(b)?))
}
@@ -123,7 +123,7 @@ impl PublicKey {
bs58::encode(self.to_bytes()).into_string()
}
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, KeyRecoveryError> {
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, Ed25519RecoveryError> {
let bytes = bs58::decode(val).into_vec()?;
Self::from_bytes(&bytes)
}
@@ -134,7 +134,7 @@ impl PublicKey {
}
impl PemStorableKey for PublicKey {
type Error = KeyRecoveryError;
type Error = Ed25519RecoveryError;
fn pem_type() -> &'static str {
"ED25519 PUBLIC KEY"
@@ -170,7 +170,7 @@ impl PrivateKey {
self.0.to_bytes()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, KeyRecoveryError> {
pub fn from_bytes(b: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(PrivateKey(ed25519_dalek::SecretKey::from_bytes(b)?))
}
@@ -178,7 +178,7 @@ impl PrivateKey {
bs58::encode(&self.to_bytes()).into_string()
}
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, KeyRecoveryError> {
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, Ed25519RecoveryError> {
let bytes = bs58::decode(val).into_vec()?;
Self::from_bytes(&bytes)
}
@@ -192,7 +192,7 @@ impl PrivateKey {
}
impl PemStorableKey for PrivateKey {
type Error = KeyRecoveryError;
type Error = Ed25519RecoveryError;
fn pem_type() -> &'static str {
"ED25519 PRIVATE KEY"
@@ -211,11 +211,20 @@ impl PemStorableKey for PrivateKey {
pub struct Signature(ed25519_dalek::Signature);
impl Signature {
pub fn to_base58_string(&self) -> String {
bs58::encode(&self.to_bytes()).into_string()
}
pub fn from_base58_string<I: AsRef<[u8]>>(val: I) -> Result<Self, Ed25519RecoveryError> {
let bytes = bs58::decode(val).into_vec()?;
Self::from_bytes(&bytes)
}
pub fn to_bytes(&self) -> [u8; SIGNATURE_LENGTH] {
self.0.to_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignatureError> {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Ed25519RecoveryError> {
Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?))
}
}
+2 -4
View File
@@ -7,9 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch = "0.14.1-updatedk256" }
#cosmwasm-std = { version = "0.14.1" }
cosmwasm-std = "1.0.0-beta2"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
@@ -17,7 +15,7 @@ schemars = "0.8"
ts-rs = { version = "5.1", optional = true }
thiserror = "1.0"
network-defaults = { path = "../network-defaults" }
fixed = "1.1"
fixed = { version = "1.1", features = ["serde"] }
az = "1.1"
log = "0.4.14"
+52 -64
View File
@@ -1,3 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
@@ -7,51 +10,41 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct UnpackedDelegation<T> {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct Delegation {
pub owner: Addr,
pub node_identity: IdentityKey,
pub delegation_data: T,
}
impl<T> UnpackedDelegation<T> {
pub fn new(owner: Addr, node_identity: IdentityKey, delegation_data: T) -> Self {
UnpackedDelegation {
owner,
node_identity,
delegation_data,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct RawDelegationData {
pub amount: Uint128,
pub amount: Coin,
pub block_height: u64,
}
impl RawDelegationData {
pub fn new(amount: Uint128, block_height: u64) -> Self {
RawDelegationData {
amount,
block_height,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct Delegation {
owner: Addr,
amount: Coin,
block_height: u64,
pub proxy: Option<Addr>, // proxy address used to delegate the funds on behalf of anouther address
}
impl Delegation {
pub fn new(owner: Addr, amount: Coin, block_height: u64) -> Self {
pub fn new(
owner: Addr,
node_identity: IdentityKey,
amount: Coin,
block_height: u64,
proxy: Option<Addr>,
) -> Self {
Delegation {
owner,
node_identity,
amount,
block_height,
proxy,
}
}
// TODO: change that to use .joined_key() and return Vec<u8>
pub fn storage_key(&self) -> (IdentityKey, Addr) {
(self.node_identity(), self.owner())
}
pub fn increment_amount(&mut self, amount: Uint128, at_height: Option<u64>) {
self.amount.amount += amount;
if let Some(at_height) = at_height {
self.block_height = at_height;
}
}
@@ -59,6 +52,10 @@ impl Delegation {
&self.amount
}
pub fn node_identity(&self) -> IdentityKey {
self.node_identity.clone()
}
pub fn owner(&self) -> Addr {
self.owner.clone()
}
@@ -72,65 +69,56 @@ impl Display for Delegation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} {} delegated by {} at block {}",
self.amount.amount, self.amount.denom, self.owner, self.block_height
"{} delegated towards {} by {} at block {}",
self.amount, self.node_identity, self.owner, self.block_height
)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedMixDelegationsResponse {
pub node_identity: IdentityKey,
pub delegations: Vec<Delegation>,
pub start_next_after: Option<Addr>,
pub start_next_after: Option<String>,
}
impl PagedMixDelegationsResponse {
pub fn new(
node_identity: IdentityKey,
delegations: Vec<Delegation>,
start_next_after: Option<Addr>,
) -> Self {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<Addr>) -> Self {
PagedMixDelegationsResponse {
node_identity,
delegations,
start_next_after,
start_next_after: start_next_after.map(|s| s.to_string()),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedReverseMixDelegationsResponse {
pub delegation_owner: Addr,
pub delegated_nodes: Vec<IdentityKey>,
pub struct PagedDelegatorDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<IdentityKey>,
}
impl PagedReverseMixDelegationsResponse {
pub fn new(
delegation_owner: Addr,
delegated_nodes: Vec<IdentityKey>,
start_next_after: Option<IdentityKey>,
) -> Self {
PagedReverseMixDelegationsResponse {
delegation_owner,
delegated_nodes,
impl PagedDelegatorDelegationsResponse {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<IdentityKey>) -> Self {
PagedDelegatorDelegationsResponse {
delegations,
start_next_after,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedAllDelegationsResponse<T> {
pub delegations: Vec<UnpackedDelegation<T>>,
pub start_next_after: Option<Vec<u8>>,
pub struct PagedAllDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<(IdentityKey, String)>,
}
impl<T> PagedAllDelegationsResponse<T> {
pub fn new(delegations: Vec<UnpackedDelegation<T>>, start_next_after: Option<Vec<u8>>) -> Self {
impl PagedAllDelegationsResponse {
pub fn new(
delegations: Vec<Delegation>,
start_next_after: Option<(IdentityKey, Addr)>,
) -> Self {
PagedAllDelegationsResponse {
delegations,
start_next_after,
start_next_after: start_next_after.map(|(id, addr)| (id, addr.to_string())),
}
}
}
+35 -19
View File
@@ -23,19 +23,27 @@ pub struct Gateway {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct GatewayBond {
pub bond_amount: Coin,
pub pledge_amount: Coin,
pub owner: Addr,
pub block_height: u64,
pub gateway: Gateway,
pub proxy: Option<Addr>,
}
impl GatewayBond {
pub fn new(bond_amount: Coin, owner: Addr, block_height: u64, gateway: Gateway) -> Self {
pub fn new(
pledge_amount: Coin,
owner: Addr,
block_height: u64,
gateway: Gateway,
proxy: Option<Addr>,
) -> Self {
GatewayBond {
bond_amount,
pledge_amount,
owner,
block_height,
gateway,
proxy,
}
}
@@ -43,8 +51,8 @@ impl GatewayBond {
&self.gateway.identity_key
}
pub fn bond_amount(&self) -> Coin {
self.bond_amount.clone()
pub fn pledge_amount(&self) -> Coin {
self.pledge_amount.clone()
}
pub fn owner(&self) -> &Addr {
@@ -59,17 +67,17 @@ impl GatewayBond {
impl PartialOrd for GatewayBond {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// first remove invalid cases
if self.bond_amount.denom != other.bond_amount.denom {
if self.pledge_amount.denom != other.pledge_amount.denom {
return None;
}
// try to order by total bond
let bond_cmp = self
.bond_amount
// try to order by total pledge
let pledge_cmp = self
.pledge_amount
.amount
.partial_cmp(&other.bond_amount.amount)?;
if bond_cmp != Ordering::Equal {
return Some(bond_cmp);
.partial_cmp(&other.pledge_amount.amount)?;
if pledge_cmp != Ordering::Equal {
return Some(pledge_cmp);
}
// then check block height
@@ -94,7 +102,10 @@ impl Display for GatewayBond {
write!(
f,
"amount: {} {}, owner: {}, identity: {}",
self.bond_amount.amount, self.bond_amount.denom, self.owner, self.gateway.identity_key
self.pledge_amount.amount,
self.pledge_amount.denom,
self.owner,
self.gateway.identity_key
)
}
}
@@ -123,7 +134,7 @@ impl PagedGatewayResponse {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct GatewayOwnershipResponse {
pub address: Addr,
pub has_gateway: bool,
pub gateway: Option<GatewayBond>,
}
#[cfg(test)]
@@ -150,38 +161,43 @@ mod tests {
let _0foos = Coin::new(0, "foo");
let gate1 = GatewayBond {
bond_amount: _150foos.clone(),
pledge_amount: _150foos.clone(),
owner: Addr::unchecked("foo1"),
block_height: 100,
gateway: gateway_fixture(),
proxy: None,
};
let gate2 = GatewayBond {
bond_amount: _150foos,
pledge_amount: _150foos,
owner: Addr::unchecked("foo2"),
block_height: 120,
gateway: gateway_fixture(),
proxy: None,
};
let gate3 = GatewayBond {
bond_amount: _50foos,
pledge_amount: _50foos,
owner: Addr::unchecked("foo3"),
block_height: 120,
gateway: gateway_fixture(),
proxy: None,
};
let gate4 = GatewayBond {
bond_amount: _140foos,
pledge_amount: _140foos,
owner: Addr::unchecked("foo4"),
block_height: 120,
gateway: gateway_fixture(),
proxy: None,
};
let gate5 = GatewayBond {
bond_amount: _0foos,
pledge_amount: _0foos,
owner: Addr::unchecked("foo5"),
block_height: 120,
gateway: gateway_fixture(),
proxy: None,
};
// summary:
+6 -7
View File
@@ -8,15 +8,14 @@ pub mod mixnode;
mod msg;
mod types;
pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250;
pub use cosmwasm_std::{Addr, Coin};
pub use delegation::{
Delegation, PagedAllDelegationsResponse, PagedMixDelegationsResponse,
PagedReverseMixDelegationsResponse, RawDelegationData, UnpackedDelegation,
Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
PagedMixDelegationsResponse,
};
pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse};
pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse};
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::{
IdentityKey, IdentityKeyRef, LayerDistribution, RewardingIntervalResponse, SphinxKey,
StateParams,
};
pub use msg::*;
pub use types::*;
+191 -81
View File
@@ -5,7 +5,7 @@ use crate::{IdentityKey, SphinxKey};
use az::CheckedCast;
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use log::error;
use network_defaults::{DEFAULT_OPERATOR_EPOCH_COST, DEFAULT_PROFIT_MARGIN};
use network_defaults::DEFAULT_OPERATOR_EPOCH_COST;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -14,6 +14,10 @@ use std::fmt::Display;
type U128 = fixed::types::U75F53; // u128 with 18 significant digits
fixed::const_fixed_from_int! {
const ONE: U128 = 1;
}
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)]
pub struct MixNode {
@@ -25,6 +29,7 @@ pub struct MixNode {
/// Base58 encoded ed25519 EdDSA public key.
pub identity_key: IdentityKey,
pub version: String,
pub profit_margin_percent: u8,
}
#[derive(
@@ -51,32 +56,68 @@ pub enum Layer {
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeRewardParams {
period_reward_pool: Uint128,
k: Uint128,
rewarded_set_size: Uint128,
active_set_size: Uint128,
reward_blockstamp: u64,
circulating_supply: Uint128,
uptime: Uint128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
}
impl NodeRewardParams {
#[allow(clippy::too_many_arguments)]
pub fn new(
period_reward_pool: u128,
k: u128,
rewarded_set_size: u128,
active_set_size: u128,
reward_blockstamp: u64,
circulating_supply: u128,
uptime: u128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
) -> NodeRewardParams {
NodeRewardParams {
period_reward_pool: Uint128(period_reward_pool),
k: Uint128(k),
period_reward_pool: Uint128::new(period_reward_pool),
rewarded_set_size: Uint128::new(rewarded_set_size),
active_set_size: Uint128::new(active_set_size),
reward_blockstamp,
circulating_supply: Uint128(circulating_supply),
uptime: Uint128(uptime),
circulating_supply: Uint128::new(circulating_supply),
uptime: Uint128::new(uptime),
sybil_resistance_percent,
in_active_set,
active_set_work_factor,
}
}
pub fn omega(&self) -> U128 {
// As per keybase://chat/nymtech#tokeneconomics/1179
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 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()
} else {
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
ONE / denom * self.rewarded_set_size()
}
}
pub fn idle_nodes(&self) -> Uint128 {
self.rewarded_set_size - self.active_set_size
}
pub fn active_set_work_factor(&self) -> U128 {
U128::from_num(self.active_set_work_factor)
}
pub fn in_active_set(&self) -> bool {
self.in_active_set
}
pub fn performance(&self) -> U128 {
U128::from_num(self.uptime.u128()) / U128::from_num(100)
}
@@ -93,8 +134,8 @@ impl NodeRewardParams {
self.period_reward_pool.u128()
}
pub fn k(&self) -> u128 {
self.k.u128()
pub fn rewarded_set_size(&self) -> u128 {
self.rewarded_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
@@ -110,7 +151,7 @@ impl NodeRewardParams {
}
pub fn one_over_k(&self) -> U128 {
U128::from_num(1) / U128::from_num(self.k.u128())
ONE / U128::from_num(self.rewarded_set_size.u128())
}
pub fn alpha(&self) -> U128 {
@@ -118,6 +159,90 @@ impl NodeRewardParams {
}
}
// cosmwasm's limited serde doesn't work with U128 directly
#[allow(non_snake_case)]
pub mod fixed_U128_as_string {
use super::U128;
use serde::de::Error;
use serde::Deserialize;
use std::str::FromStr;
pub fn serialize<S>(val: &U128, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = (*val).to_string();
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<U128, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
U128::from_str(&s).map_err(|err| {
D::Error::custom(format!(
"failed to deserialize U128 with its string representation - {}",
err
))
})
}
}
// everything required to reward delegator of given mixnode
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct DelegatorRewardParams {
node_reward_params: NodeRewardParams,
// to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]`
// for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128
#[schemars(with = "String")]
#[serde(with = "fixed_U128_as_string")]
sigma: U128,
#[schemars(with = "String")]
#[serde(with = "fixed_U128_as_string")]
profit_margin: U128,
#[schemars(with = "String")]
#[serde(with = "fixed_U128_as_string")]
node_profit: U128,
}
impl DelegatorRewardParams {
pub fn new(mixnode_bond: &MixNodeBond, node_reward_params: NodeRewardParams) -> Self {
DelegatorRewardParams {
sigma: mixnode_bond.sigma(&node_reward_params),
profit_margin: mixnode_bond.profit_margin(),
node_profit: mixnode_bond.node_profit(&node_reward_params),
node_reward_params,
}
}
pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 {
// change all values into their fixed representations
let delegation_amount = U128::from_num(delegation_amount.u128());
let circulating_supply = U128::from_num(self.node_reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward =
(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() {
int_reward
} else {
error!(
"Could not cast delegator reward ({}) to u128, returning 0",
reward,
);
0u128
}
}
pub fn node_reward_params(&self) -> &NodeRewardParams {
&self.node_reward_params
}
}
#[derive(Debug)]
pub struct NodeRewardResult {
reward: U128,
@@ -141,46 +266,45 @@ impl NodeRewardResult {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixNodeBond {
pub bond_amount: Coin,
pub pledge_amount: Coin,
pub total_delegation: Coin,
pub owner: Addr,
pub layer: Layer,
pub block_height: u64,
pub mix_node: MixNode,
pub profit_margin_percent: Option<u8>,
pub proxy: Option<Addr>,
}
impl MixNodeBond {
pub fn new(
bond_amount: Coin,
pledge_amount: Coin,
owner: Addr,
layer: Layer,
block_height: u64,
mix_node: MixNode,
profit_margin_percent: Option<u8>,
proxy: Option<Addr>,
) -> Self {
MixNodeBond {
total_delegation: coin(0, &bond_amount.denom),
bond_amount,
total_delegation: coin(0, &pledge_amount.denom),
pledge_amount,
owner,
layer,
block_height,
mix_node,
profit_margin_percent,
proxy,
}
}
pub fn profit_margin(&self) -> U128 {
U128::from_num(self.profit_margin_percent.unwrap_or(DEFAULT_PROFIT_MARGIN))
/ U128::from_num(100)
U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100)
}
pub fn identity(&self) -> &String {
&self.mix_node.identity_key
}
pub fn bond_amount(&self) -> Coin {
self.bond_amount.clone()
pub fn pledge_amount(&self) -> Coin {
self.pledge_amount.clone()
}
pub fn owner(&self) -> &Addr {
@@ -192,10 +316,10 @@ impl MixNodeBond {
}
pub fn total_stake(&self) -> Option<u128> {
if self.bond_amount.denom != self.total_delegation.denom {
if self.pledge_amount.denom != self.total_delegation.denom {
None
} else {
Some(self.bond_amount.amount.u128() + self.total_delegation.amount.u128())
Some(self.pledge_amount.amount.u128() + self.total_delegation.amount.u128())
}
}
@@ -203,39 +327,38 @@ impl MixNodeBond {
self.total_delegation.clone()
}
pub fn bond_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.bond_amount().amount.u128()) / U128::from_num(circulating_supply)
pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply)
}
pub fn total_stake_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.bond_amount().amount.u128() + self.total_delegation().amount.u128())
pub fn total_bond_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.pledge_amount().amount.u128() + self.total_delegation().amount.u128())
/ U128::from_num(circulating_supply)
}
pub fn lambda(&self, params: &NodeRewardParams) -> U128 {
// Ratio of a bond to the token circulating supply
let bond_to_circulating_supply_ratio =
self.bond_to_circulating_supply(params.circulating_supply());
bond_to_circulating_supply_ratio.min(params.one_over_k())
let pledge_to_circulating_supply_ratio =
self.pledge_to_circulating_supply(params.circulating_supply());
pledge_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn sigma(&self, params: &NodeRewardParams) -> U128 {
// Ratio of a delegation to the the token circulating supply
let total_stake_to_circulating_supply_ratio =
self.total_stake_to_circulating_supply(params.circulating_supply());
total_stake_to_circulating_supply_ratio.min(params.one_over_k())
let total_bond_to_circulating_supply_ratio =
self.total_bond_to_circulating_supply(params.circulating_supply());
total_bond_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult {
// Assuming uniform work distribution across the network this is one_over_k * k
let omega_k = U128::from_num(1u128);
let lambda = self.lambda(params);
let sigma = self.sigma(params);
let reward = params.performance()
* params.period_reward_pool()
* (sigma * omega_k + params.alpha() * lambda * sigma * params.k())
/ (U128::from_num(1) + params.alpha());
* (sigma * params.omega()
+ params.alpha() * lambda * sigma * params.rewarded_set_size())
/ (ONE + params.alpha());
NodeRewardResult {
reward,
@@ -261,7 +384,7 @@ impl MixNodeBond {
};
let operator_base_reward = reward.reward.min(params.operator_cost());
let operator_reward = (self.profit_margin()
+ (U128::from_num(1) - self.profit_margin()) * reward.lambda / reward.sigma)
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
* profit;
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0));
@@ -279,67 +402,50 @@ impl MixNodeBond {
}
pub fn sigma_ratio(&self, params: &NodeRewardParams) -> U128 {
if self.total_stake_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
if self.total_bond_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
{
self.total_stake_to_circulating_supply(params.circulating_supply())
self.total_bond_to_circulating_supply(params.circulating_supply())
} else {
params.one_over_k()
}
}
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 {
let scaled_delegation_amount =
U128::from_num(delegation_amount.u128()) / U128::from_num(params.circulating_supply());
let delegator_reward = (U128::from_num(1) - self.profit_margin())
* scaled_delegation_amount
/ self.sigma(params)
* self.node_profit(params);
let reward = delegator_reward.max(U128::from_num(0));
if let Some(int_reward) = reward.checked_cast() {
int_reward
} else {
error!(
"Could not cast delegator reward ({}) to u128, returning 0 - mixnode {}",
reward,
self.identity()
);
0u128
}
let reward_params = DelegatorRewardParams::new(self, *params);
reward_params.determine_delegation_reward(delegation_amount)
}
}
impl PartialOrd for MixNodeBond {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// first remove invalid cases
if self.bond_amount.denom != self.total_delegation.denom {
if self.pledge_amount.denom != self.total_delegation.denom {
return None;
}
if other.bond_amount.denom != other.total_delegation.denom {
if other.pledge_amount.denom != other.total_delegation.denom {
return None;
}
if self.bond_amount.denom != other.bond_amount.denom {
if self.pledge_amount.denom != other.pledge_amount.denom {
return None;
}
// try to order by total bond + delegation
let total_cmp = (self.bond_amount.amount + self.total_delegation.amount)
.partial_cmp(&(self.bond_amount.amount + self.total_delegation.amount))?;
let total_cmp = (self.pledge_amount.amount + self.total_delegation.amount)
.partial_cmp(&(self.pledge_amount.amount + self.total_delegation.amount))?;
if total_cmp != Ordering::Equal {
return Some(total_cmp);
}
// then if those are equal, prefer higher bond over delegation
let bond_cmp = self
.bond_amount
let pledge_cmp = self
.pledge_amount
.amount
.partial_cmp(&other.bond_amount.amount)?;
if bond_cmp != Ordering::Equal {
return Some(bond_cmp);
.partial_cmp(&other.pledge_amount.amount)?;
if pledge_cmp != Ordering::Equal {
return Some(pledge_cmp);
}
// then look at delegation (I'm not sure we can get here, but better safe than sorry)
@@ -378,7 +484,10 @@ impl Display for MixNodeBond {
write!(
f,
"amount: {} {}, owner: {}, identity: {}",
self.bond_amount.amount, self.bond_amount.denom, self.owner, self.mix_node.identity_key
self.pledge_amount.amount,
self.pledge_amount.denom,
self.owner,
self.mix_node.identity_key
)
}
}
@@ -407,7 +516,7 @@ impl PagedMixnodeResponse {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixOwnershipResponse {
pub address: Addr,
pub has_node: bool,
pub mixnode: Option<MixNodeBond>,
}
#[cfg(test)]
@@ -423,6 +532,7 @@ mod tests {
sphinx_key: "sphinxkey".to_string(),
identity_key: "identitykey".to_string(),
version: "0.11.0".to_string(),
profit_margin_percent: 10,
}
}
@@ -433,53 +543,53 @@ mod tests {
let _0foos = Coin::new(0, "foo");
let mix1 = MixNodeBond {
bond_amount: _150foos.clone(),
pledge_amount: _150foos.clone(),
total_delegation: _50foos.clone(),
owner: Addr::unchecked("foo1"),
layer: Layer::One,
block_height: 100,
mix_node: mixnode_fixture(),
profit_margin_percent: Some(10),
proxy: None,
};
let mix2 = MixNodeBond {
bond_amount: _150foos.clone(),
pledge_amount: _150foos.clone(),
total_delegation: _50foos.clone(),
owner: Addr::unchecked("foo2"),
layer: Layer::One,
block_height: 120,
mix_node: mixnode_fixture(),
profit_margin_percent: Some(10),
proxy: None,
};
let mix3 = MixNodeBond {
bond_amount: _50foos,
pledge_amount: _50foos,
total_delegation: _150foos.clone(),
owner: Addr::unchecked("foo3"),
layer: Layer::One,
block_height: 120,
mix_node: mixnode_fixture(),
profit_margin_percent: Some(10),
proxy: None,
};
let mix4 = MixNodeBond {
bond_amount: _150foos.clone(),
pledge_amount: _150foos.clone(),
total_delegation: _0foos.clone(),
owner: Addr::unchecked("foo4"),
layer: Layer::One,
block_height: 120,
mix_node: mixnode_fixture(),
profit_margin_percent: Some(10),
proxy: None,
};
let mix5 = MixNodeBond {
bond_amount: _0foos,
pledge_amount: _0foos,
total_delegation: _150foos,
owner: Addr::unchecked("foo5"),
layer: Layer::One,
block_height: 120,
mix_node: mixnode_fixture(),
profit_margin_percent: Some(10),
proxy: None,
};
// summary:
+63 -26
View File
@@ -2,27 +2,30 @@
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::NodeRewardParams;
use crate::StateParams;
use crate::ContractStateParams;
use crate::{Gateway, IdentityKey, MixNode};
use cosmwasm_std::Addr;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {}
pub struct InstantiateMsg {
pub rewarding_validator_address: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
BondMixnode {
mix_node: MixNode,
owner_signature: String,
},
UnbondMixnode {},
BondGateway {
gateway: Gateway,
owner_signature: String,
},
UnbondGateway {},
UpdateStateParams(StateParams),
UpdateContractStateParams(ContractStateParams),
DelegateToMixnode {
mix_identity: IdentityKey,
@@ -37,21 +40,12 @@ pub enum ExecuteMsg {
rewarding_interval_nonce: u32,
},
RewardMixnode {
identity: IdentityKey,
// percentage value in range 0-100
uptime: u32,
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
FinishMixnodeRewarding {
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
RewardMixnodeV2 {
RewardMixnode {
identity: IdentityKey,
// percentage value in range 0-100
params: NodeRewardParams,
@@ -59,11 +53,41 @@ pub enum ExecuteMsg {
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
RewardNextMixDelegators {
mix_identity: IdentityKey,
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
DelegateToMixnodeOnBehalf {
mix_identity: IdentityKey,
delegate: String,
},
UndelegateFromMixnodeOnBehalf {
mix_identity: IdentityKey,
delegate: String,
},
BondMixnodeOnBehalf {
mix_node: MixNode,
owner: String,
owner_signature: String,
},
UnbondMixnodeOnBehalf {
owner: String,
},
BondGatewayOnBehalf {
gateway: Gateway,
owner: String,
owner_signature: String,
},
UnbondGatewayOnBehalf {
owner: String,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetContractVersion {},
GetMixNodes {
limit: Option<u32>,
start_after: Option<IdentityKey>,
@@ -73,36 +97,49 @@ pub enum QueryMsg {
limit: Option<u32>,
},
OwnsMixnode {
address: Addr,
address: String,
},
OwnsGateway {
address: Addr,
address: String,
},
StateParams {},
CurrentRewardingInterval {},
GetMixDelegations {
// gets all [paged] delegations in the entire network
// TODO: do we even want that?
GetAllNetworkDelegations {
start_after: Option<(IdentityKey, String)>,
limit: Option<u32>,
},
// gets all [paged] delegations associated with particular mixnode
GetMixnodeDelegations {
mix_identity: IdentityKey,
start_after: Option<Addr>,
// since `start_after` is user-provided input, we can't use `Addr` as we
// can't guarantee it's validated.
start_after: Option<String>,
limit: Option<u32>,
},
GetAllMixDelegations {
start_after: Option<Vec<u8>>,
limit: Option<u32>,
},
GetReverseMixDelegations {
delegation_owner: Addr,
// gets all [paged] delegations associated with particular delegator
GetDelegatorDelegations {
// since `delegator` is user-provided input, we can't use `Addr` as we
// can't guarantee it's validated.
delegator: String,
start_after: Option<IdentityKey>,
limit: Option<u32>,
},
GetMixDelegation {
// gets delegation associated with particular mixnode, delegator pair
GetDelegationDetails {
mix_identity: IdentityKey,
address: Addr,
delegator: String,
},
LayerDistribution {},
GetRewardPool {},
GetCirculatingSupply {},
GetEpochRewardPercent {},
GetSybilResistancePercent {},
GetRewardingStatus {
mix_identity: IdentityKey,
rewarding_interval_nonce: u32,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+68 -17
View File
@@ -1,8 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::DelegatorRewardParams;
use crate::Layer;
use cosmwasm_std::{Decimal, Uint128};
use cosmwasm_std::{Addr, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
@@ -34,14 +35,13 @@ pub struct RewardingIntervalResponse {
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StateParams {
pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours
pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system
pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system
pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
pub struct ContractStateParams {
// so currently epoch_length is being unused and validator API performs rewarding
// based on its own epoch length config value. I guess that's fine for time being
// however, in the future, the contract constant should be controlling it instead.
// pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours
pub minimum_mixnode_pledge: Uint128, // minimum amount a mixnode must pledge to get into the system
pub minimum_gateway_pledge: Uint128, // minimum amount a gateway must pledge to get into the system
// number of mixnode that are going to get rewarded during current rewarding interval (k_m)
// based on overall demand for private bandwidth-
@@ -50,23 +50,21 @@ pub struct StateParams {
// subset of rewarded mixnodes that are actively receiving mix traffic
// used to handle shorter-term (e.g. hourly) fluctuations of demand
pub mixnode_active_set_size: u32,
pub active_set_work_factor: u8,
}
impl Display for StateParams {
impl Display for ContractStateParams {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Contract state parameters: [ ")?;
write!(f, "epoch length: {}; ", self.epoch_length)?;
write!(f, "minimum mixnode bond: {}; ", self.minimum_mixnode_bond)?;
write!(f, "minimum gateway bond: {}; ", self.minimum_gateway_bond)?;
write!(
f,
"mixnode bond reward rate: {}; ",
self.mixnode_bond_reward_rate
"minimum mixnode pledge: {}; ",
self.minimum_mixnode_pledge
)?;
write!(
f,
"mixnode delegation reward rate: {}; ",
self.mixnode_delegation_reward_rate
"minimum gateway pledge: {}; ",
self.minimum_gateway_pledge
)?;
write!(
f,
@@ -77,10 +75,63 @@ impl Display for StateParams {
f,
"mixnode active set size: {}",
self.mixnode_active_set_size
)?;
write!(
f,
"mixnode active set work factor: {}",
self.active_set_work_factor
)
}
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct RewardingResult {
pub operator_reward: Uint128,
pub total_delegator_reward: Uint128,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PendingDelegatorRewarding {
// keep track of the running rewarding results so we'd known how much was the operator and its delegators rewarded
pub running_results: RewardingResult,
pub next_start: Addr,
pub rewarding_params: DelegatorRewardParams,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum RewardingStatus {
Complete(RewardingResult),
PendingNextDelegatorPage(PendingDelegatorRewarding),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MixnodeRewardingStatusResponse {
pub status: Option<RewardingStatus>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MixnetContractVersion {
// VERGEN_BUILD_TIMESTAMP
pub build_timestamp: String,
// VERGEN_BUILD_SEMVER
pub build_version: String,
// VERGEN_GIT_SHA
pub commit_sha: String,
// VERGEN_GIT_COMMIT_TIMESTAMP
pub commit_timestamp: String,
// VERGEN_GIT_BRANCH
pub commit_branch: String,
// VERGEN_RUSTC_SEMVER
pub rustc_version: String,
}
// type aliases for better reasoning about available data
pub type IdentityKey = String;
pub type IdentityKeyRef<'a> = &'a str;
+1
View File
@@ -63,6 +63,7 @@ pub fn default_api_endpoints() -> Vec<Url> {
}
pub const DEFAULT_MIXNET_CONTRACT_ADDRESS: &str = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen";
pub const DEFAULT_VESTING_CONTRACT_ADDRESS: &str = "";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "punk1v9qauwdq5terag6uvfsdytcs2d0sdmfdy7hgk3";
/// How much bandwidth (in bytes) one token can buy
+43
View File
@@ -0,0 +1,43 @@
[package]
name = "nymcoconut"
version = "0.5.0"
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>", "Ania Piotrowska <ania@nymtech.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
itertools = "0.10"
digest = "0.9"
rand = "0.8"
thiserror = "1.0"
serde = "1.0"
serde_derive = "1.0"
bs58 = "0.4.0"
sha2 = "0.9"
[dependencies.ff]
version = "0.10"
default-features = false
[dependencies.group]
version = "0.10"
default-features = false
[dev-dependencies]
criterion = { version="0.3", features=["html_reports"] }
doc-comment = "0.3"
[dev-dependencies.bincode]
version = "1"
#[[bench]]
#name = "benchmarks"
#harness = false
[features]
default = []
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
getrandom = { version="0.2", features=["js"] }
+322
View File
@@ -0,0 +1,322 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::ops::{Deref, Mul};
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G1Projective, Scalar};
use group::Curve;
use serde_derive::{Deserialize, Serialize};
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::traits::{Base58, Bytable};
use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar};
/// Type alias for the ephemeral key generated during ElGamal encryption
pub type EphemeralKey = Scalar;
/// Two G1 points representing ElGamal ciphertext
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective);
impl TryFrom<&[u8]> for Ciphertext {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Ciphertext> {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"Ciphertext must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
let c1 = try_deserialize_g1_projective(
c1_bytes,
CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()),
)?;
let c2 = try_deserialize_g1_projective(
c2_bytes,
CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()),
)?;
Ok(Ciphertext(c1, c2))
}
}
impl Ciphertext {
pub fn c1(&self) -> &G1Projective {
&self.0
}
pub fn c2(&self) -> &G1Projective {
&self.1
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Ciphertext> {
Ciphertext::try_from(bytes)
}
}
/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PrivateKey(pub(crate) Scalar);
impl PrivateKey {
/// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve
/// that represents original h^m.
pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective {
let (c1, c2) = &(ciphertext.0, ciphertext.1);
// (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d
c2 - c1 * self.0
}
pub fn public_key(&self, params: &Parameters) -> PublicKey {
PublicKey(params.gen1() * self.0)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
pub fn from_bytes(bytes: &[u8; 32]) -> Result<PrivateKey> {
try_deserialize_scalar(
bytes,
CoconutError::Deserialization(
"Failed to deserialize ElGamal private key - it was not in the canonical form"
.to_string(),
),
)
.map(PrivateKey)
}
}
impl Bytable for PrivateKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
PrivateKey::from_bytes(slice.try_into().unwrap())
}
}
impl Base58 for PrivateKey {}
// TODO: perhaps be more explicit and apart from gamma also store generator and group order?
/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PublicKey(G1Projective);
impl PublicKey {
/// Encrypt encrypts the given message in the form of h^m,
/// where h is a point on the G1 curve using the given public key.
/// The random k is returned alongside the encryption
/// as it is required by the Coconut Scheme to create proofs of knowledge.
pub fn encrypt(
&self,
params: &Parameters,
h: &G1Projective,
msg: &Scalar,
) -> (Ciphertext, EphemeralKey) {
let k = params.random_scalar();
// c1 = g1^k
let c1 = params.gen1() * k;
// c2 = gamma^k * h^m
let c2 = self.0 * k + h * msg;
(Ciphertext(c1, c2), k)
}
pub fn to_bytes(&self) -> [u8; 48] {
self.to_byte_vec().try_into().unwrap()
}
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
Ok(PublicKey::try_from(bytes.to_vec().as_slice()).unwrap())
}
}
impl Bytable for PublicKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.0.to_affine().to_compressed().into()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Ok(PublicKey::from_bytes(slice.try_into().unwrap()).unwrap())
}
}
impl TryFrom<&[u8]> for PublicKey {
type Error = CoconutError;
fn try_from(slice: &[u8]) -> Result<PublicKey> {
try_deserialize_g1_projective(
slice.try_into().unwrap(),
CoconutError::Deserialization(
"Failed to deserialize compressed ElGamal public key".to_string(),
),
)
.map(PublicKey)
}
}
impl Base58 for PublicKey {}
impl Deref for PublicKey {
type Target = G1Projective;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a, 'b> Mul<&'b Scalar> for &'a PublicKey {
type Output = G1Projective;
fn mul(self, rhs: &'b Scalar) -> Self::Output {
self.0 * rhs
}
}
#[derive(Serialize, Deserialize)]
/// A convenient wrapper for both keys of the ElGamal keypair
pub struct ElGamalKeyPair {
private_key: PrivateKey,
public_key: PublicKey,
}
impl ElGamalKeyPair {
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
pub fn private_key(&self) -> &PrivateKey {
&self.private_key
}
}
/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters]
pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
let private_key = params.random_scalar();
let gamma = params.gen1() * private_key;
ElGamalKeyPair {
private_key: PrivateKey(private_key),
public_key: PublicKey(gamma),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keygen() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let expected = params.gen1() * keypair.private_key.0;
let gamma = keypair.public_key.0;
assert_eq!(
expected, gamma,
"Public key, gamma, should be equal to g1^d, where d is the private key"
);
}
#[test]
fn encryption() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(&params, &h, &m);
let expected_c1 = params.gen1() * ephemeral_key;
assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k");
let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m;
assert_eq!(
expected_c2, ciphertext.1,
"c2 should be equal to gamma^k * h^m"
);
}
#[test]
fn decryption() {
let params = Parameters::default();
let keypair = super::elgamal_keygen(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, _) = keypair.public_key.encrypt(&params, &h, &m);
let dec = keypair.private_key.decrypt(&ciphertext);
let expected = h * m;
assert_eq!(
expected, dec,
"after ElGamal decryption, original h^m should be obtained"
);
}
#[test]
fn private_key_bytes_roundtrip() {
let params = Parameters::default();
let private_key = PrivateKey(params.random_scalar());
let bytes = private_key.to_bytes();
// also make sure it is equivalent to the internal scalar's bytes
assert_eq!(private_key.0.to_bytes(), bytes);
assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap())
}
#[test]
fn public_key_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let public_key = PublicKey(params.gen1() * r);
let bytes = public_key.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes
assert_eq!(public_key.0.to_affine().to_compressed(), bytes);
assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap())
}
#[test]
fn ciphertext_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s);
let bytes = ciphertext.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
ciphertext.0.to_affine().to_compressed(),
ciphertext.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap())
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
/// A `Result` alias where the `Err` case is `coconut_rs::Error`.
pub type Result<T> = std::result::Result<T, CoconutError>;
#[derive(Error, Debug)]
pub enum CoconutError {
#[error("Setup error: {0}")]
Setup(String),
#[error("encountered error during keygen")]
Keygen,
#[error("Issuance related error: {0}")]
Issuance(String),
#[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)]
IssuanceMaxAttributes { max: usize, requested: usize },
#[error("Interpolation error: {0}")]
Interpolation(String),
#[error("Aggregation error: {0}")]
Aggregation(String),
#[error("Unblind error: {0}")]
Unblind(String),
#[error("Verification error: {0}")]
Verification(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error(
"Deserailization error, expected at least {} bytes, got {}",
min,
actual
)]
DeserializationMinLength { min: usize, actual: usize },
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")]
DeserializationInvalidLength {
actual: usize,
target: usize,
modulus_target: usize,
modulus: usize,
object: String,
},
}
+15
View File
@@ -0,0 +1,15 @@
use crate::{BlindSignRequest, BlindedSignature, Bytable, Theta};
macro_rules! impl_clone {
($struct:ident) => {
impl Clone for $struct {
fn clone(&self) -> Self {
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
}
}
};
}
impl_clone!(BlindSignRequest);
impl_clone!(BlindedSignature);
impl_clone!(Theta);
+2
View File
@@ -0,0 +1,2 @@
mod clone;
mod serde;
+56
View File
@@ -0,0 +1,56 @@
use crate::elgamal::PrivateKey;
use crate::scheme::SecretKey;
use crate::{
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, Theta, VerificationKey,
};
use serde::de::Unexpected;
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
macro_rules! impl_serde {
($struct:ident, $visitor:ident) => {
pub struct $visitor {}
impl Serialize for $struct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_bs58())
}
}
impl<'de> Visitor<'de> for $visitor {
type Value = $struct;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "A base58 encoded struct")
}
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
match $struct::try_from_bs58(s) {
Ok(x) => Ok(x),
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
}
}
}
impl<'de> Deserialize<'de> for $struct {
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str($visitor {})
}
}
};
}
impl_serde!(SecretKey, V1);
impl_serde!(VerificationKey, V2);
impl_serde!(PublicKey, V3);
impl_serde!(PrivateKey, V4);
impl_serde!(BlindSignRequest, V5);
impl_serde!(BlindedSignature, V6);
impl_serde!(Signature, V7);
impl_serde!(Theta, V8);
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::convert::TryInto;
use bls12_381::Scalar;
pub use elgamal::elgamal_keygen;
pub use elgamal::ElGamalKeyPair;
pub use elgamal::PublicKey;
pub use error::CoconutError;
pub use scheme::aggregation::aggregate_signature_shares;
pub use scheme::aggregation::aggregate_verification_keys;
pub use scheme::issuance::blind_sign;
pub use scheme::issuance::prepare_blind_sign;
pub use scheme::issuance::BlindSignRequest;
pub use scheme::keygen::ttp_keygen;
pub use scheme::keygen::KeyPair;
pub use scheme::keygen::VerificationKey;
pub use scheme::setup::setup;
pub use scheme::setup::Parameters;
pub use scheme::verification::prove_bandwidth_credential;
pub use scheme::verification::verify_credential;
pub use scheme::verification::Theta;
pub use scheme::BlindedSignature;
pub use scheme::Signature;
pub use scheme::SignatureShare;
pub use traits::Base58;
pub use utils::hash_to_scalar;
use crate::traits::Bytable;
pub mod elgamal;
mod error;
mod impls;
mod proofs;
mod scheme;
#[cfg(test)]
mod tests;
mod traits;
mod utils;
pub type Attribute = Scalar;
pub type PrivateAttribute = Attribute;
pub type PublicAttribute = Attribute;
impl Bytable for Attribute {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap())
}
}
impl Base58 for Attribute {}
+663
View File
@@ -0,0 +1,663 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: look at https://crates.io/crates/merlin to perhaps use it instead?
use std::borrow::Borrow;
use std::convert::TryInto;
use bls12_381::{G1Projective, G2Projective, Scalar};
use digest::generic_array::typenum::Unsigned;
use digest::Digest;
use group::GroupEncoding;
use itertools::izip;
use sha2::Sha256;
use crate::elgamal::Ciphertext;
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::scheme::VerificationKey;
use crate::utils::{hash_g1, try_deserialize_scalar, try_deserialize_scalar_vec};
use crate::{elgamal, Attribute, ElGamalKeyPair};
// as per the reference python implementation
type ChallengeDigest = Sha256;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ProofCmCs {
challenge: Scalar,
response_opening: Scalar,
response_private_elgamal_key: Scalar,
response_keys: Vec<Scalar>,
response_attributes: Vec<Scalar>,
}
// note: this is slightly different from the reference python implementation
// as we omit the unnecessary string conversion. Instead we concatenate byte
// representations together and hash that.
// note2: G1 and G2 elements are using their compressed representations
// and as per the bls12-381 library all elements are using big-endian form
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
fn compute_challenge<D, I, B>(iter: I) -> Scalar
where
D: Digest,
I: Iterator<Item = B>,
B: AsRef<[u8]>,
{
let mut h = D::new();
for point_representation in iter {
h.update(point_representation);
}
let digest = h.finalize();
// TODO: I don't like the 0 padding here (though it's what we've been using before,
// but we never had a security audit anyway...)
// instead we could maybe use the `from_bytes` variant and adding some suffix
// when computing the digest until we produce a valid scalar.
let mut bytes = [0u8; 64];
let pad_size = 64usize
.checked_sub(D::OutputSize::to_usize())
.unwrap_or_default();
bytes[pad_size..].copy_from_slice(&digest);
Scalar::from_bytes_wide(&bytes)
}
fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
witness - challenge * secret
}
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
where
S: Borrow<Scalar>,
{
debug_assert_eq!(witnesses.len(), secrets.len());
witnesses
.iter()
.zip(secrets.iter())
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
.collect()
}
impl ProofCmCs {
/// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment
/// using the Fiat-Shamir heuristic.
pub(crate) fn construct(
params: &Parameters,
elgamal_keypair: &ElGamalKeyPair,
ephemeral_keys: &[elgamal::EphemeralKey],
commitment: &G1Projective,
commitment_opening: &Scalar,
private_attributes: &[Attribute],
priv_attributes_ciphertexts: &[Ciphertext],
) -> Self {
// note: this is only called from `prepare_blind_sign` that already checks
// whether private attributes are non-empty and whether we don't have too many
// attributes in total to sign.
// we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len()
// witness creation
let witness_commitment_opening = params.random_scalar();
let witness_private_elgamal_key = params.random_scalar();
let witness_keys = params.n_random_scalars(ephemeral_keys.len());
let witness_attributes = params.n_random_scalars(private_attributes.len());
// recompute h
let h = hash_g1(commitment.to_bytes());
let hs_bytes = params
.gen_hs()
.iter()
.map(|h| h.to_bytes())
.collect::<Vec<_>>();
let g1 = params.gen1();
// compute commitments
let commitment_private_key_elgamal = g1 * witness_private_elgamal_key;
// Aw[i] = (wk[i] * g1)
let commitment_keys1_bytes = witness_keys
.iter()
.map(|wk_i| g1 * wk_i)
.map(|witness| witness.to_bytes())
.collect::<Vec<_>>();
// Bw[i] = (wm[i] * h) + (wk[i] * gamma)
let commitment_keys2_bytes = witness_keys
.iter()
.zip(witness_attributes.iter())
.map(|(wk_i, wm_i)| elgamal_keypair.public_key() * wk_i + h * wm_i)
.map(|witness| witness.to_bytes())
.collect::<Vec<_>>();
// zkp commitment for the attributes commitment cm
// Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i])
let commitment_attributes = g1 * witness_commitment_opening
+ witness_attributes
.iter()
.zip(params.gen_hs().iter())
.map(|(wm_i, hs_i)| hs_i * wm_i)
.sum::<G1Projective>();
let ciphertexts_bytes = priv_attributes_ciphertexts
.iter()
.map(|c| c.to_bytes())
.collect::<Vec<_>>();
// compute challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
.chain(std::iter::once(h.to_bytes().as_ref()))
.chain(std::iter::once(
elgamal_keypair.public_key().to_bytes().as_ref(),
))
.chain(std::iter::once(commitment.to_bytes().as_ref()))
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
.chain(std::iter::once(
commitment_private_key_elgamal.to_bytes().as_ref(),
))
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
);
// Responses
let response_opening =
produce_response(&witness_commitment_opening, &challenge, commitment_opening);
let response_private_elgamal_key = produce_response(
&witness_private_elgamal_key,
&challenge,
&elgamal_keypair.private_key().0,
);
let response_keys = produce_responses(&witness_keys, &challenge, ephemeral_keys);
let response_attributes = produce_responses(
&witness_attributes,
&challenge,
&private_attributes.iter().collect::<Vec<_>>(),
);
ProofCmCs {
challenge,
response_opening,
response_private_elgamal_key,
response_keys,
response_attributes,
}
}
pub(crate) fn verify(
&self,
params: &Parameters,
pub_key: &elgamal::PublicKey,
commitment: &G1Projective,
attributes_ciphertexts: &[elgamal::Ciphertext],
) -> bool {
if self.response_keys.len() != attributes_ciphertexts.len() {
return false;
}
// recompute h
let h = hash_g1(commitment.to_bytes());
let g1 = params.gen1();
let hs_bytes = params
.gen_hs()
.iter()
.map(|h| h.to_bytes())
.collect::<Vec<_>>();
// recompute witnesses commitments
let commitment_private_key_elgamal =
pub_key * &self.challenge + g1 * self.response_private_elgamal_key;
// Aw[i] = (c * c1[i]) + (rk[i] * g1)
let commitment_keys1_bytes = attributes_ciphertexts
.iter()
.map(|ciphertext| ciphertext.c1())
.zip(self.response_keys.iter())
.map(|(c1, res_k)| c1 * self.challenge + g1 * res_k)
.map(|witness| witness.to_bytes())
.collect::<Vec<_>>();
// Bw[i] = (c * c2[i]) + (rk[i] * gamma) + (rm[i] * h)
let commitment_keys2_bytes = izip!(
attributes_ciphertexts
.iter()
.map(|ciphertext| ciphertext.c2()),
self.response_keys.iter(),
self.response_attributes.iter()
)
.map(|(c2, res_key, res_attr)| c2 * self.challenge + pub_key * res_key + h * res_attr)
.map(|witness| witness.to_bytes())
.collect::<Vec<_>>();
// Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n])
let commitment_attributes = commitment * self.challenge
+ g1 * self.response_opening
+ self
.response_attributes
.iter()
.zip(params.gen_hs().iter())
.map(|(res_attr, hs)| hs * res_attr)
.sum::<G1Projective>();
let ciphertexts_bytes = attributes_ciphertexts
.iter()
.map(|c| c.to_bytes())
.collect::<Vec<_>>();
// re-compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen1().to_bytes().as_ref())
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
.chain(std::iter::once(h.to_bytes().as_ref()))
.chain(std::iter::once(pub_key.to_bytes().as_ref()))
.chain(std::iter::once(commitment.to_bytes().as_ref()))
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
.chain(std::iter::once(
commitment_private_key_elgamal.to_bytes().as_ref(),
))
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
);
challenge == self.challenge
}
// challenge || response opening || response private elgamal key || keys len || response keys || attributes len || response attributes
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let keys_len = self.response_keys.len() as u64;
let attributes_len = self.response_attributes.len() as u64;
let mut bytes = Vec::with_capacity(16 + (keys_len + attributes_len + 3) as usize * 32);
bytes.extend_from_slice(&self.challenge.to_bytes());
bytes.extend_from_slice(&self.response_opening.to_bytes());
bytes.extend_from_slice(&self.response_private_elgamal_key.to_bytes());
bytes.extend_from_slice(&keys_len.to_le_bytes());
for rk in &self.response_keys {
bytes.extend_from_slice(&rk.to_bytes());
}
bytes.extend_from_slice(&attributes_len.to_le_bytes());
for rm in &self.response_attributes {
bytes.extend_from_slice(&rm.to_bytes());
}
bytes
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
// at the very minimum there must be a single attribute being proven
if bytes.len() < 32 * 5 + 16 || (bytes.len() - 16) % 32 != 0 {
return Err(
CoconutError::Deserialization(
"tried to deserialize proof of ciphertexts and commitment with bytes of invalid length".to_string())
);
}
let mut idx = 0;
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let response_private_elgamal_key_bytes = bytes[idx..idx + 32].try_into().unwrap();
idx += 32;
let challenge = try_deserialize_scalar(
&challenge_bytes,
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
let response_opening = try_deserialize_scalar(
&response_opening_bytes,
CoconutError::Deserialization(
"Failed to deserialize the response to the random".to_string(),
),
)?;
let response_private_elgamal_key = try_deserialize_scalar(
&response_private_elgamal_key_bytes,
CoconutError::Deserialization(
"Failed to deserialize the response to the private ElGamal key".to_string(),
),
)?;
let rk_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
idx += 8;
if bytes[idx..].len() < rk_len as usize * 32 + 8 {
return Err(
CoconutError::Deserialization(
"tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()),
);
}
let rk_end = idx + rk_len as usize * 32;
let response_keys = try_deserialize_scalar_vec(
rk_len,
&bytes[idx..rk_end],
CoconutError::Deserialization("Failed to deserialize keys response".to_string()),
)?;
let rm_len = u64::from_le_bytes(bytes[rk_end..rk_end + 8].try_into().unwrap());
let response_attributes = try_deserialize_scalar_vec(
rm_len,
&bytes[rk_end + 8..],
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
)?;
Ok(ProofCmCs {
challenge,
response_opening,
response_private_elgamal_key,
response_keys,
response_attributes,
})
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ProofKappaZeta {
// c
challenge: Scalar,
// responses
response_serial_number: Scalar,
response_binding_number: Scalar,
response_blinder: Scalar,
}
impl ProofKappaZeta {
pub(crate) fn construct(
params: &Parameters,
verification_key: &VerificationKey,
serial_number: &Attribute,
binding_number: &Attribute,
blinding_factor: &Scalar,
blinded_message: &G2Projective,
blinded_serial_number: &G2Projective,
) -> Self {
// create the witnesses
let witness_blinder = params.random_scalar();
let witness_serial_number = params.random_scalar();
let witness_binding_number = params.random_scalar();
let witness_attributes = vec![witness_serial_number, witness_binding_number];
let beta_bytes = verification_key
.beta
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
// witnesses commitments
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
let commitment_kappa = params.gen2() * witness_blinder
+ verification_key.alpha
+ witness_attributes
.iter()
.zip(verification_key.beta.iter())
.map(|(wm_i, beta_i)| beta_i * wm_i)
.sum::<G2Projective>();
// zeta is the public value associated with the serial number
let commitment_zeta = params.gen2() * witness_serial_number;
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen2().to_bytes().as_ref())
.chain(std::iter::once(blinded_message.to_bytes().as_ref()))
.chain(std::iter::once(blinded_serial_number.to_bytes().as_ref()))
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
.chain(beta_bytes.iter().map(|b| b.as_ref()))
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
);
// responses
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
let response_serial_number =
produce_response(&witness_serial_number, &challenge, serial_number);
let response_binding_number =
produce_response(&witness_binding_number, &challenge, binding_number);
ProofKappaZeta {
challenge,
response_serial_number,
response_binding_number,
response_blinder,
}
}
pub(crate) fn private_attributes_len(&self) -> usize {
2
}
pub(crate) fn verify(
&self,
params: &Parameters,
verification_key: &VerificationKey,
kappa: &G2Projective,
zeta: &G2Projective,
) -> bool {
let beta_bytes = verification_key
.beta
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
let response_attributes = vec![self.response_serial_number, self.response_binding_number];
// re-compute witnesses commitments
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
let commitment_kappa = kappa * self.challenge
+ params.gen2() * self.response_blinder
+ verification_key.alpha * (Scalar::one() - self.challenge)
+ response_attributes
.iter()
.zip(verification_key.beta.iter())
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>();
// zeta is the public value associated with the serial number
let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number;
// compute the challenge
let challenge = compute_challenge::<ChallengeDigest, _, _>(
std::iter::once(params.gen2().to_bytes().as_ref())
.chain(std::iter::once(kappa.to_bytes().as_ref()))
.chain(std::iter::once(zeta.to_bytes().as_ref()))
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
.chain(beta_bytes.iter().map(|b| b.as_ref()))
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
);
challenge == self.challenge
}
// challenge || response serial number || response binding number || repose blinder
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let attributes_len = 2; // because we have serial number and the binding number
let mut bytes = Vec::with_capacity((1 + attributes_len + 1) as usize * 32);
bytes.extend_from_slice(&self.challenge.to_bytes());
bytes.extend_from_slice(&self.response_serial_number.to_bytes());
bytes.extend_from_slice(&self.response_binding_number.to_bytes());
bytes.extend_from_slice(&self.response_blinder.to_bytes());
bytes
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
// at the very minimum there must be a single attribute being proven
if bytes.len() < 32 * 4 || (bytes.len()) % 32 != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len(),
modulus: 32,
object: "kappa and zeta".to_string(),
target: 32 * 4,
});
}
let challenge_bytes = bytes[..32].try_into().unwrap();
let challenge = try_deserialize_scalar(
&challenge_bytes,
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
)?;
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
let response_serial_number = try_deserialize_scalar(
serial_number_bytes,
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
)?;
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
let response_binding_number = try_deserialize_scalar(
binding_number_bytes,
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
)?;
let blinder_bytes = bytes[96..].try_into().unwrap();
let response_blinder = try_deserialize_scalar(
&blinder_bytes,
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
)?;
Ok(ProofKappaZeta {
challenge,
response_serial_number,
response_binding_number,
response_blinder,
})
}
}
// proof builder:
// - commitment
// - challenge
// - responses
#[cfg(test)]
mod tests {
use group::Group;
use rand::thread_rng;
use crate::scheme::issuance::{compute_attribute_encryption, compute_commitment_hash};
use crate::scheme::keygen::keygen;
use crate::scheme::setup::setup;
use crate::scheme::verification::{compute_kappa, compute_zeta};
use super::*;
#[test]
fn proof_cm_cs_bytes_roundtrip() {
let mut rng = thread_rng();
let mut params = setup(1).unwrap();
let elgamal_keypair = elgamal::elgamal_keygen(&params);
let private_attributes = params.n_random_scalars(1);
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
let cm = G1Projective::random(&mut rng);
let r = params.random_scalar();
let commitment_hash = compute_commitment_hash(cm);
let (attributes_ciphertexts, _): (Vec<_>, Vec<_>) = compute_attribute_encryption(
&params,
private_attributes.as_ref(),
elgamal_keypair.public_key(),
commitment_hash,
);
let ephemeral_keys = params.n_random_scalars(1);
// 0 public 1 private
let pi_s = ProofCmCs::construct(
&mut params,
&elgamal_keypair,
&ephemeral_keys,
&cm,
&r,
&private_attributes,
&*attributes_ciphertexts,
);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
// 2 private
let private_attributes = params.n_random_scalars(2);
let ephemeral_keys = params.n_random_scalars(2);
let pi_s = ProofCmCs::construct(
&mut params,
&elgamal_keypair,
&ephemeral_keys,
&cm,
&r,
&private_attributes,
&*attributes_ciphertexts,
);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
}
#[test]
fn proof_kappa_zeta_bytes_roundtrip() {
let mut params = setup(4).unwrap();
let keypair = keygen(&mut params);
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let r = params.random_scalar();
let kappa = compute_kappa(&params, &keypair.verification_key(), &private_attributes, r);
let zeta = compute_zeta(&params, serial_number);
// 0 public 2 private
let pi_v = ProofKappaZeta::construct(
&mut params,
&keypair.verification_key(),
&serial_number,
&binding_number,
&r,
&kappa,
&zeta,
);
let proof_bytes = pi_v.to_bytes();
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
assert_eq!(proof_from_bytes, pi_v);
// 2 public 2 private
let mut params = setup(4).unwrap();
let keypair = keygen(&mut params);
let pi_v = ProofKappaZeta::construct(
&mut params,
&keypair.verification_key(),
&serial_number,
&binding_number,
&r,
&kappa,
&zeta,
);
let proof_bytes = pi_v.to_bytes();
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
assert_eq!(proof_from_bytes, pi_v);
}
}
+377
View File
@@ -0,0 +1,377 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::iter::Sum;
use core::ops::Mul;
use bls12_381::{G2Prepared, G2Projective, Scalar};
use group::Curve;
use itertools::Itertools;
use crate::error::{CoconutError, Result};
use crate::scheme::verification::check_bilinear_pairing;
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
use crate::utils::perform_lagrangian_interpolation_at_origin;
use crate::{Attribute, Parameters};
pub(crate) trait Aggregatable: Sized {
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
// if aggregation is a threshold one, all indices should be unique
indices.iter().unique_by(|&index| index).count() == indices.len()
}
}
// includes `VerificationKey`
impl<T> Aggregatable for T
where
T: Sum,
for<'a> T: Sum<&'a T>,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
if aggregatable.is_empty() {
return Err(CoconutError::Aggregation("Empty set of values".to_string()));
}
if let Some(indices) = indices {
if !Self::check_unique_indices(indices) {
return Err(CoconutError::Aggregation("Non-unique indices".to_string()));
}
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
} else {
// non-threshold
Ok(aggregatable.iter().sum())
}
}
}
impl Aggregatable for PartialSignature {
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
let h = sigs
.get(0)
.ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))?
.sig1();
// TODO: is it possible to avoid this allocation?
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
Ok(Signature(*h, aggr_sigma))
}
}
/// Ensures all provided verification keys were generated to verify the same number of attributes.
fn check_same_key_size(keys: &[VerificationKey]) -> bool {
keys.iter().map(|vk| vk.beta.len()).all_equal()
}
pub fn aggregate_verification_keys(
keys: &[VerificationKey],
indices: Option<&[SignerIndex]>,
) -> Result<VerificationKey> {
if !check_same_key_size(keys) {
return Err(CoconutError::Aggregation(
"Verification keys are of different sizes".to_string(),
));
}
Aggregatable::aggregate(keys, indices)
}
pub fn aggregate_signatures(
params: &Parameters,
verification_key: &VerificationKey,
attributes: &[Attribute],
signatures: &[PartialSignature],
indices: Option<&[SignerIndex]>,
) -> Result<Signature> {
// aggregate the signature
let signature = match Aggregatable::aggregate(signatures, indices) {
Ok(res) => res,
Err(err) => return Err(err),
};
// Verify the signature
let alpha = verification_key.alpha;
let tmp = attributes
.iter()
.zip(verification_key.beta.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
if !check_bilinear_pairing(
&signature.0.to_affine(),
&G2Prepared::from((alpha + tmp).to_affine()),
&signature.1.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CoconutError::Aggregation(
"Verification of the aggregated signature failed".to_string(),
));
}
Ok(signature)
}
pub fn aggregate_signature_shares(
params: &Parameters,
verification_key: &VerificationKey,
attributes: &[Attribute],
shares: &[SignatureShare],
) -> Result<Signature> {
let (signatures, indices): (Vec<_>, Vec<_>) = shares
.iter()
.map(|share| (*share.signature(), share.index()))
.unzip();
aggregate_signatures(
params,
verification_key,
attributes,
&signatures,
Some(&indices),
)
}
#[cfg(test)]
mod tests {
use bls12_381::G1Projective;
use group::Group;
use crate::scheme::issuance::sign;
use crate::scheme::keygen::ttp_keygen;
use crate::scheme::setup::Parameters;
use crate::scheme::verification::verify;
use super::*;
#[test]
fn key_aggregation_works_for_any_subset_of_keys() {
let mut params = Parameters::new(2).unwrap();
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let vks = keypairs
.into_iter()
.map(|keypair| keypair.verification_key())
.collect::<Vec<_>>();
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
assert_eq!(aggr_vk1, aggr_vk2);
// TODO: should those two actually work or not?
// aggregating threshold+1
let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
assert_eq!(aggr_vk1, aggr_more);
// aggregating all
let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
assert_eq!(aggr_all, aggr_vk1);
// not taking enough points (threshold was 3)
let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
assert_ne!(aggr_not_enough, aggr_vk1);
// taking wrong index
let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap();
assert_ne!(aggr_vk1, aggr_bad);
}
#[test]
fn key_aggregation_doesnt_work_for_empty_set_of_keys() {
let keys: Vec<VerificationKey> = vec![];
assert!(aggregate_verification_keys(&keys, None).is_err());
}
#[test]
fn key_aggregation_doesnt_work_if_indices_have_invalid_length() {
let keys = vec![VerificationKey::identity(3)];
assert!(aggregate_verification_keys(&keys, Some(&[])).is_err());
assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err());
}
#[test]
fn key_aggregation_doesnt_work_for_non_unique_indices() {
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)];
assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err());
}
#[test]
fn key_aggregation_doesnt_work_for_keys_of_different_size() {
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)];
assert!(aggregate_verification_keys(&keys, None).is_err())
}
#[test]
fn signature_aggregation_works_for_any_subset_of_signatures() {
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
.unzip();
let sigs = sks
.iter()
.map(|sk| sign(&mut params, sk, &attributes).unwrap())
.collect::<Vec<_>>();
// aggregating (any) threshold works
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
let aggr_sig1 = aggregate_signatures(
&params,
&aggr_vk_1,
&attributes,
&sigs[..3],
Some(&[1, 2, 3]),
)
.unwrap();
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
let aggr_sig2 = aggregate_signatures(
&params,
&aggr_vk_1,
&attributes,
&sigs[2..],
Some(&[3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_sig1, aggr_sig2);
// verify credential for good measure
assert!(verify(&params, &aggr_vk_1, &attributes, &aggr_sig1));
assert!(verify(&params, &aggr_vk_2, &attributes, &aggr_sig2));
// aggregating threshold+1 works
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
let aggr_more = aggregate_signatures(
&params,
&aggr_vk_more,
&attributes,
&sigs[1..],
Some(&[2, 3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_sig1, aggr_more);
// aggregating all
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
let aggr_all = aggregate_signatures(
&params,
&aggr_vk_all,
&attributes,
&sigs,
Some(&[1, 2, 3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_all, aggr_sig1);
// not taking enough points (threshold was 3) should fail
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
let aggr_not_enough = aggregate_signatures(
&params,
&aggr_vk_not_enough,
&attributes,
&sigs[..2],
Some(&[1, 2]),
)
.unwrap();
assert_ne!(aggr_not_enough, aggr_sig1);
// taking wrong index should fail
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
assert!(aggregate_signatures(
&params,
&aggr_vk_bad,
&attributes,
&sigs[2..],
Some(&[42, 123, 100]),
)
.is_err());
}
fn random_signature() -> Signature {
let mut rng = rand::thread_rng();
Signature(
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
)
}
#[test]
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
let signatures: Vec<Signature> = vec![];
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(
aggregate_signatures(&params, &aggr_vk_all, &attributes, &signatures, None).is_err()
);
}
#[test]
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
let signatures = vec![random_signature()];
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(
aggregate_signatures(&params, &aggr_vk_all, &attributes, &signatures, Some(&[]))
.is_err()
);
assert!(aggregate_signatures(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[1, 2]),
)
.is_err());
}
#[test]
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
let signatures = vec![random_signature(), random_signature()];
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
.unzip();
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
assert!(aggregate_signatures(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[1, 1]),
)
.is_err());
}
// TODO: test for aggregating non-threshold keys
}
+382
View File
@@ -0,0 +1,382 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G1Affine, G1Projective, Scalar};
use group::{Curve, GroupEncoding};
use crate::elgamal::{Ciphertext, EphemeralKey};
use crate::error::{CoconutError, Result};
use crate::proofs::ProofCmCs;
use crate::scheme::setup::Parameters;
use crate::scheme::BlindedSignature;
use crate::scheme::SecretKey;
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
#[cfg(test)]
use crate::Signature;
use crate::{elgamal, Attribute, ElGamalKeyPair};
// TODO: possibly completely remove those two functions.
// They only exist to have a simpler and smaller code snippets to test
// basic functionalities.
use crate::traits::{Base58, Bytable};
use crate::utils::{hash_g1, try_deserialize_g1_projective};
// TODO NAMING: double check this one
// Lambda
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct BlindSignRequest {
// cm
commitment: G1Projective,
// h
commitment_hash: G1Projective,
// c
private_attributes_ciphertexts: Vec<elgamal::Ciphertext>,
// pi_s
pi_s: ProofCmCs,
}
impl TryFrom<&[u8]> for BlindSignRequest {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
if bytes.len() < 48 + 48 + 8 + 96 {
return Err(CoconutError::DeserializationMinLength {
min: 48 + 48 + 8 + 96,
actual: bytes.len(),
});
}
let mut j = 0;
let commitment_bytes_len = 48;
let commitment_hash_bytes_len = 48;
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
let commitment = try_deserialize_g1_projective(
&cm_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
),
)?;
j += commitment_bytes_len;
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
let commitment_hash = try_deserialize_g1_projective(
&cm_hash_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment hash".to_string(),
),
)?;
j += commitment_hash_bytes_len;
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
j += 8;
if bytes[j..].len() < c_len as usize * 96 {
return Err(CoconutError::DeserializationMinLength {
min: c_len as usize * 96,
actual: bytes[56..].len(),
});
}
let mut private_attributes_ciphertexts = Vec::with_capacity(c_len as usize);
for i in 0..c_len as usize {
let start = j + i * 96;
let end = start + 96;
private_attributes_ciphertexts.push(Ciphertext::try_from(&bytes[start..end])?)
}
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 96..])?;
Ok(BlindSignRequest {
commitment,
commitment_hash,
private_attributes_ciphertexts,
pi_s,
})
}
}
impl Bytable for BlindSignRequest {
fn to_byte_vec(&self) -> Vec<u8> {
let cm_bytes = self.commitment.to_affine().to_compressed();
let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed();
let c_len = self.private_attributes_ciphertexts.len() as u64;
let proof_bytes = self.pi_s.to_bytes();
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 96 + proof_bytes.len());
bytes.extend_from_slice(&cm_bytes);
bytes.extend_from_slice(&cm_hash_bytes);
bytes.extend_from_slice(&c_len.to_le_bytes());
for c in &self.private_attributes_ciphertexts {
bytes.extend_from_slice(&c.to_bytes());
}
bytes.extend_from_slice(&proof_bytes);
bytes
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
BlindSignRequest::from_bytes(slice)
}
}
impl Base58 for BlindSignRequest {}
impl BlindSignRequest {
fn verify_proof(&self, params: &Parameters, pub_key: &elgamal::PublicKey) -> bool {
self.pi_s.verify(
params,
pub_key,
&self.commitment,
&self.private_attributes_ciphertexts,
)
}
pub fn get_commitment_hash(&self) -> G1Projective {
self.commitment_hash
}
pub fn to_bytes(&self) -> Vec<u8> {
self.to_byte_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
BlindSignRequest::try_from(bytes)
}
}
pub fn compute_private_attributes_commitment(
params: &Parameters,
private_attributes: &[Attribute],
hs: &[G1Affine],
) -> (Scalar, G1Projective) {
let commitment_opening = params.random_scalar();
// Produces h0 ^ m0 * h1^m1 * .... * hn^mn
// where m0, m1, ...., mn are private attributes
let attr_cm = private_attributes
.iter()
.zip(hs)
.map(|(&m, h)| h * m)
.sum::<G1Projective>();
// Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn
let commitment = params.gen1() * commitment_opening + attr_cm;
(commitment_opening, commitment)
}
pub fn compute_commitment_hash(commitment: G1Projective) -> G1Projective {
hash_g1(commitment.to_bytes())
}
pub fn compute_attribute_encryption(
params: &Parameters,
private_attributes: &[Attribute],
pub_key: &elgamal::PublicKey,
commitment_hash: G1Projective,
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
private_attributes
.iter()
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
.unzip()
}
/// Builds cryptographic material required for blind sign.
pub fn prepare_blind_sign(
params: &Parameters,
elgamal_keypair: &ElGamalKeyPair,
private_attributes: &[Attribute],
public_attributes: &[Attribute],
) -> Result<BlindSignRequest> {
if private_attributes.is_empty() {
return Err(CoconutError::Issuance(
"Tried to prepare blind sign request for an empty set of private attributes"
.to_string(),
));
}
let hs = params.gen_hs();
if private_attributes.len() + public_attributes.len() > hs.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: hs.len(),
requested: private_attributes.len() + public_attributes.len(),
});
}
let (commitment_opening, commitment) =
compute_private_attributes_commitment(params, private_attributes, hs);
// Compute the challenge as the commitment hash
let commitment_hash = compute_commitment_hash(commitment);
// build ElGamal encryption
let (private_attributes_ciphertexts, ephemeral_keys): (Vec<_>, Vec<_>) =
compute_attribute_encryption(
params,
private_attributes,
elgamal_keypair.public_key(),
commitment_hash,
);
let pi_s = ProofCmCs::construct(
params,
elgamal_keypair,
&ephemeral_keys,
&commitment,
&commitment_opening,
private_attributes,
&*private_attributes_ciphertexts,
);
Ok(BlindSignRequest {
commitment,
commitment_hash,
private_attributes_ciphertexts,
pi_s,
})
}
pub fn blind_sign(
params: &Parameters,
signing_secret_key: &SecretKey,
prover_pub_key: &elgamal::PublicKey,
blind_sign_request: &BlindSignRequest,
public_attributes: &[Attribute],
) -> Result<BlindedSignature> {
let num_private = blind_sign_request.private_attributes_ciphertexts.len();
let hs = params.gen_hs();
if num_private + public_attributes.len() > hs.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: hs.len(),
requested: num_private + public_attributes.len(),
});
}
// Verify the commitment hash
let h = hash_g1(blind_sign_request.commitment.to_bytes());
if !(h == blind_sign_request.commitment_hash) {
return Err(CoconutError::Issuance(
"Failed to verify the commitment hash".to_string(),
));
}
// Verify the ZK proof
if !blind_sign_request.verify_proof(params, prover_pub_key) {
return Err(CoconutError::Issuance(
"Failed to verify the proof of knowledge".to_string(),
));
}
// in python implementation there are n^2 G1 multiplications, let's do it with a single one instead.
// i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes)
// rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately
let signed_public = h * public_attributes
.iter()
.zip(signing_secret_key.ys.iter().skip(num_private))
.map(|(attr, yi)| attr * yi)
.sum::<Scalar>();
// c1[0] ^ y[0] * ... * c1[m] ^ y[m]
let sig_1 = blind_sign_request
.private_attributes_ciphertexts
.iter()
.map(|ciphertext| ciphertext.c1())
.zip(signing_secret_key.ys.iter())
.map(|(c1, yi)| c1 * yi)
.sum();
// h ^ x + c2[0] ^ y[0] + ... c2[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
let sig_2 = blind_sign_request
.private_attributes_ciphertexts
.iter()
.map(|ciphertext| ciphertext.c2())
.zip(signing_secret_key.ys.iter())
.map(|(c2, yi)| c2 * yi)
.chain(std::iter::once(h * signing_secret_key.x))
.chain(std::iter::once(signed_public))
.sum();
Ok(BlindedSignature(h, elgamal::Ciphertext(sig_1, sig_2)))
}
#[cfg(test)]
pub fn sign(
params: &mut Parameters,
secret_key: &SecretKey,
public_attributes: &[Attribute],
) -> Result<Signature> {
if public_attributes.len() > secret_key.ys.len() {
return Err(CoconutError::IssuanceMaxAttributes {
max: secret_key.ys.len(),
requested: public_attributes.len(),
});
}
// TODO: why in the python implementation this hash onto the curve is present
// while it's not used in the paper? the paper uses random exponent instead.
// (the python implementation hashes string representation of all attributes onto the curve,
// but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary
// transformation. If I'm wrong, please correct me.)
let attributes_sum = public_attributes.iter().sum::<Scalar>();
let h = hash_g1((params.gen1() * attributes_sum).to_bytes());
// x + m0 * y0 + m1 * y1 + ... mn * yn
let exponent = secret_key.x
+ public_attributes
.iter()
.zip(secret_key.ys.iter())
.map(|(m_i, y_i)| m_i * y_i)
.sum::<Scalar>();
let sig2 = h * exponent;
Ok(Signature(h, sig2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blind_sign_request_bytes_roundtrip() {
let mut params = Parameters::new(1).unwrap();
let public_attributes = params.n_random_scalars(0);
let private_attributes = params.n_random_scalars(1);
let elgamal_keypair = elgamal::elgamal_keygen(&params);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let bytes = lambda.to_bytes();
println!("{:?}", bytes.len());
assert_eq!(
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
lambda
);
let mut params = Parameters::new(4).unwrap();
let public_attributes = params.n_random_scalars(2);
let private_attributes = params.n_random_scalars(2);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let bytes = lambda.to_bytes();
assert_eq!(
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
lambda
);
}
}
+539
View File
@@ -0,0 +1,539 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::borrow::Borrow;
use core::iter::Sum;
use core::ops::{Add, Mul};
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G2Projective, Scalar};
use group::Curve;
use serde_derive::{Deserialize, Serialize};
use crate::error::{CoconutError, Result};
use crate::scheme::aggregation::aggregate_verification_keys;
use crate::scheme::setup::Parameters;
use crate::scheme::SignerIndex;
use crate::traits::Bytable;
use crate::utils::{
try_deserialize_g2_projective, try_deserialize_scalar, try_deserialize_scalar_vec, Polynomial,
};
use crate::Base58;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct SecretKey {
pub(crate) x: Scalar,
pub(crate) ys: Vec<Scalar>,
}
impl TryFrom<&[u8]> for SecretKey {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 32 * 2 + 8,
modulus: 32,
object: "secret key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
let actual_ys_len = (bytes.len() - 40) / 32;
if ys_len as usize != actual_ys_len {
return Err(CoconutError::Deserialization(format!(
"Tried to deserialize secret key with inconsistent ys len (expected {}, got {})",
ys_len, actual_ys_len
)));
}
let x = try_deserialize_scalar(
&x_bytes,
CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()),
)?;
let ys = try_deserialize_scalar_vec(
ys_len,
&bytes[40..],
CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()),
)?;
Ok(SecretKey { x, ys })
}
}
impl SecretKey {
/// Derive verification key using this secret key.
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
let g2 = params.gen2();
VerificationKey {
alpha: g2 * self.x,
beta: self.ys.iter().map(|y| g2 * y).collect(),
}
}
// x || ys.len() || ys
pub fn to_bytes(&self) -> Vec<u8> {
let ys_len = self.ys.len() as u64;
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) as usize * 32);
bytes.extend_from_slice(&self.x.to_bytes());
bytes.extend_from_slice(&ys_len.to_le_bytes());
for y in self.ys.iter() {
bytes.extend_from_slice(&y.to_bytes())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey> {
SecretKey::try_from(bytes)
}
}
impl Bytable for SecretKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
SecretKey::try_from(slice)
}
}
impl Base58 for SecretKey {}
// TODO: perhaps change points to affine representation
// to make verification slightly more efficient?
#[derive(Debug, PartialEq, Clone)]
pub struct VerificationKey {
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
pub(crate) alpha: G2Projective,
pub(crate) beta: Vec<G2Projective>,
}
impl TryFrom<&[u8]> for VerificationKey {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<VerificationKey> {
if bytes.len() < 96 * 2 + 8 || (bytes.len() - 8) % 96 != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 96 * 2 + 8,
modulus: 96,
object: "secret key".to_string(),
});
}
// this conversion will not fail as we are taking the same length of data
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
let beta_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
let actual_beta_len = (bytes.len() - 104) / 96;
if beta_len as usize != actual_beta_len {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize verification key with inconsistent beta len (expected {}, got {})",
beta_len, actual_beta_len
)));
}
let alpha = try_deserialize_g2_projective(
&alpha_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G2 point (alpha)".to_string(),
),
)?;
let mut beta = Vec::with_capacity(actual_beta_len);
for i in 0..actual_beta_len {
let start = 104 + i * 96;
let end = start + 96;
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g2_projective(
&beta_i_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G2 point (beta)".to_string(),
),
)?;
beta.push(beta_i)
}
Ok(VerificationKey { alpha, beta })
}
}
impl<'b> Add<&'b VerificationKey> for VerificationKey {
type Output = VerificationKey;
#[inline]
fn add(self, rhs: &'b VerificationKey) -> VerificationKey {
// If you're trying to add two keys together that were created
// for different number of attributes, just panic as it's a
// nonsense operation.
assert_eq!(
self.beta.len(),
rhs.beta.len(),
"trying to add verification keys generated for different number of attributes"
);
VerificationKey {
alpha: self.alpha + rhs.alpha,
beta: self
.beta
.iter()
.zip(rhs.beta.iter())
.map(|(self_beta, rhs_beta)| self_beta + rhs_beta)
.collect(),
}
}
}
impl<'a> Mul<Scalar> for &'a VerificationKey {
type Output = VerificationKey;
#[inline]
fn mul(self, rhs: Scalar) -> Self::Output {
VerificationKey {
alpha: self.alpha * rhs,
beta: self.beta.iter().map(|b_i| b_i * rhs).collect(),
}
}
}
impl<T> Sum<T> for VerificationKey
where
T: Borrow<VerificationKey>,
{
#[inline]
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = T>,
{
let mut peekable = iter.peekable();
let head_attributes = match peekable.peek() {
Some(head) => head.borrow().beta.len(),
None => {
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
// of VerificationKey. So should it panic here or just return some nonsense value?
return VerificationKey::identity(0);
}
};
peekable.fold(VerificationKey::identity(head_attributes), |acc, item| {
acc + item.borrow()
})
}
}
impl VerificationKey {
/// Create a (kinda) identity verification key using specified
/// number of 'beta' elements
pub(crate) fn identity(beta_size: usize) -> Self {
VerificationKey {
alpha: G2Projective::identity(),
beta: vec![G2Projective::identity(); beta_size],
}
}
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
aggregate_verification_keys(sigs, indices)
}
pub fn alpha(&self) -> &G2Projective {
&self.alpha
}
pub fn beta(&self) -> &Vec<G2Projective> {
&self.beta
}
pub fn to_bytes(&self) -> Vec<u8> {
let beta_len = self.beta.len() as u64;
let mut bytes = Vec::with_capacity(8 + (beta_len + 1) as usize * 96);
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
bytes.extend_from_slice(&beta_len.to_le_bytes());
for beta in self.beta.iter() {
bytes.extend_from_slice(&beta.to_affine().to_compressed())
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKey> {
VerificationKey::try_from(bytes)
}
}
impl Bytable for VerificationKey {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
VerificationKey::try_from(slice)
}
}
impl Base58 for VerificationKey {}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct KeyPair {
secret_key: SecretKey,
verification_key: VerificationKey,
/// Optional index value specifying polynomial point used during threshold key generation.
pub index: Option<SignerIndex>,
}
impl KeyPair {
const MARKER_BYTES: &'static [u8] = b"coconutkeypair";
pub fn secret_key(&self) -> SecretKey {
self.secret_key.clone()
}
pub fn verification_key(&self) -> VerificationKey {
self.verification_key.clone()
}
pub fn to_bytes(&self) -> Vec<u8> {
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
self.to_byte_vec()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
KeyPair::try_from_byte_slice(bytes)
}
}
impl Bytable for KeyPair {
fn to_byte_vec(&self) -> Vec<u8> {
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
let mut byts = vec![];
let secret_key_bytes = self.secret_key.to_bytes();
let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes();
let verification_key_bytes = self.verification_key.to_bytes();
let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes();
byts.extend_from_slice(Self::MARKER_BYTES);
byts.extend_from_slice(&secret_key_len);
byts.extend_from_slice(&secret_key_bytes);
byts.extend_from_slice(&verification_key_len);
byts.extend_from_slice(&verification_key_bytes);
if let Some(index) = self.index {
byts.extend_from_slice(&index.to_le_bytes())
}
byts
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
KeyPair::try_from(slice)
}
}
impl Base58 for KeyPair {}
impl TryFrom<&[u8]> for KeyPair {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<KeyPair> {
let header_len = Self::MARKER_BYTES.len();
// we must be able to at the very least read the length of secret key which is past the header
// and is 8 bytes long
if bytes.len() < header_len + 8 {
return Err(CoconutError::DeserializationMinLength {
min: header_len + 8,
actual: bytes.len(),
});
}
let secret_key_len =
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
let secret_key_start = header_len + 8;
let secret_key =
SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?;
// we must be able to read the length of verification key
if bytes.len() < secret_key_start + secret_key_len + 8 {
return Err(CoconutError::DeserializationMinLength {
min: secret_key_start + secret_key_len + 8,
actual: bytes.len(),
});
}
let verification_key_len = u64::from_le_bytes(
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
.try_into()
.unwrap(),
) as usize;
let verification_key_start = secret_key_start + secret_key_len + 8;
let verification_key = VerificationKey::try_from(
&bytes[verification_key_start..verification_key_start + verification_key_len],
)?;
let consumed_bytes = verification_key_start + verification_key_len;
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
Some(u64::from_le_bytes(
bytes[consumed_bytes..consumed_bytes + 8]
.try_into()
.unwrap(),
))
} else {
None
};
Ok(KeyPair {
secret_key,
verification_key,
index,
})
}
}
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
/// that are independent of each other.
#[cfg(test)]
pub fn keygen(params: &Parameters) -> KeyPair {
let attributes = params.gen_hs().len();
let x = params.random_scalar();
let ys = params.n_random_scalars(attributes);
let secret_key = SecretKey { x, ys };
let verification_key = secret_key.verification_key(params);
KeyPair {
secret_key,
verification_key,
index: None,
}
}
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
/// such that they support threshold aggregation by `threshold` number of parties.
/// It is expected that this procedure is executed by a Trusted Third Party.
pub fn ttp_keygen(
params: &Parameters,
threshold: u64,
num_authorities: u64,
) -> Result<Vec<KeyPair>> {
if threshold == 0 {
return Err(CoconutError::Setup(
"Tried to generate threshold keys with a 0 threshold value".to_string(),
));
}
if threshold > num_authorities {
return Err(
CoconutError::Setup(
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
));
}
let attributes = params.gen_hs().len();
// generate polynomials
let v = Polynomial::new_random(params, threshold - 1);
let ws = (0..attributes)
.map(|_| Polynomial::new_random(params, threshold - 1))
.collect::<Vec<_>>();
// TODO: potentially if we had some known authority identifier we could use that instead
// of the increasing (1,2,3,...) sequence
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
// generate polynomial shares
let x = polynomial_indices
.iter()
.map(|&id| v.evaluate(&Scalar::from(id)));
let ys = polynomial_indices.iter().map(|&id| {
ws.iter()
.map(|w| w.evaluate(&Scalar::from(id)))
.collect::<Vec<_>>()
});
// finally set the keys
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys });
let keypairs = secret_keys
.zip(polynomial_indices.iter())
.map(|(secret_key, index)| {
let verification_key = secret_key.verification_key(params);
KeyPair {
secret_key,
verification_key,
index: Some(*index),
}
})
.collect();
Ok(keypairs)
}
#[cfg(test)]
mod tests {
use crate::scheme::setup::setup;
use super::*;
#[test]
fn keypair_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let keypair1 = keygen(&mut params1);
let keypair5 = keygen(&mut params5);
let bytes1 = keypair1.to_bytes();
let bytes5 = keypair5.to_bytes();
assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1);
assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5);
}
#[test]
fn secret_key_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let keypair1 = keygen(&mut params1);
let keypair5 = keygen(&mut params5);
let bytes1 = keypair1.secret_key.to_bytes();
let bytes5 = keypair5.secret_key.to_bytes();
assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key);
assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key);
}
#[test]
fn verification_key_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let keypair1 = &keygen(&mut params1);
let keypair5 = &keygen(&mut params5);
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
assert_eq!(
VerificationKey::try_from(bytes1.as_slice()).unwrap(),
keypair1.verification_key
);
assert_eq!(
VerificationKey::try_from(bytes5.as_slice()).unwrap(),
keypair5.verification_key
);
}
}
+661
View File
@@ -0,0 +1,661 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO: implement https://crates.io/crates/signature traits?
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
use group::Curve;
pub use keygen::{SecretKey, VerificationKey};
use crate::elgamal::Ciphertext;
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::scheme::verification::check_bilinear_pairing;
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g1_projective;
use crate::{elgamal, Attribute};
pub mod aggregation;
pub mod issuance;
pub mod keygen;
pub mod setup;
pub mod verification;
pub type SignerIndex = u64;
// (h, s)
#[derive(Debug, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective);
pub type PartialSignature = Signature;
impl TryFrom<&[u8]> for Signature {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Signature> {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"Signature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
let sig1 = try_deserialize_g1_projective(
sig1_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()),
)?;
let sig2 = try_deserialize_g1_projective(
sig2_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()),
)?;
Ok(Signature(sig1, sig2))
}
}
impl Signature {
pub(crate) fn sig1(&self) -> &G1Projective {
&self.0
}
pub(crate) fn sig2(&self) -> &G1Projective {
&self.1
}
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
let r = params.random_scalar();
let r_prime = params.random_scalar();
let h_prime = self.0 * r_prime;
let s_prime = (self.1 * r_prime) + (h_prime * r);
(Signature(h_prime, s_prime), r)
}
pub fn to_bytes(self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
Signature::try_from(bytes)
}
}
impl Bytable for Signature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Signature::from_bytes(slice)
}
}
impl Base58 for Signature {}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct BlindedSignature(G1Projective, elgamal::Ciphertext);
impl Bytable for BlindedSignature {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Self::from_bytes(slice)
}
}
impl Base58 for BlindedSignature {}
impl TryFrom<&[u8]> for BlindedSignature {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
if bytes.len() != 144 {
return Err(CoconutError::Deserialization(format!(
"BlindedSignature must be exactly 144 bytes, got {}",
bytes.len()
)));
}
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let h = try_deserialize_g1_projective(
h_bytes,
CoconutError::Deserialization("Failed to deserialize compressed h".to_string()),
)?;
let c_tilde = Ciphertext::try_from(&bytes[48..])?;
Ok(BlindedSignature(h, c_tilde))
}
}
impl BlindedSignature {
pub fn unblind(
&self,
params: &Parameters,
private_key: &elgamal::PrivateKey,
partial_verification_key: &VerificationKey,
private_attributes: &[Attribute],
public_attributes: &[Attribute],
commitment_hash: &G1Projective,
) -> Result<Signature> {
// parse the signature
let h = &self.0;
let c = &self.1;
let sig2 = private_key.decrypt(c);
// Verify the commitment hash
if !(commitment_hash == h) {
return Err(CoconutError::Unblind(
"Verification of commitment hash from signature failed".to_string(),
));
}
let alpha = partial_verification_key.alpha;
let tmp = private_attributes
.iter()
.chain(public_attributes.iter())
.zip(partial_verification_key.beta.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
// Verify the signature share
if !check_bilinear_pairing(
&h.to_affine(),
&G2Prepared::from((alpha + tmp).to_affine()),
&sig2.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CoconutError::Unblind(
"Verification of signature share failed".to_string(),
));
}
Ok(Signature(self.0, sig2))
}
pub fn to_bytes(&self) -> [u8; 144] {
let mut bytes = [0u8; 144];
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
bytes[48..].copy_from_slice(&self.1.to_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
BlindedSignature::try_from(bytes)
}
}
// perhaps this should take signature by reference? we'll see how it goes
pub struct SignatureShare {
signature: Signature,
index: SignerIndex,
}
impl SignatureShare {
pub fn new(signature: Signature, index: SignerIndex) -> Self {
SignatureShare { signature, index }
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub fn index(&self) -> SignerIndex {
self.index
}
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
// aggregate_signature_shares(shares)
// }
}
#[cfg(test)]
mod tests {
use group::GroupEncoding;
use crate::hash_to_scalar;
use crate::scheme::aggregation::{aggregate_signatures, aggregate_verification_keys};
use crate::scheme::issuance::{blind_sign, prepare_blind_sign, sign};
use crate::scheme::keygen::{keygen, ttp_keygen};
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
use crate::utils::hash_g1;
use super::*;
#[test]
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
let mut params = Parameters::new(2).unwrap();
let private_attributes = params.n_random_scalars(2 as usize);
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
let lambda =
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&mut params);
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap();
let wrong_commitment_opening = params.random_scalar();
let wrong_commitment = params.gen1() * wrong_commitment_opening;
let fake_commitment_hash = hash_g1(wrong_commitment.to_bytes());
assert!(sig1
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes,
&[],
&fake_commitment_hash,
)
.is_err());
}
#[test]
fn unblind_returns_error_if_signature_verification_fails() {
let mut params = Parameters::new(2).unwrap();
let private_attributes = vec![hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
let private_attributes2 = vec![hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
let lambda =
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&mut params);
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap();
assert!(sig1
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes2,
&[],
&lambda.get_commitment_hash(),
)
.is_err());
}
#[test]
fn verification_on_two_private_attributes() {
let mut params = Parameters::new(2).unwrap();
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
let keypair1 = keygen(&mut params);
let keypair2 = keygen(&mut params);
let lambda =
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap()
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
)
.unwrap();
let sig2 = blind_sign(
&mut params,
&keypair2.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap()
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair2.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&mut params,
&keypair1.verification_key(),
&sig1,
serial_number,
binding_number,
)
.unwrap();
let theta2 = prove_bandwidth_credential(
&mut params,
&keypair2.verification_key(),
&sig2,
serial_number,
binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&keypair1.verification_key(),
&theta1,
&[],
));
assert!(verify_credential(
&params,
&keypair2.verification_key(),
&theta2,
&[],
));
assert!(!verify_credential(
&params,
&keypair1.verification_key(),
&theta2,
&[],
));
}
#[test]
fn verification_on_two_public_attributes() {
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypair1 = keygen(&mut params);
let keypair2 = keygen(&mut params);
let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap();
let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap();
assert!(verify(
&params,
&keypair1.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
&keypair2.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
&keypair1.verification_key(),
&attributes,
&sig2,
));
}
#[test]
fn verification_on_two_public_and_two_private_attributes() {
let mut params = Parameters::new(4).unwrap();
let public_attributes = params.n_random_scalars(2);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
let keypair1 = keygen(&mut params);
let keypair2 = keygen(&mut params);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&public_attributes,
)
.unwrap()
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
)
.unwrap();
let sig2 = blind_sign(
&mut params,
&keypair2.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&public_attributes,
)
.unwrap()
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair2.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&mut params,
&keypair1.verification_key(),
&sig1,
serial_number,
binding_number,
)
.unwrap();
let theta2 = prove_bandwidth_credential(
&mut params,
&keypair2.verification_key(),
&sig2,
serial_number,
binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&keypair1.verification_key(),
&theta1,
&public_attributes,
));
assert!(verify_credential(
&params,
&keypair2.verification_key(),
&theta2,
&public_attributes,
));
assert!(!verify_credential(
&params,
&keypair1.verification_key(),
&theta2,
&public_attributes,
));
}
#[test]
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
let mut params = Parameters::new(4).unwrap();
let public_attributes = params.n_random_scalars(2);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let elgamal_keypair = elgamal::elgamal_keygen(&params);
let keypairs = ttp_keygen(&mut params, 2, 3).unwrap();
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let sigs = keypairs
.iter()
.map(|keypair| {
blind_sign(
&mut params,
&keypair.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&public_attributes,
)
.unwrap()
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
)
.unwrap()
})
.collect::<Vec<_>>();
let vks = keypairs
.into_iter()
.map(|keypair| keypair.verification_key())
.collect::<Vec<_>>();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
let aggr_sig =
aggregate_signatures(&params, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2]))
.unwrap();
let theta = prove_bandwidth_credential(
&mut params,
&aggr_vk,
&aggr_sig,
serial_number,
binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&aggr_vk,
&theta,
&public_attributes,
));
// taking different subset of keys and credentials
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
let aggr_sig =
aggregate_signatures(&params, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3]))
.unwrap();
let theta = prove_bandwidth_credential(
&mut params,
&aggr_vk,
&aggr_sig,
serial_number,
binding_number,
)
.unwrap();
assert!(verify_credential(
&params,
&aggr_vk,
&theta,
&public_attributes,
));
}
#[test]
fn signature_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let signature = Signature(params.gen1() * r, params.gen1() * s);
let bytes = signature.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
signature.0.to_affine().to_compressed(),
signature.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap())
}
#[test]
fn blinded_signature_bytes_roundtrip() {
let params = Parameters::default();
let r = params.random_scalar();
let s = params.random_scalar();
let t = params.random_scalar();
let blinded_sig = BlindedSignature(
params.gen1() * t,
Ciphertext(params.gen1() * r, params.gen1() * s),
);
let bytes = blinded_sig.to_bytes();
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
let expected_bytes = [
blinded_sig.0.to_affine().to_compressed(),
blinded_sig.1 .0.to_affine().to_compressed(),
blinded_sig.1 .1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap())
}
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar};
use ff::Field;
use group::Curve;
use rand::thread_rng;
use crate::error::{CoconutError, Result};
use crate::utils::hash_g1;
/// System-wide parameters used for the protocol
pub struct Parameters {
/// Generator of the G1 group
g1: G1Affine,
/// Additional generators of the G1 group
hs: Vec<G1Affine>,
/// Generator of the G2 group
g2: G2Affine,
/// Precomputed G2 generator used for the miller loop
_g2_prepared_miller: G2Prepared,
}
impl Parameters {
pub fn new(num_attributes: u32) -> Result<Parameters> {
if num_attributes == 0 {
return Err(CoconutError::Setup(
"Tried to setup the scheme for 0 attributes".to_string(),
));
}
let hs = (1..=num_attributes)
.map(|i| hash_g1(format!("h{}", i)).to_affine())
.collect();
Ok(Parameters {
g1: G1Affine::generator(),
hs,
g2: G2Affine::generator(),
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
})
}
pub(crate) fn gen1(&self) -> &G1Affine {
&self.g1
}
pub(crate) fn gen2(&self) -> &G2Affine {
&self.g2
}
pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared {
&self._g2_prepared_miller
}
pub(crate) fn gen_hs(&self) -> &[G1Affine] {
&self.hs
}
pub fn random_scalar(&self) -> Scalar {
// lazily-initialized thread-local random number generator, seeded by the system
let mut rng = thread_rng();
Scalar::random(&mut rng)
}
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
(0..n).map(|_| self.random_scalar()).collect()
}
}
pub fn setup(num_attributes: u32) -> Result<Parameters> {
Parameters::new(num_attributes)
}
// for ease of use in tests requiring params
// TODO: not sure if this will have to go away when tests require some specific number of generators
#[cfg(test)]
impl Default for Parameters {
fn default() -> Self {
Parameters {
g1: G1Affine::generator(),
hs: Vec::new(),
g2: G2Affine::generator(),
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
}
}
}
@@ -0,0 +1,296 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::ops::Neg;
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
use group::{Curve, Group};
use crate::error::{CoconutError, Result};
use crate::proofs::ProofKappaZeta;
use crate::scheme::setup::Parameters;
use crate::scheme::Signature;
use crate::scheme::VerificationKey;
use crate::traits::{Base58, Bytable};
use crate::utils::try_deserialize_g2_projective;
use crate::Attribute;
// TODO NAMING: this whole thing
// Theta
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Theta {
// blinded_message (kappa)
pub blinded_message: G2Projective,
// blinded serial number (zeta)
pub blinded_serial_number: G2Projective,
// sigma
pub credential: Signature,
// pi_v
pub pi_v: ProofKappaZeta,
}
impl TryFrom<&[u8]> for Theta {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<Theta> {
if bytes.len() < 288 {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()),
));
}
let blinded_message_bytes = bytes[..96].try_into().unwrap();
let blinded_message = try_deserialize_g2_projective(
&blinded_message_bytes,
CoconutError::Deserialization(
"failed to deserialize the blinded message (kappa)".to_string(),
),
)?;
let blinded_serial_number_bytes = bytes[96..192].try_into().unwrap();
let blinded_serial_number = try_deserialize_g2_projective(
&blinded_serial_number_bytes,
CoconutError::Deserialization(
"failed to deserialize the blinded serial number (zeta)".to_string(),
),
)?;
let credential = Signature::try_from(&bytes[192..288])?;
let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?;
Ok(Theta {
blinded_message,
blinded_serial_number,
credential,
pi_v,
})
}
}
impl Theta {
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
self.pi_v.verify(
params,
verification_key,
&self.blinded_message,
&self.blinded_serial_number,
)
}
// blinded message (kappa) || blinded serial number (zeta) || credential || pi_v
pub fn to_bytes(&self) -> Vec<u8> {
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed();
let credential_bytes = self.credential.to_bytes();
let proof_bytes = self.pi_v.to_bytes();
let mut bytes = Vec::with_capacity(288 + proof_bytes.len());
bytes.extend_from_slice(&blinded_message_bytes);
bytes.extend_from_slice(&blinded_serial_number_bytes);
bytes.extend_from_slice(&credential_bytes);
bytes.extend_from_slice(&proof_bytes);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Theta> {
Theta::try_from(bytes)
}
}
impl Bytable for Theta {
fn to_byte_vec(&self) -> Vec<u8> {
self.to_bytes()
}
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
Theta::try_from(slice)
}
}
impl Base58 for Theta {}
pub fn compute_kappa(
params: &Parameters,
verification_key: &VerificationKey,
private_attributes: &[Attribute],
blinding_factor: Scalar,
) -> G2Projective {
params.gen2() * blinding_factor
+ verification_key.alpha
+ private_attributes
.iter()
.zip(verification_key.beta.iter())
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>()
}
pub fn compute_zeta(params: &Parameters, serial_number: Attribute) -> G2Projective {
params.gen2() * serial_number
}
pub fn prove_bandwidth_credential(
params: &Parameters,
verification_key: &VerificationKey,
signature: &Signature,
serial_number: Attribute,
binding_number: Attribute,
) -> Result<Theta> {
if verification_key.beta.len() < 2 {
return Err(
CoconutError::Verification(
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
verification_key.beta.len()
)));
}
// Randomize the signature
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
// blinded_message : kappa in the paper.
// Value kappa is needed since we want to show a signature sigma'.
// In order to verify sigma' we need both the verification key vk and the message m.
// However, we do not want to reveal m to whomever we are showing the signature.
// Thus, we need kappa which allows us to verify sigma'. In particular,
// kappa is computed on m as input, but thanks to the use or random value r,
// it does not reveal any information about m.
let private_attributes = vec![serial_number, binding_number];
let blinded_message = compute_kappa(
params,
verification_key,
&private_attributes,
sign_blinding_factor,
);
// zeta is a commitment to the serial number (i.e., a public value associated with the serial number)
let blinded_serial_number = compute_zeta(params, serial_number);
let pi_v = ProofKappaZeta::construct(
params,
verification_key,
&serial_number,
&binding_number,
&sign_blinding_factor,
&blinded_message,
&blinded_serial_number,
);
Ok(Theta {
blinded_message,
blinded_serial_number,
credential: signature_prime,
pi_v,
})
}
/// Checks whether e(P, Q) * e(-R, S) == id
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
// checking e(P, Q) * e(-R, S) == id
// is equivalent to checking e(P, Q) == e(R, S)
// but requires only a single final exponentiation rather than two of them
// and therefore, as seen via benchmarks.rs, is almost 50% faster
// (1.47ms vs 2.45ms, tested on R9 5900X)
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
multi_miller.final_exponentiation().is_identity().into()
}
pub fn verify_credential(
params: &Parameters,
verification_key: &VerificationKey,
theta: &Theta,
public_attributes: &[Attribute],
) -> bool {
if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta.len() {
return false;
}
if !theta.verify_proof(params, verification_key) {
return false;
}
let kappa = if public_attributes.is_empty() {
theta.blinded_message
} else {
let signed_public_attributes = public_attributes
.iter()
.zip(
verification_key
.beta
.iter()
.skip(theta.pi_v.private_attributes_len()),
)
.map(|(pub_attr, beta_i)| beta_i * pub_attr)
.sum::<G2Projective>();
theta.blinded_message + signed_public_attributes
};
check_bilinear_pairing(
&theta.credential.0.to_affine(),
&G2Prepared::from(kappa.to_affine()),
&(theta.credential.1).to_affine(),
params.prepared_miller_g2(),
) && !bool::from(theta.credential.0.is_identity())
}
// Used in tests only
#[cfg(test)]
pub fn verify(
params: &Parameters,
verification_key: &VerificationKey,
public_attributes: &[Attribute],
sig: &Signature,
) -> bool {
let kappa = (verification_key.alpha
+ public_attributes
.iter()
.zip(verification_key.beta.iter())
.map(|(m_i, b_i)| b_i * m_i)
.sum::<G2Projective>())
.to_affine();
check_bilinear_pairing(
&sig.0.to_affine(),
&G2Prepared::from(kappa),
&sig.1.to_affine(),
params.prepared_miller_g2(),
) && !bool::from(sig.0.is_identity())
}
#[cfg(test)]
mod tests {
use crate::scheme::keygen::keygen;
use crate::scheme::setup::setup;
use super::*;
#[test]
fn theta_bytes_roundtrip() {
let mut params = setup(2).unwrap();
let keypair = keygen(&mut params);
let r = params.random_scalar();
let s = params.random_scalar();
let signature = Signature(params.gen1() * r, params.gen1() * s);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let theta = prove_bandwidth_credential(
&mut params,
&keypair.verification_key(),
&signature,
serial_number,
binding_number,
)
.unwrap();
let bytes = theta.to_bytes();
assert_eq!(Theta::try_from(bytes.as_slice()).unwrap(), theta);
}
}
+106
View File
@@ -0,0 +1,106 @@
use crate::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
CoconutError, Signature, SignatureShare, VerificationKey,
};
#[test]
fn main() -> Result<(), CoconutError> {
let params = setup(5)?;
let public_attributes = params.n_random_scalars(2);
let serial_number = params.random_scalar();
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let elgamal_keypair = elgamal_keygen(&params);
// generate commitment and encryption
let blind_sign_request = prepare_blind_sign(
&params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)?;
// generate_keys
let coconut_keypairs = ttp_keygen(&params, 2, 3)?;
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// aggregate verification keys
let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?;
// generate blinded signatures
let mut blinded_signatures = Vec::new();
for keypair in coconut_keypairs {
let blinded_signature = blind_sign(
&params,
&keypair.secret_key(),
&elgamal_keypair.public_key(),
&blind_sign_request,
&public_attributes,
)?;
blinded_signatures.push(blinded_signature)
}
// Unblind
let unblinded_signatures: Vec<Signature> = blinded_signatures
.into_iter()
.zip(verification_keys.iter())
.map(|(signature, verification_key)| {
signature
.unblind(
&params,
&elgamal_keypair.private_key(),
&verification_key,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
)
.unwrap()
})
.collect();
// Aggregate signatures
let signature_shares: Vec<SignatureShare> = unblinded_signatures
.iter()
.enumerate()
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
.collect();
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
attributes.extend_from_slice(&private_attributes);
attributes.extend_from_slice(&public_attributes);
// Randomize credentials and generate any cryptographic material to verify them
let signature =
aggregate_signature_shares(&params, &verification_key, &attributes, &signature_shares)?;
// Generate cryptographic material to verify them
let theta = prove_bandwidth_credential(
&params,
&verification_key,
&signature,
serial_number,
binding_number,
)?;
// Verify credentials
assert!(verify_credential(
&params,
&verification_key,
&theta,
&public_attributes,
));
Ok(())
}
+1
View File
@@ -0,0 +1 @@
mod e2e;
+22
View File
@@ -0,0 +1,22 @@
use crate::CoconutError;
pub trait Bytable
where
Self: Sized,
{
fn to_byte_vec(&self) -> Vec<u8>;
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError>;
}
pub trait Base58
where
Self: Bytable,
{
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap())
}
fn to_bs58(&self) -> String {
bs58::encode(self.to_byte_vec()).into_string()
}
}
+386
View File
@@ -0,0 +1,386 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use core::iter::Sum;
use core::ops::Mul;
use std::convert::TryInto;
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
use ff::Field;
use crate::error::{CoconutError, Result};
use crate::scheme::setup::Parameters;
use crate::scheme::SignerIndex;
pub struct Polynomial {
coefficients: Vec<Scalar>,
}
impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
pub fn new_random(params: &Parameters, degree: u64) -> Self {
Polynomial {
coefficients: params.n_random_scalars((degree + 1) as usize),
}
}
/// Evaluates the polynomial at point x.
pub fn evaluate(&self, x: &Scalar) -> Scalar {
if self.coefficients.is_empty() {
Scalar::zero()
// if x is zero then we can ignore most of the expensive computation and
// just return the last term of the polynomial
} else if x.is_zero() {
// we checked that coefficients are not empty so unwrap here is fine
*self.coefficients.first().unwrap()
} else {
self.coefficients
.iter()
.enumerate()
// coefficient[n] * x ^ n
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
.sum()
}
}
}
#[inline]
fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
let x = Scalar::zero();
points
.iter()
.enumerate()
.map(|(i, point_i)| {
let mut numerator = Scalar::one();
let mut denominator = Scalar::one();
let xi = Scalar::from(*point_i);
for (j, point_j) in points.iter().enumerate() {
if j != i {
let xj = Scalar::from(*point_j);
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
numerator *= x - xj;
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
denominator *= xi - xj;
}
}
// numerator / denominator
numerator * denominator.invert().unwrap()
})
.collect()
}
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
/// It can be used for Scalars, G1 and G2 points.
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
points: &[SignerIndex],
values: &[T],
) -> Result<T>
where
T: Sum,
for<'a> &'a T: Mul<Scalar, Output = T>,
{
if points.is_empty() || values.is_empty() {
return Err(CoconutError::Interpolation(
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
));
}
if points.len() != values.len() {
return Err(CoconutError::Interpolation(
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
.to_string(),
));
}
let coefficients = generate_lagrangian_coefficients_at_origin(points);
Ok(coefficients
.into_iter()
.zip(values.iter())
.map(|(coeff, val)| val * coeff)
.sum())
}
// A temporary way of hashing particular message into G1.
// Implementation idea was taken from `threshold_crypto`:
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
// Eventually it should get replaced by, most likely, the osswu map
// method once ideally it's implemented inside the pairing crate.
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
pub(crate) fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
}
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
let mut output = vec![Scalar::zero()];
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
msg.as_ref(),
SCALAR_HASH_DOMAIN,
&mut output,
);
output[0]
}
pub(crate) fn try_deserialize_scalar_vec(
expected_len: u64,
bytes: &[u8],
err: CoconutError,
) -> Result<Vec<Scalar>> {
if bytes.len() != expected_len as usize * 32 {
return Err(err);
}
let mut out = Vec::with_capacity(expected_len as usize);
for i in 0..expected_len as usize {
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
None => return Err(err),
Some(scalar) => scalar,
};
out.push(s)
}
Ok(out)
}
pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
}
pub(crate) fn try_deserialize_g1_projective(
bytes: &[u8; 48],
err: CoconutError,
) -> Result<G1Projective> {
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
.ok_or(err)
.map(G1Projective::from)
}
pub(crate) fn try_deserialize_g2_projective(
bytes: &[u8; 96],
err: CoconutError,
) -> Result<G2Projective> {
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
.ok_or(err)
.map(G2Projective::from)
}
// use core::fmt;
// #[cfg(feature = "serde")]
// use serde::de::Visitor;
// #[cfg(feature = "serde")]
// use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
//
// // #[cfg(feature = "serde")]
// #[serde(remote = "Scalar")]
// pub(crate) struct ScalarDef(pub Scalar);
//
// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
//
// impl Serialize for ScalarDef {
// fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
// where
// S: Serializer,
// {
// use serde::ser::SerializeTuple;
// let mut tup = serializer.serialize_tuple(32)?;
// for byte in self.0.to_bytes().iter() {
// tup.serialize_element(byte)?;
// }
// tup.end()
// }
// }
//
// impl<'de> Deserialize<'de> for ScalarDef {
// fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
// where
// D: Deserializer<'de>,
// {
// struct ScalarVisitor;
//
// impl<'de> Visitor<'de> for ScalarVisitor {
// type Value = ScalarDef;
//
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
// formatter.write_str("a 32-byte canonical bls12_381 scalar")
// }
//
// fn visit_seq<A>(self, mut seq: A) -> core::result::Result<ScalarDef, A::Error>
// where
// A: serde::de::SeqAccess<'de>,
// {
// let mut bytes = [0u8; 32];
// for i in 0..32 {
// bytes[i] = seq
// .next_element()?
// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?;
// }
//
// let res = Scalar::from_bytes(&bytes);
// if res.is_some().into() {
// Ok(ScalarDef(res.unwrap()))
// } else {
// Err(serde::de::Error::custom(
// &"scalar was not canonically encoded",
// ))
// }
// }
// }
//
// deserializer.deserialize_tuple(32, ScalarVisitor)
// }
// }
//
// #[cfg(feature = "serde")]
// pub(crate) struct G1ProjectiveSerdeHelper(Scalar);
//
// #[cfg(feature = "serde")]
// pub(crate) struct G2ProjectiveSerdeHelper(Scalar);
#[cfg(test)]
mod tests {
use rand::RngCore;
use super::*;
#[test]
fn polynomial_evaluation() {
// y = 42 (it should be 42 regardless of x)
let poly = Polynomial {
coefficients: vec![Scalar::from(42)],
};
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
// y = x + 10, at x = 2 (exp: 12)
let poly = Polynomial {
coefficients: vec![Scalar::from(10), Scalar::from(1)],
};
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
let poly = Polynomial {
coefficients: vec![
(-Scalar::from(3)),
Scalar::from(2),
(-Scalar::from(5)),
Scalar::zero(),
Scalar::from(1),
],
};
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
// empty polynomial
let poly = Polynomial {
coefficients: vec![],
};
// should always be 0
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
}
#[test]
fn performing_lagrangian_scalar_interpolation_at_origin() {
// x^2 + 3
// x, f(x):
// 1, 4,
// 2, 7,
// 3, 12,
let points = vec![1, 2, 3];
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
assert_eq!(
Scalar::from(3),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// x^3 + 3x^2 - 5x + 11
// x, f(x):
// 1, 10
// 2, 21
// 3, 50
// 4, 103
let points = vec![1, 2, 3, 4];
let values = vec![
Scalar::from(10),
Scalar::from(21),
Scalar::from(50),
Scalar::from(103),
];
assert_eq!(
Scalar::from(11),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
// more points than it is required
// x^2 + x + 10
// x, f(x)
// 1, 12
// 2, 16
// 3, 22
// 4, 30
// 5, 40
let points = vec![1, 2, 3, 4, 5];
let values = vec![
Scalar::from(12),
Scalar::from(16),
Scalar::from(22),
Scalar::from(30),
Scalar::from(40),
];
assert_eq!(
Scalar::from(10),
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
);
}
#[test]
fn hash_g1_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_g1(msg1), hash_g1(msg1));
assert_eq!(hash_g1(msg2), hash_g1(msg2));
assert_ne!(hash_g1(msg1), hash_g1(msg2));
}
#[test]
fn hash_scalar_sanity_check() {
let mut rng = rand::thread_rng();
let mut msg1 = [0u8; 1024];
rng.fill_bytes(&mut msg1);
let mut msg2 = [0u8; 1024];
rng.fill_bytes(&mut msg2);
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
}
}
+2 -2
View File
@@ -22,9 +22,9 @@ const CLIENT_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH;
#[derive(Debug)]
pub enum RecipientFormattingError {
MalformedRecipientError,
MalformedIdentityError(identity::KeyRecoveryError),
MalformedIdentityError(identity::Ed25519RecoveryError),
MalformedEncryptionKeyError(encryption::KeyRecoveryError),
MalformedGatewayError(identity::KeyRecoveryError),
MalformedGatewayError(identity::Ed25519RecoveryError),
}
impl fmt::Display for RecipientFormattingError {
+4 -4
View File
@@ -13,7 +13,7 @@ use std::net::SocketAddr;
#[derive(Debug)]
pub enum GatewayConversionError {
InvalidIdentityKey(identity::KeyRecoveryError),
InvalidIdentityKey(identity::Ed25519RecoveryError),
InvalidSphinxKey(encryption::KeyRecoveryError),
InvalidAddress(String, io::Error),
InvalidStake,
@@ -26,8 +26,8 @@ impl From<encryption::KeyRecoveryError> for GatewayConversionError {
}
}
impl From<identity::KeyRecoveryError> for GatewayConversionError {
fn from(err: identity::KeyRecoveryError) -> Self {
impl From<identity::Ed25519RecoveryError> for GatewayConversionError {
fn from(err: identity::Ed25519RecoveryError) -> Self {
GatewayConversionError::InvalidIdentityKey(err)
}
}
@@ -125,7 +125,7 @@ impl<'a> TryFrom<&'a GatewayBond> for Node {
Ok(Node {
owner: bond.owner.as_str().to_owned(),
stake: bond.bond_amount.amount.into(),
stake: bond.pledge_amount.amount.into(),
location: bond.gateway.location.clone(),
host,
mix_host,
+4 -4
View File
@@ -13,7 +13,7 @@ use std::net::SocketAddr;
#[derive(Debug)]
pub enum MixnodeConversionError {
InvalidIdentityKey(identity::KeyRecoveryError),
InvalidIdentityKey(identity::Ed25519RecoveryError),
InvalidSphinxKey(encryption::KeyRecoveryError),
InvalidAddress(String, io::Error),
InvalidStake,
@@ -26,8 +26,8 @@ impl From<encryption::KeyRecoveryError> for MixnodeConversionError {
}
}
impl From<identity::KeyRecoveryError> for MixnodeConversionError {
fn from(err: identity::KeyRecoveryError) -> Self {
impl From<identity::Ed25519RecoveryError> for MixnodeConversionError {
fn from(err: identity::Ed25519RecoveryError) -> Self {
MixnodeConversionError::InvalidIdentityKey(err)
}
}
@@ -117,7 +117,7 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node {
Ok(Node {
owner: bond.owner.as_str().to_owned(),
stake: bond.bond_amount.amount.into(),
stake: bond.pledge_amount.amount.into(),
delegation: bond.total_delegation.amount.into(),
host,
mix_host,
+1641
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[workspace]
members = ["erc20-bridge", "mixnet", "vesting"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
+12 -8
View File
@@ -89,8 +89,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c16b255449b3f5cd7fa4b79acd5225b5185655261087a3d8aaac44f88a0e23e9"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -101,16 +102,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abad1a6ff427a2f66890a4dce6354b4563cd07cee91a942300e011c921c09ed2"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-std"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1660ee3d5734672e1eb4f0ceda403e2d83345e15143a48845f340f3252ce99a6"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -124,8 +127,9 @@ dependencies = [
[[package]]
name = "cosmwasm-storage"
version = "0.14.1"
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
version = "1.0.0-beta2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3b4efe3b4f86df668520a02e9a29c23eea99b64dfcacb0e59b98346418af7f"
dependencies = [
"cosmwasm-std",
"serde",
+2 -16
View File
@@ -5,22 +5,9 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace] # adding a blank workspace to keep it out of the global workspace.
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
@@ -31,9 +18,8 @@ config = { path = "../../common/config"}
[dependencies]
erc20-bridge-contract = { path = "../../common/erc20-bridge-contract" }
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
cosmwasm-storage = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
cosmwasm-std = "1.0.0-beta2"
cosmwasm-storage = "1.0.0-beta2"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+1 -1
View File
@@ -68,7 +68,7 @@ pub mod tests {
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies(&[]);
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {};
let info = mock_info("creator", &[]);
+1 -1
View File
@@ -11,7 +11,7 @@ pub mod helpers {
use erc20_bridge_contract::payment::Payment;
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies(&[]);
let mut deps = mock_dependencies();
let msg = InstantiateMsg {};
let env = mock_env();
let info = mock_info("creator", &[]);
+531 -524
View File
File diff suppressed because it is too large Load Diff
+12 -20
View File
@@ -12,22 +12,9 @@ exclude = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace] # adding a blank workspace to keep it out of the global workspace.
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = 'abort'
incremental = false
overflow-checks = true
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
@@ -35,18 +22,23 @@ backtraces = ["cosmwasm-std/backtraces"]
[dependencies]
mixnet-contract = { path = "../../common/mixnet-contract" }
config = { path = "../../common/config"}
vesting-contract = { path = "../vesting" }
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
cosmwasm-storage = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
#cosmwasm-std = { version = "0.14.1", features = ["iterator"] }
#cosmwasm-storage = { version = "0.14.1", features = ["iterator"] }
cosmwasm-std = "1.0.0-beta2"
cosmwasm-storage = "1.0.0-beta2"
cw-storage-plus = "0.10.3"
bs58 = "0.4.0"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
[dev-dependencies]
cosmwasm-schema = { version = "0.14.0" }
cosmwasm-schema = "1.0.0-beta2"
fixed = "1.1"
rand_chacha = "0.2"
rand = "0.7"
crypto = { path = "../../common/crypto" }
[build-dependencies]
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use vergen::{vergen, Config};
fn main() {
vergen(Config::default()).expect("failed to extract build metadata")
}
-2
View File
@@ -5,7 +5,6 @@ extern crate mixnet_contract;
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
use mixnet_contract::{ExecuteMsg, InstantiateMsg, MixNodeBond, QueryMsg};
use mixnet_contracts::state::State;
use std::env::current_dir;
use std::fs::create_dir_all;
@@ -18,6 +17,5 @@ fn main() {
export_schema(&schema_for!(InstantiateMsg), &out_dir);
export_schema(&schema_for!(ExecuteMsg), &out_dir);
export_schema(&schema_for!(QueryMsg), &out_dir);
export_schema(&schema_for!(State), &out_dir);
export_schema(&schema_for!(MixNodeBond), &out_dir);
}
+3 -3
View File
@@ -69,10 +69,10 @@
{
"type": "object",
"required": [
"update_state_params"
"update_contract_settings"
],
"properties": {
"update_state_params": {
"update_contract_settings": {
"$ref": "#/definitions/StateParams"
}
},
@@ -346,4 +346,4 @@
"type": "string"
}
}
}
}
+3 -3
View File
@@ -101,10 +101,10 @@
{
"type": "object",
"required": [
"state_params"
"contract_settings_params"
],
"properties": {
"state_params": {
"contract_settings_params": {
"type": "object"
}
},
@@ -321,4 +321,4 @@
"type": "string"
}
}
}
}
+183 -103
View File
@@ -1,30 +1,35 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::u128;
use crate::helpers::calculate_epoch_reward_rate;
use crate::state::State;
use crate::storage::{config, layer_distribution};
use crate::{error::ContractError, queries, transactions};
use config::defaults::REWARDING_VALIDATOR_ADDRESS;
use cosmwasm_std::{
entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse,
Response, Uint128,
use crate::delegations::queries::query_all_network_delegations_paged;
use crate::delegations::queries::query_delegator_delegations_paged;
use crate::delegations::queries::query_mixnode_delegation;
use crate::delegations::queries::query_mixnode_delegations_paged;
use crate::error::ContractError;
use crate::gateways::queries::query_gateways_paged;
use crate::gateways::queries::query_owns_gateway;
use crate::mixnet_contract_settings::models::ContractState;
use crate::mixnet_contract_settings::queries::query_rewarding_interval;
use crate::mixnet_contract_settings::queries::{
query_contract_settings_params, query_contract_version,
};
use mixnet_contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StateParams};
pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::bonding_queries as mixnode_queries;
use crate::mixnodes::bonding_queries::query_mixnodes_paged;
use crate::mixnodes::layer_queries::query_layer_distribution;
use crate::rewards::queries::query_reward_pool;
use crate::rewards::queries::{query_circulating_supply, query_rewarding_status};
use crate::rewards::storage as rewards_storage;
use cosmwasm_std::{
entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128,
};
use mixnet_contract::{ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
/// Constant specifying minimum of coin required to bond a gateway
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
pub const INITIAL_GATEWAY_PLEDGE: Uint128 = Uint128::new(100_000_000);
/// Constant specifying minimum of coin required to bond a mixnode
pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000);
// percentage annual increase. Given starting value of x, we expect to have 1.1x at the end of the year
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110;
pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110;
pub const INITIAL_MIXNODE_PLEDGE: Uint128 = Uint128::new(100_000_000);
pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200;
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
@@ -32,37 +37,26 @@ pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000;
pub const EPOCH_REWARD_PERCENT: u8 = 2; // Used to calculate epoch reward pool
pub const DEFAULT_SYBIL_RESISTANCE_PERCENT: u8 = 30;
pub const DEFAULT_ACTIVE_SET_WORK_FACTOR: u8 = 10;
// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms
pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000;
fn default_initial_state(owner: Addr, env: Env) -> State {
let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE);
let mixnode_delegation_reward_rate = Decimal::percent(INITIAL_MIXNODE_DELEGATION_REWARD_RATE);
State {
fn default_initial_state(
owner: Addr,
rewarding_validator_address: Addr,
env: Env,
) -> ContractState {
ContractState {
owner,
rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value
params: StateParams {
epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH,
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
mixnode_bond_reward_rate,
mixnode_delegation_reward_rate,
rewarding_validator_address,
params: ContractStateParams {
minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE,
minimum_gateway_pledge: INITIAL_GATEWAY_PLEDGE,
mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE,
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
active_set_work_factor: DEFAULT_ACTIVE_SET_WORK_FACTOR,
},
rewarding_interval_starting_block: env.block.height,
latest_rewarding_interval_nonce: 0,
rewarding_in_progress: false,
mixnode_epoch_bond_reward: calculate_epoch_reward_rate(
INITIAL_DEFAULT_EPOCH_LENGTH,
mixnode_bond_reward_rate,
),
mixnode_epoch_delegation_reward: calculate_epoch_reward_rate(
INITIAL_DEFAULT_EPOCH_LENGTH,
mixnode_delegation_reward_rate,
),
}
}
@@ -76,12 +70,15 @@ pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
_msg: InstantiateMsg,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let state = default_initial_state(info.sender, env);
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
let state = default_initial_state(info.sender, rewarding_validator_address, env);
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?;
mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?;
rewards_storage::REWARD_POOL.save(deps.storage, &Uint128::new(INITIAL_REWARD_POOL))?;
config(deps.storage).save(&state)?;
layer_distribution(deps.storage).save(&Default::default())?;
Ok(Response::default())
}
@@ -94,34 +91,42 @@ pub fn execute(
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::BondMixnode { mix_node } => {
transactions::try_add_mixnode(deps, env, info, mix_node)
}
ExecuteMsg::UnbondMixnode {} => transactions::try_remove_mixnode(deps, info),
ExecuteMsg::BondGateway { gateway } => {
transactions::try_add_gateway(deps, env, info, gateway)
}
ExecuteMsg::UnbondGateway {} => transactions::try_remove_gateway(deps, info),
ExecuteMsg::UpdateStateParams(params) => {
transactions::try_update_state_params(deps, info, params)
}
ExecuteMsg::RewardMixnode {
identity,
uptime,
rewarding_interval_nonce,
} => transactions::try_reward_mixnode(
ExecuteMsg::BondMixnode {
mix_node,
owner_signature,
} => crate::mixnodes::transactions::try_add_mixnode(
deps,
env,
info,
identity,
uptime,
rewarding_interval_nonce,
mix_node,
owner_signature,
),
ExecuteMsg::RewardMixnodeV2 {
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, info)
}
ExecuteMsg::BondGateway {
gateway,
owner_signature,
} => crate::gateways::transactions::try_add_gateway(
deps,
env,
info,
gateway,
owner_signature,
),
ExecuteMsg::UnbondGateway {} => {
crate::gateways::transactions::try_remove_gateway(deps, info)
}
ExecuteMsg::UpdateContractStateParams(params) => {
crate::mixnet_contract_settings::transactions::try_update_contract_settings(
deps, info, params,
)
}
ExecuteMsg::RewardMixnode {
identity,
params,
rewarding_interval_nonce,
} => transactions::try_reward_mixnode_v2(
} => crate::rewards::transactions::try_reward_mixnode(
deps,
env,
info,
@@ -130,88 +135,161 @@ pub fn execute(
rewarding_interval_nonce,
),
ExecuteMsg::DelegateToMixnode { mix_identity } => {
transactions::try_delegate_to_mixnode(deps, env, info, mix_identity)
crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity)
}
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity)
crate::delegations::transactions::try_remove_delegation_from_mixnode(
deps,
info,
mix_identity,
)
}
ExecuteMsg::BeginMixnodeRewarding {
rewarding_interval_nonce,
} => transactions::try_begin_mixnode_rewarding(deps, env, info, rewarding_interval_nonce),
} => crate::rewards::transactions::try_begin_mixnode_rewarding(
deps,
env,
info,
rewarding_interval_nonce,
),
ExecuteMsg::FinishMixnodeRewarding {
rewarding_interval_nonce,
} => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce),
} => crate::rewards::transactions::try_finish_mixnode_rewarding(
deps,
info,
rewarding_interval_nonce,
),
ExecuteMsg::RewardNextMixDelegators {
mix_identity,
rewarding_interval_nonce,
} => crate::rewards::transactions::try_reward_next_mixnode_delegators(
deps,
info,
mix_identity,
rewarding_interval_nonce,
),
ExecuteMsg::DelegateToMixnodeOnBehalf {
mix_identity,
delegate,
} => crate::delegations::transactions::try_delegate_to_mixnode_on_behalf(
deps,
env,
info,
mix_identity,
delegate,
),
ExecuteMsg::UndelegateFromMixnodeOnBehalf {
mix_identity,
delegate,
} => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf(
deps,
info,
mix_identity,
delegate,
),
ExecuteMsg::BondMixnodeOnBehalf {
mix_node,
owner,
owner_signature,
} => crate::mixnodes::transactions::try_add_mixnode_on_behalf(
deps,
env,
info,
mix_node,
owner,
owner_signature,
),
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner)
}
ExecuteMsg::BondGatewayOnBehalf {
gateway,
owner,
owner_signature,
} => crate::gateways::transactions::try_add_gateway_on_behalf(
deps,
env,
info,
gateway,
owner,
owner_signature,
),
ExecuteMsg::UnbondGatewayOnBehalf { owner } => {
crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner)
}
}
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let query_res = match msg {
QueryMsg::GetContractVersion {} => to_binary(&query_contract_version()),
QueryMsg::GetMixNodes { start_after, limit } => {
to_binary(&queries::query_mixnodes_paged(deps, start_after, limit)?)
to_binary(&query_mixnodes_paged(deps, start_after, limit)?)
}
QueryMsg::GetGateways { limit, start_after } => {
to_binary(&queries::query_gateways_paged(deps, start_after, limit)?)
to_binary(&query_gateways_paged(deps, start_after, limit)?)
}
QueryMsg::OwnsMixnode { address } => {
to_binary(&queries::query_owns_mixnode(deps, address)?)
to_binary(&mixnode_queries::query_owns_mixnode(deps, address)?)
}
QueryMsg::OwnsGateway { address } => {
to_binary(&queries::query_owns_gateway(deps, address)?)
}
QueryMsg::StateParams {} => to_binary(&queries::query_state_params(deps)),
QueryMsg::CurrentRewardingInterval {} => {
to_binary(&queries::query_rewarding_interval(deps))
}
QueryMsg::LayerDistribution {} => to_binary(&queries::query_layer_distribution(deps)),
QueryMsg::GetMixDelegations {
QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?),
QueryMsg::StateParams {} => to_binary(&query_contract_settings_params(deps)?),
QueryMsg::CurrentRewardingInterval {} => to_binary(&query_rewarding_interval(deps)?),
QueryMsg::LayerDistribution {} => to_binary(&query_layer_distribution(deps)?),
QueryMsg::GetMixnodeDelegations {
mix_identity,
start_after,
limit,
} => to_binary(&queries::query_mixnode_delegations_paged(
} => to_binary(&query_mixnode_delegations_paged(
deps,
mix_identity,
start_after,
limit,
)?),
QueryMsg::GetAllMixDelegations { start_after, limit } => to_binary(
&queries::query_all_mixnode_delegations_paged(deps, start_after, limit)?,
QueryMsg::GetAllNetworkDelegations { start_after, limit } => to_binary(
&query_all_network_delegations_paged(deps, start_after, limit)?,
),
QueryMsg::GetReverseMixDelegations {
delegation_owner,
QueryMsg::GetDelegatorDelegations {
delegator: delegation_owner,
start_after,
limit,
} => to_binary(&queries::query_reverse_mixnode_delegations_paged(
} => to_binary(&query_delegator_delegations_paged(
deps,
delegation_owner,
start_after,
limit,
)?),
QueryMsg::GetMixDelegation {
QueryMsg::GetDelegationDetails {
mix_identity,
address,
} => to_binary(&queries::query_mixnode_delegation(
deps,
mix_identity,
address,
)?),
QueryMsg::GetRewardPool {} => to_binary(&queries::query_reward_pool(deps)),
QueryMsg::GetCirculatingSupply {} => to_binary(&queries::query_circulating_supply(deps)),
delegator,
} => to_binary(&query_mixnode_delegation(deps, mix_identity, delegator)?),
QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?),
QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?),
QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT),
QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT),
QueryMsg::GetRewardingStatus {
mix_identity,
rewarding_interval_nonce,
} => to_binary(&query_rewarding_status(
deps,
mix_identity,
rewarding_interval_nonce,
)?),
};
Ok(query_res?)
}
#[entry_point]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
todo!("ACTIVE_STATE_WORK_FACTOR to State");
// Ok(Default::default())
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::support::tests::helpers::*;
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary};
@@ -219,9 +297,11 @@ pub mod tests {
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies(&[]);
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {};
let msg = InstantiateMsg {
rewarding_validator_address: config::defaults::REWARDING_VALIDATOR_ADDRESS.to_string(),
};
let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
@@ -243,7 +323,7 @@ pub mod tests {
// Contract balance should match what we initialized it as
assert_eq!(
coins(0, DENOM),
query_contract_balance(env.contract.address, deps)
test_helpers::query_contract_balance(env.contract.address, deps)
);
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod queries;
pub(crate) mod storage;
pub(crate) mod transactions;
+649
View File
@@ -0,0 +1,649 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use crate::error::ContractError;
use cosmwasm_std::Deps;
use cosmwasm_std::Order;
use cosmwasm_std::StdResult;
use cw_storage_plus::{Bound, PrimaryKey};
use mixnet_contract::PagedAllDelegationsResponse;
use mixnet_contract::PagedDelegatorDelegationsResponse;
use mixnet_contract::PagedMixDelegationsResponse;
use mixnet_contract::{Delegation, IdentityKey};
pub(crate) fn query_all_network_delegations_paged(
deps: Deps,
start_after: Option<(IdentityKey, String)>,
limit: Option<u32>,
) -> StdResult<PagedAllDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|start| start.joined_key())
.map(Bound::exclusive);
let delegations = storage::delegations()
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations
.last()
.map(|delegation| delegation.storage_key());
Ok(PagedAllDelegationsResponse::new(
delegations,
start_next_after,
))
}
pub(crate) fn query_delegator_delegations_paged(
deps: Deps,
delegation_owner: String,
start_after: Option<IdentityKey>,
limit: Option<u32>,
) -> StdResult<PagedDelegatorDelegationsResponse> {
let validated_owner = deps.api.addr_validate(&delegation_owner)?;
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|mix_identity| Bound::Exclusive((mix_identity, validated_owner.clone()).joined_key()));
let delegations = storage::delegations()
.idx
.owner
.prefix(validated_owner)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations
.last()
.map(|delegation| delegation.node_identity());
Ok(PagedDelegatorDelegationsResponse::new(
delegations,
start_next_after,
))
}
// queries for delegation value of given address for particular node
pub(crate) fn query_mixnode_delegation(
deps: Deps,
mix_identity: IdentityKey,
delegator: String,
) -> Result<Delegation, ContractError> {
let validated_delegator = deps.api.addr_validate(&delegator)?;
let storage_key = (mix_identity.clone(), validated_delegator.clone()).joined_key();
storage::delegations()
.may_load(deps.storage, storage_key)?
.ok_or(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: validated_delegator,
})
}
pub(crate) fn query_mixnode_delegations_paged(
deps: Deps,
mix_identity: IdentityKey,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedMixDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?
.map(|addr| Bound::Exclusive((mix_identity.clone(), addr).joined_key()));
let delegations = storage::delegations()
.idx
.mixnode
.prefix(mix_identity)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations.last().map(|delegation| delegation.owner());
Ok(PagedMixDelegationsResponse::new(
delegations,
start_next_after,
))
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr, Storage};
pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &str) {
for i in 0..n {
let address = format!("address{}", i);
test_helpers::save_dummy_delegation(storage, node_identity, address);
}
}
#[cfg(test)]
mod querying_for_mixnode_delegations_paged {
use super::*;
use mixnet_contract::IdentityKey;
#[test]
fn retrieval_obeys_limits() {
let mut deps = test_helpers::init_contract();
let limit = 2;
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(100, &mut deps.storage, &node_identity);
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
None,
Option::from(limit),
)
.unwrap();
assert_eq!(limit, page1.delegations.len() as u32);
}
#[test]
fn retrieval_has_default_limit() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&node_identity,
);
// query without explicitly setting a limit
let page1 =
query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None).unwrap();
assert_eq!(
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
page1.delegations.len() as u32
);
}
#[test]
fn retrieval_has_max_limit() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&node_identity,
);
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
None,
Option::from(crazy_limit),
)
.unwrap();
// we default to a decent sized upper bound instead
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.delegations.len() as u32);
}
#[test]
fn pagination_works() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "100");
let per_page = 2;
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
None,
Option::from(per_page),
)
.unwrap();
// page should have 1 result on it
assert_eq!(1, page1.delegations.len());
// save another
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "200");
// page1 should have 2 results on it
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "300");
// page1 still has 2 results
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
assert_eq!("200".to_string(), page1.start_next_after.unwrap());
// retrieving the next page should start after the last key on this page
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.delegations.len());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "400");
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
}
}
#[cfg(test)]
mod querying_for_all_mixnode_delegations_paged {
use super::*;
use crate::support::tests::test_helpers;
use mixnet_contract::IdentityKey;
#[test]
fn retrieval_obeys_limits() {
let mut deps = test_helpers::init_contract();
let limit = 2;
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(100, &mut deps.storage, &node_identity);
let page1 =
query_all_network_delegations_paged(deps.as_ref(), None, Option::from(limit))
.unwrap();
assert_eq!(limit, page1.delegations.len() as u32);
}
#[test]
fn retrieval_has_default_limit() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&node_identity,
);
// query without explicitly setting a limit
let page1 = query_all_network_delegations_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
page1.delegations.len() as u32
);
}
#[test]
fn retrieval_has_max_limit() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&node_identity,
);
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
let page1 =
query_all_network_delegations_paged(deps.as_ref(), None, Option::from(crazy_limit))
.unwrap();
// we default to a decent sized upper bound instead
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.delegations.len() as u32);
}
#[test]
fn pagination_works() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "100");
let per_page = 2;
let page1 =
query_all_network_delegations_paged(deps.as_ref(), None, Option::from(per_page))
.unwrap();
// page should have 1 result on it
assert_eq!(1, page1.delegations.len());
// save another
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "200");
// page1 should have 2 results on it
let page1 =
query_all_network_delegations_paged(deps.as_ref(), None, Option::from(per_page))
.unwrap();
assert_eq!(2, page1.delegations.len());
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "300");
// page1 still has 2 results
let page1 =
query_all_network_delegations_paged(deps.as_ref(), None, Option::from(per_page))
.unwrap();
assert_eq!(2, page1.delegations.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_all_network_delegations_paged(
deps.as_ref(),
Option::from(start_after.clone()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.delegations.len());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "400");
let page2 = query_all_network_delegations_paged(
deps.as_ref(),
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
}
}
#[test]
fn mix_deletion_query_returns_current_delegation_value() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
let delegation_owner = Addr::unchecked("bar");
let delegation = Delegation::new(
delegation_owner.clone(),
node_identity.clone(),
coin(1234, DENOM),
1234,
None,
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.unwrap();
assert_eq!(
Ok(delegation),
query_mixnode_delegation(deps.as_ref(), node_identity, delegation_owner.to_string())
)
}
#[test]
fn mix_deletion_query_returns_error_if_delegation_doesnt_exist() {
let mut deps = test_helpers::init_contract();
let node_identity1: IdentityKey = "foo1".into();
let node_identity2: IdentityKey = "foo2".into();
let delegation_owner1 = Addr::unchecked("bar");
let delegation_owner2 = Addr::unchecked("bar2");
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
}),
query_mixnode_delegation(
deps.as_ref(),
node_identity1.clone(),
delegation_owner1.to_string()
)
);
// add delegation from a different address
let delegation = Delegation::new(
delegation_owner2.clone(),
node_identity1.clone(),
coin(1234, DENOM),
1234,
None,
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
}),
query_mixnode_delegation(
deps.as_ref(),
node_identity1.clone(),
delegation_owner1.to_string()
)
);
// add delegation for a different node
let delegation = Delegation::new(
delegation_owner1.clone(),
node_identity2.clone(),
coin(1234, DENOM),
1234,
None,
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: Addr::unchecked(delegation_owner1.clone())
}),
query_mixnode_delegation(
deps.as_ref(),
node_identity1.clone(),
delegation_owner1.to_string()
)
)
}
#[cfg(test)]
mod querying_for_reverse_mixnode_delegations_paged {
use super::*;
fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &str) {
for i in 0..n {
let node_identity = format!("node{}", i);
test_helpers::save_dummy_delegation(storage, node_identity, delegation_owner);
}
}
#[test]
fn retrieval_obeys_limits() {
let mut deps = test_helpers::init_contract();
let limit = 2;
let delegation_owner = "foo".to_string();
store_n_reverse_delegations(100, &mut deps.storage, &delegation_owner);
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner,
None,
Option::from(limit),
)
.unwrap();
assert_eq!(limit, page1.delegations.len() as u32);
}
#[test]
fn retrieval_has_default_limit() {
let mut deps = test_helpers::init_contract();
let delegation_owner = "foo".to_string();
store_n_reverse_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&delegation_owner,
);
// query without explicitly setting a limit
let page1 =
query_delegator_delegations_paged(deps.as_ref(), delegation_owner, None, None)
.unwrap();
assert_eq!(
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
page1.delegations.len() as u32
);
}
#[test]
fn retrieval_has_max_limit() {
let mut deps = test_helpers::init_contract();
let delegation_owner = "foo".to_string();
store_n_reverse_delegations(
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
&mut deps.storage,
&delegation_owner,
);
// query with a crazy high limit in an attempt to use too many resources
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner,
None,
Option::from(crazy_limit),
)
.unwrap();
// we default to a decent sized upper bound instead
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.delegations.len() as u32);
}
#[test]
fn pagination_works() {
let mut deps = test_helpers::init_contract();
let delegation_owner = "bar".to_string();
test_helpers::save_dummy_delegation(&mut deps.storage, "100", &delegation_owner);
let per_page = 2;
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
None,
Option::from(per_page),
)
.unwrap();
// page should have 1 result on it
assert_eq!(1, page1.delegations.len());
// save another
test_helpers::save_dummy_delegation(&mut deps.storage, "200", &delegation_owner);
// page1 should have 2 results on it
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
test_helpers::save_dummy_delegation(&mut deps.storage, "300", &delegation_owner);
// page1 still has 2 results
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
// retrieving the next page should start after the last key on this page
let start_after: IdentityKey = page1.start_next_after.unwrap();
let page2 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.delegations.len());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, "400", &delegation_owner);
let start_after = String::from("2");
let page2 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner,
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
}
}
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
use mixnet_contract::{Addr, Delegation, IdentityKey};
// storage prefixes
const DELEGATION_PK_NAMESPACE: &str = "dl";
const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo";
const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm";
// paged retrieval limits for all queries and transactions
pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500;
pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250;
// It's a composite key on node's identity and delegator address
type PrimaryKey = Vec<u8>;
pub(crate) struct DelegationIndex<'a> {
pub(crate) owner: MultiIndex<'a, (Addr, PrimaryKey), Delegation>,
pub(crate) mixnode: MultiIndex<'a, (IdentityKey, PrimaryKey), Delegation>,
}
impl<'a> IndexList<Delegation> for DelegationIndex<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<Delegation>> + '_> {
let v: Vec<&dyn Index<Delegation>> = vec![&self.owner, &self.mixnode];
Box::new(v.into_iter())
}
}
// I was really going back and forth about the data stored on the disk vs primary key duplication.
// It was basically between convenience and bloat, but in the end I decided the convenience wins.
//
// Basically I had 2 approaches. a) store delegator address and mixnode identity only as primary key of delegation or
// b) store it both as primary key AND inside delegation data.
// For the longest time I was in favour of a), since that removed any data duplication. However...,
// that also required that during index creation I recovered delegator address and mixnode identity
// from the Vec<u8>. That doesn't sound that terrible. However, even though I'm 99.99% certain that
// conversion would be impossible to fail, I'd still have to call an `unwrap` here due to required
// type signature and I didn't feel super comfortable doing that in our smart contract...
// So to get rid of this uncertainty I went with the b) approach. Even though each stored delegation
// takes over ~250B (since the key has to be duplicated), in the grand blockchain scheme of things
// it's not that terrible. Say we had 100_000_000 delegations -> that's still only 25GB of data
// and as a nice by-product it cleans up code a little bit by only having a single Delegation type.
pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> {
let indexes = DelegationIndex {
owner: MultiIndex::new(
|d, pk| (d.owner.clone(), pk),
DELEGATION_PK_NAMESPACE,
DELEGATION_OWNER_IDX_NAMESPACE,
),
mixnode: MultiIndex::new(
|d, pk| (d.node_identity.clone(), pk),
DELEGATION_PK_NAMESPACE,
DELEGATION_MIXNODE_IDX_NAMESPACE,
),
};
IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes)
}
#[cfg(test)]
mod tests {
use crate::delegations::storage;
use cosmwasm_std::Addr;
use mixnet_contract::IdentityKey;
#[cfg(test)]
mod reverse_mix_delegations {
use super::*;
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Order};
use cw_storage_plus::PrimaryKey;
use mixnet_contract::Delegation;
#[test]
fn reverse_mix_delegation_exists() {
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
let delegation_owner = Addr::unchecked("bar");
let delegation = coin(12345, DENOM);
let dummy_data = Delegation::new(
delegation_owner.clone(),
node_identity.clone(),
delegation,
mock_env().block.height,
None,
);
storage::delegations()
.save(
&mut deps.storage,
(node_identity.clone(), delegation_owner.clone()).joined_key(),
&dummy_data,
)
.unwrap();
let read = storage::delegations()
.idx
.owner
.prefix(delegation_owner)
.range(&deps.storage, None, None, Order::Ascending)
.map(|record| record.unwrap().1)
.collect::<Vec<_>>();
assert_eq!(1, read.len());
assert_eq!(dummy_data, read[0]);
}
#[test]
fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() {
let mut deps = test_helpers::init_contract();
let node_identity1: IdentityKey = "foo1".into();
let node_identity2: IdentityKey = "foo2".into();
let delegation_owner1 = Addr::unchecked("bar");
let delegation_owner2 = Addr::unchecked("bar2");
let delegation = coin(12345, DENOM);
assert!(test_helpers::read_delegation(
deps.as_ref().storage,
&node_identity1,
&delegation_owner1
)
.is_none());
// add delegation for a different node
let dummy_data = Delegation::new(
delegation_owner1.clone(),
node_identity2.clone(),
delegation.clone(),
mock_env().block.height,
None,
);
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner1.clone()).joined_key(),
&dummy_data,
)
.unwrap();
storage::delegations()
.idx
.owner
.prefix(delegation_owner1.clone())
.range(&deps.storage, None, None, Order::Ascending)
.map(|record| record.unwrap().1)
.for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1));
// add delegation from a different owner
let dummy_data = Delegation::new(
delegation_owner2.clone(),
node_identity1.clone(),
delegation.clone(),
mock_env().block.height,
None,
);
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner2.clone()).joined_key(),
&dummy_data,
)
.unwrap();
storage::delegations()
.idx
.owner
.prefix(delegation_owner1.clone())
.range(&deps.storage, None, None, Order::Ascending)
.map(|record| record.unwrap().1)
.for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1));
}
}
}
@@ -0,0 +1,862 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use crate::error::ContractError;
use crate::mixnodes::storage as mixnodes_storage;
use crate::support::helpers::generate_storage_key;
use config::defaults::DENOM;
use cosmwasm_std::{coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response};
use cw_storage_plus::PrimaryKey;
use mixnet_contract::Delegation;
use mixnet_contract::IdentityKey;
use vesting_contract::messages::ExecuteMsg as VestingContractExecuteMsg;
fn validate_delegation_stake(mut delegation: Vec<Coin>) -> Result<Coin, ContractError> {
// check if anything was put as delegation
if delegation.is_empty() {
return Err(ContractError::EmptyDelegation);
}
if delegation.len() > 1 {
return Err(ContractError::MultipleDenoms);
}
// check that the denomination is correct
if delegation[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have provided a non-zero amount in the delegation
if delegation[0].amount.is_zero() {
return Err(ContractError::EmptyDelegation);
}
Ok(delegation.pop().unwrap())
}
pub(crate) fn try_delegate_to_mixnode(
deps: DepsMut,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
) -> Result<Response, ContractError> {
// check if the delegation contains any funds of the appropriate denomination
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(deps, env, mix_identity, info.sender.as_str(), amount, None)
}
pub(crate) fn try_delegate_to_mixnode_on_behalf(
deps: DepsMut,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
delegate: String,
) -> Result<Response, ContractError> {
// check if the delegation contains any funds of the appropriate denomination
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(
deps,
env,
mix_identity,
&delegate,
amount,
Some(info.sender),
)
}
pub(crate) fn _try_delegate_to_mixnode(
deps: DepsMut,
env: Env,
mix_identity: IdentityKey,
delegate: &str,
amount: Coin,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
// check if the target node actually exists
if mixnodes_storage::mixnodes()
.may_load(deps.storage, &mix_identity)?
.is_none()
{
return Err(ContractError::MixNodeBondNotFound {
identity: mix_identity,
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
|total_delegation| {
// since we know that the target node exists and because the total_delegation bucket
// entry is created whenever the node itself is added, the unwrap here is fine
// as the entry MUST exist
Ok(total_delegation.unwrap() + amount.amount)
},
)?;
// update [or create new] delegation of this delegator
storage::delegations().update::<_, ContractError>(
deps.storage,
storage_key,
|existing_delegation| {
Ok(match existing_delegation {
Some(mut existing_delegation) => {
existing_delegation.increment_amount(amount.amount, Some(env.block.height));
existing_delegation
}
None => Delegation::new(
delegate.to_owned(),
mix_identity,
amount,
env.block.height,
proxy,
),
})
},
)?;
Ok(Response::default())
}
pub(crate) fn try_remove_delegation_from_mixnode(
deps: DepsMut,
info: MessageInfo,
mix_identity: IdentityKey,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, info.sender.as_str(), None)
}
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
deps: DepsMut,
info: MessageInfo,
mix_identity: IdentityKey,
delegate: String,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, &delegate, Some(info.sender))
}
pub(crate) fn _try_remove_delegation_from_mixnode(
deps: DepsMut,
mix_identity: IdentityKey,
delegate: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
match delegation_map.may_load(deps.storage, storage_key.clone())? {
None => Err(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: delegate,
}),
Some(old_delegation) => {
// remove all delegation associated with this delegator
if proxy != old_delegation.proxy {
return Err(ContractError::ProxyMismatch {
existing: old_delegation
.proxy
.map_or_else(|| "None".to_string(), |a| a.to_string()),
incoming: proxy.map_or_else(|| "None".to_string(), |a| a.to_string()),
});
}
// remove old delegation data from the store
// note for reviewers: I'm using `replace` as `remove` is just `may_load` followed by `replace`
// and we've already performed `may_load` and have access to pre-existing data
delegation_map.replace(deps.storage, storage_key, None, Some(&old_delegation))?;
// send delegated funds back to the delegation owner
let return_tokens = BankMsg::Send {
to_address: proxy.as_ref().unwrap_or(&delegate).to_string(),
amount: coins(
old_delegation.amount.amount.u128(),
old_delegation.amount.denom.clone(),
),
};
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
|total_delegation| {
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
// have never gotten here in the first place
// the second unwrap is also fine because we should NEVER underflow here,
// if we do, it means we have some serious error in our logic
Ok(total_delegation
.unwrap()
.checked_sub(old_delegation.amount.amount)
.unwrap())
},
)?;
let mut response = Response::new().add_message(return_tokens);
if let Some(proxy) = &proxy {
let msg = Some(VestingContractExecuteMsg::TrackUndelegation {
owner: delegate.as_str().to_string(),
mix_identity: mix_identity.clone(),
amount: old_delegation.amount,
});
let track_undelegation_msg = wasm_execute(proxy, &msg, coins(0, DENOM))?;
response = response.add_message(track_undelegation_msg);
}
Ok(response)
}
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::coins;
use crate::support::tests::test_helpers;
use super::storage;
use super::*;
#[cfg(test)]
mod delegation_stake_validation {
use cosmwasm_std::coin;
use super::*;
#[test]
fn stake_cant_be_empty() {
assert_eq!(
Err(ContractError::EmptyDelegation),
validate_delegation_stake(vec![])
)
}
#[test]
fn stake_must_have_single_coin_type() {
assert_eq!(
Err(ContractError::MultipleDenoms),
validate_delegation_stake(vec![
coin(123, DENOM),
coin(123, "BTC"),
coin(123, "DOGE")
])
)
}
#[test]
fn stake_coin_must_be_of_correct_type() {
assert_eq!(
Err(ContractError::WrongDenom {}),
validate_delegation_stake(coins(123, "DOGE"))
)
}
#[test]
fn stake_coin_must_have_value_greater_than_zero() {
assert_eq!(
Err(ContractError::EmptyDelegation),
validate_delegation_stake(coins(0, DENOM))
)
}
#[test]
fn stake_can_have_any_positive_value() {
// this might change in the future, but right now an arbitrary (positive) value can be delegated
assert!(validate_delegation_stake(coins(1, DENOM)).is_ok());
assert!(validate_delegation_stake(coins(123, DENOM)).is_ok());
assert!(validate_delegation_stake(coins(10000000000, DENOM)).is_ok());
}
}
#[cfg(test)]
mod mix_stake_delegation {
use super::*;
use crate::mixnodes::transactions::try_remove_mixnode;
use crate::support::tests::test_helpers::good_mixnode_bond;
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::Addr;
#[test]
fn fails_if_node_doesnt_exist() {
let mut deps = test_helpers::init_contract();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: "non-existent-mix-identity".into()
}),
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info("sender", &coins(123, DENOM)),
"non-existent-mix-identity".into(),
)
);
}
#[test]
fn succeeds_for_existing_node() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
let delegation = coin(123, DENOM);
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.is_ok());
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
delegation.clone(),
mock_env().block.height,
None,
);
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
);
// node's "total_delegation" is increased
assert_eq!(
delegation.amount,
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
#[test]
fn fails_if_node_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
}),
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(123, DENOM)),
identity,
)
);
}
#[test]
fn succeeds_if_node_rebonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation = coin(123, DENOM);
let delegation_owner = Addr::unchecked("sender");
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.is_ok());
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
delegation.clone(),
mock_env().block.height,
None,
);
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
);
// node's "total_delegation" is increased
assert_eq!(
delegation.amount,
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
#[test]
fn is_possible_for_an_already_delegated_node() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
let delegation1 = coin(100, DENOM);
let delegation2 = coin(50, DENOM);
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &[delegation1.clone()]),
identity.clone(),
)
.unwrap();
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &[delegation2.clone()]),
identity.clone(),
)
.unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
coin(delegation1.amount.u128() + delegation2.amount.u128(), DENOM),
mock_env().block.height,
None,
);
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
);
// node's "total_delegation" is sum of both
assert_eq!(
delegation1.amount + delegation2.amount,
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
#[test]
fn block_height_is_updated_on_new_delegation() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
let delegation = coin(100, DENOM);
let env1 = mock_env();
let mut env2 = mock_env();
let initial_height = env1.block.height;
let updated_height = initial_height + 42;
// second env has grown in block height
env2.block.height = updated_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
env2,
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.unwrap();
let updated =
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner).unwrap();
assert_eq!(delegation.amount + delegation.amount, updated.amount.amount);
assert_eq!(updated_height, updated.block_height);
}
#[test]
fn block_height_is_not_updated_on_different_delegator() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner1 = Addr::unchecked("sender1");
let delegation_owner2 = Addr::unchecked("sender2");
let delegation1 = coin(100, DENOM);
let delegation2 = coin(120, DENOM);
let env1 = mock_env();
let mut env2 = mock_env();
let initial_height = env1.block.height;
let second_height = initial_height + 42;
// second env has grown in block height
env2.block.height = second_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
mock_info(delegation_owner1.as_str(), &[delegation1.clone()]),
identity.clone(),
)
.unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
env2,
mock_info(delegation_owner2.as_str(), &[delegation2.clone()]),
identity.clone(),
)
.unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
);
assert_eq!(
second_height,
test_helpers::read_delegation(&deps.storage, identity, &delegation_owner2)
.unwrap()
.block_height
);
}
#[test]
fn is_disallowed_for_already_delegated_node_if_it_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
}),
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(50, DENOM)),
identity,
)
);
}
#[test]
fn is_allowed_for_multiple_nodes() {
let mut deps = test_helpers::init_contract();
let mixnode_owner1 = "bob";
let mixnode_owner2 = "fred";
let identity1 =
test_helpers::add_mixnode(mixnode_owner1, good_mixnode_bond(), deps.as_mut());
let identity2 =
test_helpers::add_mixnode(mixnode_owner2, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(123, DENOM)),
identity1.clone(),
)
.is_ok());
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(42, DENOM)),
identity2.clone(),
)
.is_ok());
let expected1 = Delegation::new(
delegation_owner.clone(),
identity1.clone(),
coin(123, DENOM),
mock_env().block.height,
None,
);
let expected2 = Delegation::new(
delegation_owner.clone(),
identity2.clone(),
coin(42, DENOM),
mock_env().block.height,
None,
);
assert_eq!(
expected1,
test_helpers::read_delegation(&deps.storage, identity1, &delegation_owner).unwrap()
);
assert_eq!(
expected2,
test_helpers::read_delegation(&deps.storage, identity2, &delegation_owner).unwrap()
);
}
#[test]
fn is_allowed_by_multiple_users() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation1 = coin(123, DENOM);
let delegation2 = coin(234, DENOM);
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info("sender1", &[delegation1.clone()]),
identity.clone(),
)
.is_ok());
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info("sender2", &[delegation2.clone()]),
identity.clone(),
)
.is_ok());
// node's "total_delegation" is sum of both
assert_eq!(
delegation1.amount + delegation2.amount,
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
#[test]
fn delegation_is_not_removed_if_node_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
let delegation_amount = coin(100, DENOM);
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &vec![delegation_amount.clone()]),
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
delegation_amount,
mock_env().block.height,
None,
);
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).unwrap()
)
}
}
#[cfg(test)]
mod removing_mix_stake_delegation {
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::Addr;
use cosmwasm_std::Uint128;
use crate::mixnodes::transactions::try_remove_mixnode;
use crate::support::tests::test_helpers::good_mixnode_bond;
use super::storage;
use super::*;
#[test]
fn fails_if_delegation_never_existed() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: identity.clone(),
address: delegation_owner.clone(),
}),
try_remove_delegation_from_mixnode(
deps.as_mut(),
mock_info(delegation_owner.as_str(), &[]),
identity,
)
);
}
#[test]
fn succeeds_if_delegation_existed() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
identity.clone(),
)
.unwrap();
assert_eq!(
Ok(Response::new().add_message(BankMsg::Send {
to_address: delegation_owner.clone().into(),
amount: coins(100, DENOM),
})),
try_remove_delegation_from_mixnode(
deps.as_mut(),
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
);
assert!(storage::delegations()
.may_load(
&deps.storage,
(identity.clone(), delegation_owner).joined_key(),
)
.unwrap()
.is_none());
// and total delegation is cleared
assert_eq!(
Uint128::zero(),
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
#[test]
fn succeeds_if_delegation_existed_even_if_node_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner = Addr::unchecked("sender");
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Ok(Response::new().add_message(BankMsg::Send {
to_address: delegation_owner.clone().into(),
amount: coins(100, DENOM),
})),
try_remove_delegation_from_mixnode(
deps.as_mut(),
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
);
assert!(
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).is_none()
);
}
#[test]
fn total_delegation_is_preserved_if_only_some_undelegate() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let identity =
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
let delegation_owner1 = Addr::unchecked("sender1");
let delegation_owner2 = Addr::unchecked("sender2");
let delegation1 = coin(123, DENOM);
let delegation2 = coin(234, DENOM);
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner1.as_str(), &[delegation1.clone()]),
identity.clone(),
)
.is_ok());
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
mock_info(delegation_owner2.as_str(), &[delegation2.clone()]),
identity.clone(),
)
.is_ok());
// sender1 undelegates
try_remove_delegation_from_mixnode(
deps.as_mut(),
mock_info(delegation_owner1.as_str(), &[]),
identity.clone(),
)
.unwrap();
// but total delegation should still equal to what sender2 sent
// node's "total_delegation" is sum of both
assert_eq!(
delegation2.amount,
mixnodes_storage::TOTAL_DELEGATION
.load(&deps.storage, &identity)
.unwrap()
)
}
}
// #[cfg(test)]
// mod multi_delegations {
// use super::*;
// use crate::delegations::helpers;
// use crate::delegations::queries::tests::store_n_mix_delegations;
// use crate::support::tests::test_helpers;
// use mixnet_contract::IdentityKey;
// use mixnet_contract::RawDelegationData;
//
// #[test]
// fn multiple_page_delegations() {
// let mut deps = test_helpers::init_contract();
// let node_identity: IdentityKey = "foo".into();
// store_n_mix_delegations(
// storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
// &mut deps.storage,
// &node_identity,
// );
// let mix_bucket = storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
// let mix_delegations = helpers::Delegations::new(mix_bucket);
// assert_eq!(
// storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
// mix_delegations.count() as u32
// );
// }
// }
}
+26 -19
View File
@@ -24,9 +24,6 @@ pub enum ContractError {
#[error("Not enough funds sent for gateway bond. (received {received}, minimum {minimum})")]
InsufficientGatewayBond { received: u128, minimum: u128 },
#[error("Gateway ({identity}) does not exist")]
GatewayBondNotFound { identity: IdentityKey },
#[error("{owner} does not seem to own any mixnodes")]
NoAssociatedMixNodeBond { owner: Addr },
@@ -45,17 +42,14 @@ pub enum ContractError {
#[error("No coin was sent for the bonding, you must send {}", DENOM)]
NoBondFound,
#[error("The bond reward rate for mixnode was set to be lower than 1")]
DecreasingMixnodeBondReward,
#[error("The delegation reward rate for mixnode was set to be lower than 1")]
DecreasingMixnodeDelegationReward,
#[error("Provided active set size is bigger than the demanded set")]
InvalidActiveSetSize,
#[error("The node had uptime larger than 100%")]
UnexpectedUptime,
#[error("Provided active set size is zero")]
ZeroActiveSet,
#[error("Provided rewarded set size is zero")]
ZeroRewardedSet,
#[error("This address has already bonded a mixnode")]
AlreadyOwnsMixnode,
@@ -72,23 +66,15 @@ pub enum ContractError {
#[error("No funds were provided for the delegation")]
EmptyDelegation,
#[error("Request did not come from the node owner ({owner})")]
InvalidSender { owner: Addr },
#[error("Could not find any delegation information associated with mixnode {identity} for {address}")]
NoMixnodeDelegationFound {
identity: IdentityKey,
address: Addr,
},
#[error("Overflow error!")]
Overflow(#[from] cosmwasm_std::OverflowError),
#[error("We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}")]
OutOfFunds { to_remove: u128, reward_pool: u128 },
#[error("Invalid ratio")]
Ratio(#[from] mixnet_contract::error::MixnetContractError),
#[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")]
InvalidRewardingIntervalNonce { received: u32, expected: u32 },
@@ -100,4 +86,25 @@ pub enum ContractError {
#[error("Mixnode {identity} has already been rewarded during the current rewarding interval")]
MixnodeAlreadyRewarded { identity: IdentityKey },
#[error("Some of mixnodes {identity} delegators are still pending reward")]
DelegatorsPendingReward { identity: IdentityKey },
#[error("Mixnode's {identity} operator has not been rewarded yet - cannot perform delegator rewarding until that happens")]
MixnodeOperatorNotRewarded { identity: IdentityKey },
#[error("Proxy address mismatch, expected {existing}, got {incoming}")]
ProxyMismatch { existing: String, incoming: String },
#[error("Failed to recover ed25519 public key from its base58 representation - {0}")]
MalformedEd25519IdentityKey(String),
#[error("Failed to recover ed25519 signature from its base58 representation - {0}")]
MalformedEd25519Signature(String),
#[error("Provided ed25519 signature did not verify correctly")]
InvalidEd25519Signature,
#[error("Profit margin percent needs to be an integer in range [0, 100], recieved {0}")]
InvalidProfitMarginPercent(u8),
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod queries;
pub mod storage;
pub mod transactions;
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use crate::mixnodes::storage::{BOND_PAGE_DEFAULT_LIMIT, BOND_PAGE_MAX_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant.
use cosmwasm_std::{Deps, Order, StdResult};
use cw_storage_plus::Bound;
use mixnet_contract::{GatewayBond, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse};
pub(crate) fn query_gateways_paged(
deps: Deps,
start_after: Option<IdentityKey>,
limit: Option<u32>,
) -> StdResult<PagedGatewayResponse> {
let limit = limit
.unwrap_or(BOND_PAGE_DEFAULT_LIMIT)
.min(BOND_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let nodes = storage::gateways()
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| res.map(|item| item.1))
.collect::<StdResult<Vec<GatewayBond>>>()?;
let start_next_after = nodes.last().map(|node| node.identity().clone());
Ok(PagedGatewayResponse::new(nodes, limit, start_next_after))
}
pub(crate) fn query_owns_gateway(
deps: Deps,
address: String,
) -> StdResult<GatewayOwnershipResponse> {
let validated_addr = deps.api.addr_validate(&address)?;
let gateway = storage::gateways()
.idx
.owner
.item(deps.storage, validated_addr.clone())?
.map(|record| record.1);
Ok(GatewayOwnershipResponse {
address: validated_addr,
gateway,
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::contract::execute;
use crate::support::tests::test_helpers;
use cosmwasm_std::testing::{mock_env, mock_info};
#[test]
fn gateways_empty_on_init() {
let deps = test_helpers::init_contract();
let response = query_gateways_paged(deps.as_ref(), None, Option::from(2)).unwrap();
assert_eq!(0, response.nodes.len());
}
#[test]
fn gateways_paged_retrieval_obeys_limits() {
let mut deps = test_helpers::init_contract();
let limit = 2;
for n in 0..1000 {
let key = format!("bond{}", n);
test_helpers::add_gateway(&key, test_helpers::good_gateway_bond(), deps.as_mut());
}
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.nodes.len() as u32);
}
#[test]
fn gateways_paged_retrieval_has_default_limit() {
let mut deps = test_helpers::init_contract();
for n in 0..1000 {
let key = format!("bond{}", n);
test_helpers::add_gateway(&key, test_helpers::good_gateway_bond(), deps.as_mut());
}
// query without explicitly setting a limit
let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32);
}
#[test]
fn gateways_paged_retrieval_has_max_limit() {
let mut deps = test_helpers::init_contract();
for n in 0..1000 {
let key = format!("bond{}", n);
test_helpers::add_gateway(&key, test_helpers::good_gateway_bond(), deps.as_mut());
}
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * BOND_PAGE_DEFAULT_LIMIT;
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
// we default to a decent sized upper bound instead
let expected_limit = BOND_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.nodes.len() as u32);
}
#[test]
fn gateway_pagination_works() {
// prepare 4 messages and identities that are sorted by the generated identities
// (because we query them in an ascended manner)
let mut exec_data = (0..4)
.map(|i| {
let sender = format!("nym-addr{}", i);
let (msg, identity) = test_helpers::valid_bond_gateway_msg(&sender);
(msg, (sender, identity))
})
.collect::<Vec<_>>();
exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2));
let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip();
let mut deps = test_helpers::init_contract();
let info = mock_info(
&sender_identities[0].0.clone(),
&test_helpers::good_gateway_bond(),
);
execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap();
let per_page = 2;
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.nodes.len());
// save another
let info = mock_info(
&sender_identities[1].0.clone(),
&test_helpers::good_gateway_bond(),
);
execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap();
// page1 should have 2 results on it
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.nodes.len());
let info = mock_info(
&sender_identities[2].0.clone(),
&test_helpers::good_gateway_bond(),
);
execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap();
// page1 still has 2 results
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.nodes.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_gateways_paged(
deps.as_ref(),
Option::from(start_after.clone()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.nodes.len());
// save another one
let info = mock_info(
&sender_identities[3].0.clone(),
&test_helpers::good_gateway_bond(),
);
execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap();
let page2 = query_gateways_paged(
deps.as_ref(),
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.nodes.len());
}
#[test]
fn query_for_gateway_owner_works() {
let mut deps = test_helpers::init_contract();
// "fred" does not own a mixnode if there are no mixnodes
let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.gateway.is_none());
// mixnode was added to "bob", "fred" still does not own one
test_helpers::add_gateway("bob", test_helpers::good_gateway_bond(), deps.as_mut());
let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.gateway.is_none());
// "fred" now owns a gateway!
test_helpers::add_gateway("fred", test_helpers::good_gateway_bond(), deps.as_mut());
let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.gateway.is_some());
// but after unbonding it, he doesn't own one anymore
crate::gateways::transactions::try_remove_gateway(deps.as_mut(), mock_info("fred", &[]))
.unwrap();
let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.gateway.is_none());
}
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex};
use mixnet_contract::{GatewayBond, IdentityKeyRef};
// storage prefixes
const GATEWAYS_PK_NAMESPACE: &str = "gt";
const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto";
pub(crate) struct GatewayBondIndex<'a> {
pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>,
}
// IndexList is just boilerplate code for fetching a struct's indexes
// note that from my understanding this will be converted into a macro at some point in the future
impl<'a> IndexList<GatewayBond> for GatewayBondIndex<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<GatewayBond>> + '_> {
let v: Vec<&dyn Index<GatewayBond>> = vec![&self.owner];
Box::new(v.into_iter())
}
}
// gateways() is the storage access function.
pub(crate) fn gateways<'a>() -> IndexedMap<'a, IdentityKeyRef<'a>, GatewayBond, GatewayBondIndex<'a>>
{
let indexes = GatewayBondIndex {
owner: UniqueIndex::new(|d| d.owner.clone(), GATEWAYS_OWNER_IDX_NAMESPACE),
};
IndexedMap::new(GATEWAYS_PK_NAMESPACE, indexes)
}
// currently not used outside tests
#[cfg(test)]
mod tests {
use super::super::storage;
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::testing::MockStorage;
use cosmwasm_std::StdResult;
use cosmwasm_std::Storage;
use cosmwasm_std::{coin, Addr, Uint128};
use mixnet_contract::GatewayBond;
use mixnet_contract::IdentityKey;
use mixnet_contract::{Gateway, IdentityKeyRef};
// currently this is only used in tests but may become useful later on
pub(crate) fn read_gateway_pledge_amount(
storage: &dyn Storage,
identity: IdentityKeyRef,
) -> StdResult<cosmwasm_std::Uint128> {
let node = storage::gateways().load(storage, identity)?;
Ok(node.pledge_amount.amount)
}
#[test]
fn gateway_single_read_retrieval() {
let mut storage = MockStorage::new();
let bond1 = test_helpers::gateway_bond_fixture("owner1");
let bond2 = test_helpers::gateway_bond_fixture("owner2");
storage::gateways()
.save(&mut storage, "bond1", &bond1)
.unwrap();
storage::gateways()
.save(&mut storage, "bond2", &bond2)
.unwrap();
let res1 = storage::gateways().load(&storage, "bond1").unwrap();
let res2 = storage::gateways().load(&storage, "bond2").unwrap();
assert_eq!(bond1, res1);
assert_eq!(bond2, res2);
}
#[test]
fn reading_gateway_bond() {
let mut mock_storage = MockStorage::new();
let node_owner: Addr = Addr::unchecked("node-owner");
let node_identity: IdentityKey = "nodeidentity".into();
// produces an error if target gateway doesn't exist
let res = read_gateway_pledge_amount(&mock_storage, &node_identity);
assert!(res.is_err());
// returns appropriate value otherwise
let pledge_amount = 1000;
let gateway_bond = GatewayBond {
pledge_amount: coin(pledge_amount, DENOM),
owner: node_owner.clone(),
block_height: 12_345,
gateway: Gateway {
identity_key: node_identity.clone(),
..test_helpers::gateway_fixture()
},
proxy: None,
};
storage::gateways()
.save(&mut mock_storage, &node_identity, &gateway_bond)
.unwrap();
assert_eq!(
Uint128::new(pledge_amount),
read_gateway_pledge_amount(&mock_storage, &node_identity).unwrap()
);
}
}
@@ -0,0 +1,569 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use crate::error::ContractError;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature};
use config::defaults::DENOM;
use cosmwasm_std::{
coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128,
};
use mixnet_contract::{Gateway, GatewayBond, Layer};
use vesting_contract::messages::ExecuteMsg as VestingContractExecuteMsg;
pub fn try_add_gateway(
deps: DepsMut,
env: Env,
info: MessageInfo,
gateway: Gateway,
owner_signature: String,
) -> Result<Response, ContractError> {
// check if the pledge contains any funds of the appropriate denomination
let minimum_pledge = mixnet_params_storage::CONTRACT_STATE
.load(deps.storage)?
.params
.minimum_mixnode_pledge;
let pledge = validate_gateway_pledge(info.funds, minimum_pledge)?;
_try_add_gateway(
deps,
env,
gateway,
pledge,
info.sender.as_str(),
owner_signature,
None,
)
}
pub fn try_add_gateway_on_behalf(
deps: DepsMut,
env: Env,
info: MessageInfo,
gateway: Gateway,
owner: String,
owner_signature: String,
) -> Result<Response, ContractError> {
// check if the pledge contains any funds of the appropriate denomination
let minimum_pledge = mixnet_params_storage::CONTRACT_STATE
.load(deps.storage)?
.params
.minimum_mixnode_pledge;
let pledge = validate_gateway_pledge(info.funds, minimum_pledge)?;
let proxy = info.sender;
_try_add_gateway(
deps,
env,
gateway,
pledge,
&owner,
owner_signature,
Some(proxy),
)
}
pub(crate) fn _try_add_gateway(
deps: DepsMut,
env: Env,
gateway: Gateway,
pledge: Coin,
owner: &str,
owner_signature: String,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let owner = deps.api.addr_validate(owner)?;
// if the client has an active bonded mixnode or gateway, don't allow bonding
ensure_no_existing_bond(deps.storage, &owner)?;
// check if somebody else has already bonded a gateway with this identity
if let Some(existing_bond) =
storage::gateways().may_load(deps.storage, &gateway.identity_key)?
{
if existing_bond.owner != owner {
return Err(ContractError::DuplicateGateway {
owner: existing_bond.owner,
});
}
}
// check if this sender actually owns the gateway by checking the signature
validate_node_identity_signature(
deps.as_ref(),
&owner,
owner_signature,
&gateway.identity_key,
)?;
let bond = GatewayBond::new(pledge, owner, env.block.height, gateway, proxy);
storage::gateways().save(deps.storage, bond.identity(), &bond)?;
mixnet_params_storage::increment_layer_count(deps.storage, Layer::Gateway)?;
Ok(Response::new())
}
pub fn try_remove_gateway_on_behalf(
deps: DepsMut,
info: MessageInfo,
owner: String,
) -> Result<Response, ContractError> {
let proxy = info.sender;
_try_remove_gateway(deps, &owner, Some(proxy))
}
pub fn try_remove_gateway(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
_try_remove_gateway(deps, info.sender.as_ref(), None)
}
pub(crate) fn _try_remove_gateway(
deps: DepsMut,
owner: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let owner = deps.api.addr_validate(owner)?;
// try to find the node of the sender
let gateway_bond = match storage::gateways()
.idx
.owner
.item(deps.storage, owner.clone())?
{
Some(record) => record.1,
None => return Err(ContractError::NoAssociatedGatewayBond { owner }),
};
if proxy != gateway_bond.proxy {
return Err(ContractError::ProxyMismatch {
existing: gateway_bond
.proxy
.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()),
incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()),
});
}
// send bonded funds back to the bond owner
let return_tokens = BankMsg::Send {
to_address: proxy.as_ref().unwrap_or(&owner).to_string(),
amount: vec![gateway_bond.pledge_amount()],
};
// remove the bond
storage::gateways().remove(deps.storage, gateway_bond.identity())?;
// decrement layer count
mixnet_params_storage::decrement_layer_count(deps.storage, Layer::Gateway)?;
let mut response = Response::new()
.add_message(return_tokens)
.add_attribute("action", "unbond")
.add_attribute("address", owner.clone())
.add_attribute("gateway_bond", gateway_bond.to_string());
if let Some(proxy) = &proxy {
let msg = VestingContractExecuteMsg::TrackUnbondGateway {
owner: owner.as_str().to_string(),
amount: gateway_bond.pledge_amount,
};
let track_unbond_message = wasm_execute(proxy, &msg, coins(0, DENOM))?;
response = response.add_message(track_unbond_message);
}
Ok(response)
}
fn validate_gateway_pledge(
mut pledge: Vec<Coin>,
minimum_pledge: Uint128,
) -> Result<Coin, ContractError> {
// check if anything was put as bond
if pledge.is_empty() {
return Err(ContractError::NoBondFound);
}
if pledge.len() > 1 {
return Err(ContractError::MultipleDenoms);
}
// check that the denomination is correct
if pledge[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have at least 100 coins in our pledge
if pledge[0].amount < minimum_pledge {
return Err(ContractError::InsufficientGatewayBond {
received: pledge[0].amount.into(),
minimum: minimum_pledge.into(),
});
}
Ok(pledge.pop().unwrap())
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::contract::{execute, query, INITIAL_GATEWAY_PLEDGE};
use crate::error::ContractError;
use crate::gateways::transactions::validate_gateway_pledge;
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::attr;
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{coins, BankMsg, Response};
use cosmwasm_std::{from_binary, Addr, Uint128};
use mixnet_contract::Gateway;
use mixnet_contract::{ExecuteMsg, PagedGatewayResponse, QueryMsg};
#[test]
fn gateway_add() {
let mut deps = test_helpers::init_contract();
// if we fail validation (by say not sending enough funds
let insufficient_bond = Into::<u128>::into(INITIAL_GATEWAY_PLEDGE) - 1;
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
let (msg, _) = test_helpers::valid_bond_gateway_msg("anyone");
// we are informed that we didn't send enough funds
let result = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: insufficient_bond,
minimum: INITIAL_GATEWAY_PLEDGE.into(),
})
);
// make sure no gateway was inserted into the topology
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&res).unwrap();
assert_eq!(0, page.nodes.len());
// if we send enough funds
let info = mock_info("anyone", &test_helpers::good_gateway_bond());
let (msg, identity) = test_helpers::valid_bond_gateway_msg("anyone");
// we get back a message telling us everything was OK
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert!(execute_response.is_ok());
// we can query topology and the new node is there
let query_response = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
assert_eq!(1, page.nodes.len());
assert_eq!(
&Gateway {
identity_key: identity,
..test_helpers::gateway_fixture()
},
page.nodes[0].gateway()
);
// if there was already a gateway bonded by particular user
let info = mock_info("foomper", &test_helpers::good_gateway_bond());
let (msg, _) = test_helpers::valid_bond_gateway_msg("foomper");
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
let info = mock_info("foomper", &test_helpers::good_gateway_bond());
let (msg, _) = test_helpers::valid_bond_gateway_msg("foomper");
// it fails
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(Err(ContractError::AlreadyOwnsGateway), execute_response);
// bonding fails if the user already owns a mixnode
test_helpers::add_mixnode(
"mixnode-owner",
test_helpers::good_mixnode_bond(),
deps.as_mut(),
);
let info = mock_info("mixnode-owner", &test_helpers::good_gateway_bond());
let (msg, _) = test_helpers::valid_bond_gateway_msg("mixnode-owner");
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(execute_response, Err(ContractError::AlreadyOwnsMixnode));
// but after he unbonds it, it's all fine again
let info = mock_info("mixnode-owner", &[]);
let msg = ExecuteMsg::UnbondMixnode {};
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
let info = mock_info("mixnode-owner", &test_helpers::good_gateway_bond());
let (msg, _) = test_helpers::valid_bond_gateway_msg("mixnode-owner");
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert!(execute_response.is_ok());
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
// Is that right? Think about this, not sure yet.
}
#[test]
fn adding_gateway_without_existing_owner() {
let mut deps = test_helpers::init_contract();
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
// before the execution the node had no associated owner
assert!(storage::gateways()
.idx
.owner
.item(deps.as_ref().storage, Addr::unchecked("gateway-owner"))
.unwrap()
.is_none());
let (msg, identity) = test_helpers::valid_bond_gateway_msg("gateway-owner");
// it's all fine, owner is saved
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert!(execute_response.is_ok());
assert_eq!(
&identity,
storage::gateways()
.idx
.owner
.item(deps.as_ref().storage, Addr::unchecked("gateway-owner"))
.unwrap()
.unwrap()
.1
.identity()
);
}
#[test]
fn adding_gateway_with_existing_owner() {
let mut deps = test_helpers::init_contract();
let identity = test_helpers::add_gateway(
"gateway-owner",
test_helpers::good_gateway_bond(),
deps.as_mut(),
);
// request fails giving the existing owner address in the message
let info = mock_info(
"gateway-owner-pretender",
&test_helpers::good_gateway_bond(),
);
let msg = ExecuteMsg::BondGateway {
gateway: Gateway {
identity_key: identity,
..test_helpers::gateway_fixture()
},
owner_signature: "foomp".to_string(),
};
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
Err(ContractError::DuplicateGateway {
owner: Addr::unchecked("gateway-owner")
}),
execute_response
);
}
#[test]
fn adding_gateway_with_existing_unchanged_owner() {
let mut deps = test_helpers::init_contract();
test_helpers::add_gateway(
"gateway-owner",
test_helpers::good_gateway_bond(),
deps.as_mut(),
);
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
let (msg, _) = test_helpers::valid_bond_gateway_msg("gateway-owner");
let res = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(Err(ContractError::AlreadyOwnsGateway), res);
}
#[test]
fn gateway_remove() {
let mut deps = test_helpers::init_contract();
// try unbond when no nodes exist yet
let info = mock_info("anyone", &[]);
let msg = ExecuteMsg::UnbondGateway {};
let result = execute(deps.as_mut(), mock_env(), info, msg);
// we're told that there is no node for our address
assert_eq!(
result,
Err(ContractError::NoAssociatedGatewayBond {
owner: Addr::unchecked("anyone")
})
);
// let's add a node owned by bob
test_helpers::add_gateway("bob", test_helpers::good_gateway_bond(), deps.as_mut());
// attempt to unbond fred's node, which doesn't exist
let info = mock_info("fred", &[]);
let msg = ExecuteMsg::UnbondGateway {};
let result = execute(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::NoAssociatedGatewayBond {
owner: Addr::unchecked("fred")
})
);
// bob's node is still there
let nodes = test_helpers::get_gateways(&mut deps);
assert_eq!(1, nodes.len());
let first_node = &nodes[0];
assert_eq!(&Addr::unchecked("bob"), first_node.owner());
// add a node owned by fred
let fred_identity =
test_helpers::add_gateway("fred", test_helpers::good_gateway_bond(), deps.as_mut());
// let's make sure we now have 2 nodes:
assert_eq!(2, test_helpers::get_gateways(&mut deps).len());
// unbond fred's node
let info = mock_info("fred", &[]);
let msg = ExecuteMsg::UnbondGateway {};
let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
// we should see log messages come back showing an unbond message
let expected_attributes = vec![
attr("action", "unbond"),
attr("address", "fred"),
attr(
"gateway_bond",
format!(
"amount: {} {}, owner: fred, identity: {}",
INITIAL_GATEWAY_PLEDGE, DENOM, fred_identity
),
),
];
// we should see a funds transfer from the contract back to fred
let expected_message = BankMsg::Send {
to_address: String::from(info.sender),
amount: test_helpers::good_gateway_bond(),
};
// run the executor and check that we got back the correct results
let expected = Response::new()
.add_attributes(expected_attributes)
.add_message(expected_message);
assert_eq!(remove_fred, expected);
// only 1 node now exists, owned by bob:
let gateway_bonds = test_helpers::get_gateways(&mut deps);
assert_eq!(1, gateway_bonds.len());
assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner());
}
#[test]
fn removing_gateway_clears_ownership() {
let mut deps = test_helpers::init_contract();
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
let (bond_msg, identity) = test_helpers::valid_bond_gateway_msg("gateway-owner");
execute(deps.as_mut(), mock_env(), info, bond_msg.clone()).unwrap();
assert_eq!(
&identity,
storage::gateways()
.idx
.owner
.item(deps.as_ref().storage, Addr::unchecked("gateway-owner"))
.unwrap()
.unwrap()
.1
.identity()
);
let info = mock_info("gateway-owner", &[]);
let msg = ExecuteMsg::UnbondGateway {};
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
assert!(storage::gateways()
.idx
.owner
.item(deps.as_ref().storage, Addr::unchecked("gateway-owner"))
.unwrap()
.is_none());
// and since it's removed, it can be reclaimed
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
assert!(execute(deps.as_mut(), mock_env(), info, bond_msg).is_ok());
assert_eq!(
&identity,
storage::gateways()
.idx
.owner
.item(deps.as_ref().storage, Addr::unchecked("gateway-owner"))
.unwrap()
.unwrap()
.1
.identity()
);
}
#[test]
fn validating_gateway_bond() {
// you must send SOME funds
let result = validate_gateway_pledge(Vec::new(), INITIAL_GATEWAY_PLEDGE);
assert_eq!(result, Err(ContractError::NoBondFound));
// you must send at least 100 coins...
let mut bond = test_helpers::good_gateway_bond();
bond[0].amount = INITIAL_GATEWAY_PLEDGE.checked_sub(Uint128::new(1)).unwrap();
let result = validate_gateway_pledge(bond.clone(), INITIAL_GATEWAY_PLEDGE);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: Into::<u128>::into(INITIAL_GATEWAY_PLEDGE) - 1,
minimum: INITIAL_GATEWAY_PLEDGE.into(),
})
);
// more than that is still fine
let mut bond = test_helpers::good_gateway_bond();
bond[0].amount = INITIAL_GATEWAY_PLEDGE + Uint128::new(1);
let result = validate_gateway_pledge(bond.clone(), INITIAL_GATEWAY_PLEDGE);
assert!(result.is_ok());
// it must be sent in the defined denom!
let mut bond = test_helpers::good_gateway_bond();
bond[0].denom = "baddenom".to_string();
let result = validate_gateway_pledge(bond.clone(), INITIAL_GATEWAY_PLEDGE);
assert_eq!(result, Err(ContractError::WrongDenom {}));
let mut bond = test_helpers::good_gateway_bond();
bond[0].denom = "foomp".to_string();
let result = validate_gateway_pledge(bond.clone(), INITIAL_GATEWAY_PLEDGE);
assert_eq!(result, Err(ContractError::WrongDenom {}));
}
}
-389
View File
@@ -1,389 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use crate::transactions::OLD_DELEGATIONS_CHUNK_SIZE;
use cosmwasm_std::{Decimal, Order, StdError, StdResult, Uint128};
use cosmwasm_storage::ReadonlyBucket;
use mixnet_contract::{Addr, IdentityKey, PagedAllDelegationsResponse, UnpackedDelegation};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::ops::Sub;
// for time being completely ignore concept of a leap year and assume each year is exactly 365 days
// i.e. 8760 hours
const HOURS_IN_YEAR: u128 = 8760;
const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128);
// cosmwasm bucket internal value
const NAMESPACE_LENGTH: usize = 2;
pub fn decimal_to_uint128(value: Decimal) -> Uint128 {
value * DECIMAL_FRACTIONAL
}
pub fn uint128_to_decimal(value: Uint128) -> Decimal {
Decimal::from_ratio(value, DECIMAL_FRACTIONAL)
}
pub(crate) fn calculate_epoch_reward_rate(
epoch_length: u32,
annual_reward_rate: Decimal,
) -> Decimal {
// this is more of a sanity check as the contract does not allow setting annual reward rates
// to be lower than 1.
debug_assert!(annual_reward_rate >= Decimal::one());
// converts reward rate, like 1.25 into the expected gain, like 0.25
let annual_reward = annual_reward_rate.sub(Decimal::one());
// do a simple cross-multiplication:
// `annual_reward` - `HOURS_IN_YEAR`
// x - `epoch_length`
//
// x = `annual_reward` * `epoch_length` / `HOURS_IN_YEAR`
// converts reward, like 0.25 into 250000000000000000
let annual_reward_uint128 = decimal_to_uint128(annual_reward);
// calculates `annual_reward_uint128` * `epoch_length` / `HOURS_IN_YEAR`
let epoch_reward_uint128 = annual_reward_uint128.multiply_ratio(epoch_length, HOURS_IN_YEAR);
// note: this returns a % reward, like 0.05 rather than reward rate (like 1.05)
uint128_to_decimal(epoch_reward_uint128)
}
pub(crate) fn scale_reward_by_uptime(
reward: Decimal,
uptime: u32,
) -> Result<Decimal, ContractError> {
if uptime > 100 {
return Err(ContractError::UnexpectedUptime);
}
let uptime_ratio = Decimal::from_ratio(uptime, 100u128);
// if we do not convert into a more precise representation, we might end up with, for example,
// reward 0.05 and uptime of 50% which would produce 0.50 * 0.05 = 0 (because of u128 representation)
// and also the above would be impossible to compute as Mul<Decimal> for Decimal is not implemented
//
// but with the intermediate conversion, we would have
// 0.50 * 50_000_000_000_000_000 = 25_000_000_000_000_000
// which converted back would give us the proper 0.025
let uptime_ratio_u128 = decimal_to_uint128(uptime_ratio);
let scaled = reward * uptime_ratio_u128;
Ok(uint128_to_decimal(scaled))
}
// Extracts the node identity and owner of a delegation from the bytes used as
// key in the delegation buckets.
fn extract_identity_and_owner(bytes: Vec<u8>) -> StdResult<(Addr, IdentityKey)> {
if bytes.len() < NAMESPACE_LENGTH {
return Err(StdError::parse_err(
"mixnet_contract::types::IdentityKey",
"Invalid type",
));
}
let identity_size = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
let identity_bytes: Vec<u8> = bytes
.iter()
.skip(NAMESPACE_LENGTH)
.take(identity_size)
.copied()
.collect();
let identity = IdentityKey::from_utf8(identity_bytes)
.map_err(|_| StdError::parse_err("mixnet_contract::types::IdentityKey", "Invalid type"))?;
let owner_bytes: Vec<u8> = bytes
.iter()
.skip(NAMESPACE_LENGTH + identity_size)
.copied()
.collect();
let owner = Addr::unchecked(
String::from_utf8(owner_bytes)
.map_err(|_| StdError::parse_err("cosmwasm_std::addresses::Addr", "Invalid type"))?,
);
Ok((owner, identity))
}
// currently not used outside tests
#[cfg(test)]
// Converts the node identity and owner of a delegation into the bytes used as
// key in the delegation buckets.
pub(crate) fn identity_and_owner_to_bytes(identity: &IdentityKey, owner: &Addr) -> Vec<u8> {
let mut bytes = u16::to_be_bytes(identity.len() as u16).to_vec();
bytes.append(&mut identity.as_bytes().to_vec());
bytes.append(&mut owner.as_bytes().to_vec());
bytes
}
pub(crate) fn get_all_delegations_paged<T>(
bucket: &ReadonlyBucket<T>,
start_after: &Option<Vec<u8>>,
limit: usize,
) -> StdResult<PagedAllDelegationsResponse<T>>
where
T: Serialize + DeserializeOwned,
{
let delegations = bucket
.range(start_after.as_deref(), None, Order::Ascending)
.filter(|res| res.is_ok())
.take(limit)
.map(|res| {
res.map(|entry| {
let (owner, identity) = extract_identity_and_owner(entry.0).expect("Invalid node identity or address used as key in bucket. The storage is corrupted!");
UnpackedDelegation::new(owner, identity, entry.1)
})
})
.collect::<StdResult<Vec<UnpackedDelegation<T>>>>()?;
let start_next_after = if let Some(Ok(last)) = bucket
.range(start_after.as_deref(), None, Order::Ascending)
.filter(|res| res.is_ok())
.take(limit)
.last()
{
Some(last.0)
} else {
None
};
Ok(PagedAllDelegationsResponse::new(
delegations,
start_next_after,
))
}
pub struct Delegations<'a, T: Clone + Serialize + DeserializeOwned> {
delegations_bucket: ReadonlyBucket<'a, T>,
curr_delegations: Vec<UnpackedDelegation<T>>,
curr_index: usize,
start_after: Option<Vec<u8>>,
last_page: bool,
}
impl<'a, T: Clone + Serialize + DeserializeOwned> Delegations<'a, T> {
pub fn new(delegations_bucket: ReadonlyBucket<'a, T>) -> Self {
Delegations {
delegations_bucket,
curr_delegations: vec![],
curr_index: OLD_DELEGATIONS_CHUNK_SIZE,
start_after: None,
last_page: false,
}
}
}
impl<'a, T: Clone + Serialize + DeserializeOwned> Iterator for Delegations<'a, T> {
type Item = UnpackedDelegation<T>;
fn next(&mut self) -> Option<Self::Item> {
if self.curr_index == OLD_DELEGATIONS_CHUNK_SIZE && !self.last_page {
self.start_after = self.start_after.clone().map(|mut v: Vec<u8>| {
v.push(0);
v
});
let delegations_paged = get_all_delegations_paged(
&self.delegations_bucket,
&self.start_after,
OLD_DELEGATIONS_CHUNK_SIZE,
)
.ok()?;
self.curr_delegations = delegations_paged.delegations;
self.curr_index = 0;
self.start_after = delegations_paged.start_next_after;
if self.start_after.is_none() {
self.last_page = true;
}
}
if self.curr_index < self.curr_delegations.len() {
let ret = self.curr_delegations[self.curr_index].clone();
self.curr_index += 1;
Some(ret)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::queries::tests::store_n_mix_delegations;
use crate::storage::{all_mix_delegations_read, mix_delegations};
use crate::support::tests::helpers;
use cosmwasm_std::testing::mock_dependencies;
use mixnet_contract::RawDelegationData;
use std::str::FromStr;
#[test]
fn delegations_iterator() {
let mut deps = helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
store_n_mix_delegations(
2 * OLD_DELEGATIONS_CHUNK_SIZE as u32,
&mut deps.storage,
&node_identity,
);
let mix_bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
let mut delegations = Delegations::new(mix_bucket);
assert!(delegations.curr_delegations.is_empty());
assert_eq!(delegations.curr_index, OLD_DELEGATIONS_CHUNK_SIZE);
delegations.next().unwrap();
assert_eq!(
delegations.curr_delegations.len(),
OLD_DELEGATIONS_CHUNK_SIZE
);
assert_eq!(delegations.curr_index, 1);
for _ in 0..OLD_DELEGATIONS_CHUNK_SIZE {
delegations.next().unwrap();
}
assert_eq!(
delegations.curr_delegations.len(),
OLD_DELEGATIONS_CHUNK_SIZE
);
assert_eq!(delegations.curr_index, 1);
for _ in 0..OLD_DELEGATIONS_CHUNK_SIZE - 1 {
delegations.next().unwrap();
}
assert!(delegations.next().is_none());
}
#[test]
fn calculating_epoch_reward_rate() {
// 1.10
let annual_reward_rate = Decimal::from_ratio(110u128, 100u128);
// if the epoch is (for some reason) exactly one year,
// the reward rate should be unchanged
let per_epoch_rate = calculate_epoch_reward_rate(HOURS_IN_YEAR as u32, annual_reward_rate);
// 0.10
let expected = annual_reward_rate.sub(Decimal::one());
assert_eq!(expected, per_epoch_rate);
// 24 hours
let per_epoch_rate = calculate_epoch_reward_rate(24, annual_reward_rate);
// 0.1 / 365
let expected = Decimal::from_ratio(1u128, 3650u128);
assert_eq!(expected, per_epoch_rate);
let expected_per_epoch_rate_excel = Decimal::from_str("0.000273972602739726").unwrap();
assert_eq!(expected_per_epoch_rate_excel, per_epoch_rate);
// 1 hour
let per_epoch_rate = calculate_epoch_reward_rate(1, annual_reward_rate);
// 0.1 / 8760
let expected = Decimal::from_ratio(1u128, 87600u128);
assert_eq!(expected, per_epoch_rate);
}
#[test]
fn scaling_reward_by_uptime() {
// 0.05
let epoch_reward = Decimal::from_ratio(5u128, 100u128);
// scaling by 100 does nothing
let scaled = scale_reward_by_uptime(epoch_reward, 100).unwrap();
assert_eq!(epoch_reward, scaled);
// scaling by 0 makes the reward 0
let scaled = scale_reward_by_uptime(epoch_reward, 0).unwrap();
assert_eq!(Decimal::zero(), scaled);
// 50 halves it
let scaled = scale_reward_by_uptime(epoch_reward, 50).unwrap();
let expected = Decimal::from_ratio(25u128, 1000u128);
assert_eq!(expected, scaled);
// 10 takes 1/10th
let scaled = scale_reward_by_uptime(epoch_reward, 10).unwrap();
let expected = Decimal::from_ratio(5u128, 1000u128);
assert_eq!(expected, scaled);
// anything larger than 100 returns an error
assert!(scale_reward_by_uptime(epoch_reward, 101).is_err())
}
#[test]
fn identity_and_owner_deserialization() {
assert!(extract_identity_and_owner(vec![]).is_err());
assert!(extract_identity_and_owner(vec![0]).is_err());
let (owner, identity) = extract_identity_and_owner(vec![
0, 7, 109, 105, 120, 110, 111, 100, 101, 97, 108, 105, 99, 101,
])
.unwrap();
assert_eq!(owner, "alice");
assert_eq!(identity, "mixnode");
}
#[test]
fn identity_and_owner_serialization() {
let identity: IdentityKey = "gateway".into();
let owner = Addr::unchecked("bob");
assert_eq!(
vec![0, 7, 103, 97, 116, 101, 119, 97, 121, 98, 111, 98],
identity_and_owner_to_bytes(&identity, &owner)
);
}
#[test]
fn all_mix_delegations() {
let mut deps = mock_dependencies(&[]);
let node_identity1: IdentityKey = "foo1".into();
let delegation_owner1 = Addr::unchecked("bar1");
let node_identity2: IdentityKey = "foo2".into();
let delegation_owner2 = Addr::unchecked("bar2");
let raw_delegation = RawDelegationData::new(1000u128.into(), 42);
let mut start_after = None;
mix_delegations(&mut deps.storage, &node_identity1)
.save(delegation_owner1.as_bytes(), &raw_delegation)
.unwrap();
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
let response =
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
start_after = response.start_next_after;
let delegations = response.delegations;
assert_eq!(delegations.len(), 1);
assert_eq!(
delegations[0],
UnpackedDelegation::new(
delegation_owner1.clone(),
node_identity1.clone(),
raw_delegation.clone()
)
);
mix_delegations(&mut deps.storage, &node_identity2)
.save(delegation_owner2.as_bytes(), &raw_delegation)
.unwrap();
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
let response =
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
start_after = response.start_next_after;
let delegations = response.delegations;
assert_eq!(delegations.len(), 2);
assert_eq!(
delegations[1],
UnpackedDelegation::new(
delegation_owner2.clone(),
node_identity2.clone(),
raw_delegation.clone()
)
);
mix_delegations(&mut deps.storage, &node_identity1).remove(delegation_owner1.as_bytes());
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
let response =
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
let delegations = response.delegations;
assert_eq!(delegations.len(), 1);
assert_eq!(
delegations[0],
UnpackedDelegation::new(delegation_owner2, node_identity2, raw_delegation.clone()),
);
}
}
+7 -7
View File
@@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod error;
pub(crate) mod helpers;
pub mod queries;
pub mod state;
pub(crate) mod storage;
pub mod support;
pub mod transactions;
mod delegations;
mod error;
mod gateways;
mod mixnet_contract_settings;
mod mixnodes;
mod rewards;
mod support;
@@ -0,0 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod models;
pub mod queries;
pub mod storage;
pub mod transactions;

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