From fc2eedfc66a380450192fb3da207e7b4e483d516 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 7 May 2024 09:38:06 +0200 Subject: [PATCH] Another Grand Ecash Squasheroo add offline ecash library minor changes in coconut benchmarks add ecash smart contract change contract traits from coconut to ecash first wave of andrew's suggestion first wave of andrew's suggestion second wave of andrew's suggestion for ecash lib andrew's suggestion for ecash contract licensing commit safety comments for most unwraps more unwrap handling change chrono crate for time latest cargo lock error revamp small visibility fix small fix remove indexedmap from contract + some tweaks add cw2 version in ecash contract remove envryption key from contract change types from coconut to ecash types adapt api model for credential issuance adapt issued credential storage on API add signatures cache on API change API routes for new blind signing modify issued_credential table add issuance logic client-side credential and signature storage client side utils for credential issuance first wave of fix some of andrew's suggestions remove encryption key from deposit freepass issuance client side freepass issuance API side andrew's suggested fixes other suggested fix adapt change from PR below allow offline verification flag credential spending models credential spending models for client credential preperation for the client credential preperation for the client credential storage for spending on client bloom filter for API spent credential storage on validators API route for spending online and offline ecash API routes in the client lib credential storage on gateway ecash verifier to replace coconut verifier accept credentials on gateway bandwidth expiration for gateways client ask for more bandwidth if it runs out credential import adapt nym validator rewarder and sdk fix tests api tests and add constants cargo fmt and lock and small test fix cargo fmt and lock and small test fix cargo lock move stuff where they belong in ecash and static parameters move some constants, error handling and phase out time crate error revamp part 2 secret key by ref instead of clone change l in wallet and v visibility rework payinfo rework monster tuples fix expiration date signature cloning minor fixes final bits and bobs fixes final bits and bobs fixes rename l accessor to tickets_spent wave of fixes second wave of fixes change hash domain value removed benchmark flag remove useless stringification in storage nuke Bandwidth voucher change timestamps to offsetdatetime key name change post-rebase fixes update nym-connect 'time' dep due to broken semver upload ecash contract to the build server make wasm zknym-lib compile but it won't work properly just yet make wasm zknym-lib compile but it won't work properly just yet fix typo in ecash contract deps make sure to use 0.1.0 sphinx packet optimise pairings in 'check_vk_pairing' derive serde for ecash types simplified g1 tuple byte conversion further optimise the pairing unified signature type + renamed nym-api coconut module to ecash using bincode serialiser for more complex binary types using multimiller loop instead of rayon for verifying coin indices signatures batching signature verification wherever possible feature-locked rayon clippy refactor ecash contract a bit + introduce deposit storage reworked find_proposal_id various minor fixed add offline_zk_nyms to nym-node everywhere add missing #query change test value to fit new serialization optimised deposits storage removed duplicate decompression code using deposit_id instead of transaction hash removed freepasses split up ecash handling unified shared state fixed deposit_id parsing log recovered deposit id removed online verification add detailed build info to ecash contract fixed deserialisation of deposit amount received from nyxd queries changed deposit to only persist attached pubkey first iteration of split of verification and redemption basic tool for setting up new network expanded the tool with the option to bypass DKG rename + init network without DKG setting up locally running apis ecash key migration more local functionalities wip fixing sql schemas gateway immediately submitting redemption proposal and getting it passed if valid most of the gateway logic for split redemption with error recovery fixed gateway not persisting ecash signers simplify creation of compatible client create properly serialised ecash key from the beginning rebuild missing tickets and proposals on startup stop ticket issuance during DKG transition fixing build issues split out ecash storage on nym-api side master-verification-key route caching all the signatures and keys implemented aggregated routes for nym-apis swagger UI for ecash endpoints added explicit annotation for index and expiration signatures revamped client ticketbook storage save all recovery information in the same underlying storage wrapper for bloomfilter being more aggressive with marking tickets as used ensure client has correct signatures before making deposit fix deserialisation of AggregatedExpirationDateSignatureResponse + add ticketbook table split nym-api ecash routes handlers into multiple files fixed deserialisation of encoded expiration date add tt_gamma1 to challenge and change naming for paper consistency rotating double spending bloomfilter nym-api test fixes + make sure to insert initial BF params fixed ecash benchmark code updated contract schema updated CI to not upload gateway/mixnode binaries ticket bandwidth revocation added default deserialisation for zk nym config post-rebase fixes --- .../workflows/ci-build-upload-binaries.yml | 2 +- .../ci-contracts-upload-binaries.yml | 3 +- Cargo.lock | 1516 ++++++++++++----- Cargo.toml | 35 +- Makefile | 2 +- clients/native/src/commands/mod.rs | 5 + .../native/src/commands/show_ticketbooks.rs | 32 + clients/socks5/src/commands/mod.rs | 5 + .../socks5/src/commands/show_ticketbooks.rs | 32 + common/bandwidth-controller/Cargo.toml | 3 +- .../bandwidth-controller/src/acquire/mod.rs | 134 +- .../bandwidth-controller/src/acquire/state.rs | 14 - common/bandwidth-controller/src/error.rs | 21 +- common/bandwidth-controller/src/lib.rs | 239 ++- common/bandwidth-controller/src/utils.rs | 189 +- common/client-core/Cargo.toml | 4 +- .../cli_helpers/client_show_ticketbooks.rs | 127 ++ common/client-core/src/cli_helpers/mod.rs | 1 + .../src/client/mix_traffic/transceiver.rs | 12 +- common/client-core/src/error.rs | 5 + .../client-libs/gateway-client/src/client.rs | 117 +- .../gateway-client/src/socket_state.rs | 14 +- .../client-libs/validator-client/Cargo.toml | 4 +- .../validator-client/src/client.rs | 85 +- .../validator-client/src/coconut/mod.rs | 38 +- .../client-libs/validator-client/src/error.rs | 4 +- .../client-libs/validator-client/src/lib.rs | 2 +- .../validator-client/src/nym_api/mod.rs | 233 ++- .../validator-client/src/nym_api/routes.rs | 29 +- .../coconut_bandwidth_query_client.rs | 100 -- .../coconut_bandwidth_signing_client.rs | 153 -- .../nyxd/contract_traits/dkg_query_client.rs | 8 + .../contract_traits/ecash_query_client.rs | 123 ++ .../contract_traits/ecash_signing_client.rs | 124 ++ .../src/nyxd/contract_traits/mod.rs | 18 +- .../contract_traits/multisig_query_client.rs | 24 +- .../multisig_signing_client.rs | 6 +- .../client_traits/signing_client.rs | 211 +-- .../src/nyxd/cosmwasm_client/helpers.rs | 78 + .../src/nyxd/cosmwasm_client/logs.rs | 24 +- .../src/nyxd/cosmwasm_client/mod.rs | 2 + .../src/nyxd/cosmwasm_client/types.rs | 10 +- .../validator-client/src/nyxd/error.rs | 12 + .../validator-client/src/nyxd/helpers.rs | 15 +- .../validator-client/src/nyxd/mod.rs | 19 +- common/commands/Cargo.toml | 2 +- .../commands/src/coconut/generate_freepass.rs | 194 --- ...rt_credential.rs => import_ticket_book.rs} | 0 ...ue_credentials.rs => issue_ticket_book.rs} | 24 +- common/commands/src/coconut/mod.rs | 20 +- ..._credentials.rs => recover_ticket_book.rs} | 13 +- common/commands/src/utils.rs | 28 + ...oconut_bandwidth.rs => ecash_bandwidth.rs} | 19 +- .../src/validator/cosmwasm/generators/mod.rs | 4 +- .../validator/cosmwasm/generators/multisig.rs | 18 +- .../coconut-dkg/src/msg.rs | 3 + .../contracts-common/src/events.rs | 18 + .../ecash-contract/Cargo.toml | 21 + .../ecash-contract/src/blacklist.rs | 71 + .../ecash-contract/src/deposit.rs | 76 + .../ecash-contract/src/error.rs | 68 + .../ecash-contract/src/event_attributes.rs | 4 + .../ecash-contract/src/events.rs | 18 + .../ecash-contract/src/lib.rs | 12 + .../ecash-contract/src/msg.rs | 76 + .../ecash-contract/src/redeem_credential.rs | 5 + .../multisig-contract/src/error.rs | 6 + common/credential-storage/Cargo.toml | 15 +- .../20241104120001_add_ecash_tables.sql | 66 + .../credential-storage/src/backends/memory.rs | 312 ++-- common/credential-storage/src/backends/mod.rs | 2 +- .../credential-storage/src/backends/sqlite.rs | 287 +++- .../src/ephemeral_storage.rs | 160 +- common/credential-storage/src/error.rs | 14 + common/credential-storage/src/lib.rs | 15 +- common/credential-storage/src/models.rs | 70 +- .../src/persistent_storage.rs | 125 -- .../src/persistent_storage/helpers.rs | 54 + .../src/persistent_storage/mod.rs | 307 ++++ common/credential-storage/src/storage.rs | 103 +- common/credential-utils/Cargo.toml | 6 +- common/credential-utils/src/errors.rs | 20 +- common/credential-utils/src/lib.rs | 1 - .../credential-utils/src/recovery_storage.rs | 74 - common/credential-utils/src/utils.rs | 159 +- common/credentials-interface/Cargo.toml | 5 +- common/credentials-interface/src/lib.rs | 288 ++-- common/credentials/Cargo.toml | 6 +- .../src/coconut/bandwidth/freepass.rs | 142 -- .../src/coconut/bandwidth/issuance.rs | 355 ---- .../src/coconut/bandwidth/issued.rs | 217 --- .../credentials/src/coconut/bandwidth/mod.rs | 22 - .../src/coconut/bandwidth/voucher.rs | 145 -- common/credentials/src/coconut/utils.rs | 97 -- .../src/ecash/bandwidth/issuance.rs | 239 +++ .../credentials/src/ecash/bandwidth/issued.rs | 174 ++ common/credentials/src/ecash/bandwidth/mod.rs | 10 + .../src/ecash/bandwidth/serialiser.rs | 65 + .../credentials/src/{coconut => ecash}/mod.rs | 0 common/credentials/src/ecash/utils.rs | 210 +++ common/credentials/src/error.rs | 17 +- common/credentials/src/lib.rs | 9 +- common/crypto/src/shared_key.rs | 5 +- common/ecash-double-spending/Cargo.toml | 14 + common/ecash-double-spending/src/lib.rs | 136 ++ common/ecash-time/Cargo.toml | 19 + common/ecash-time/src/lib.rs | 71 + common/network-defaults/Cargo.toml | 2 +- common/network-defaults/src/ecash.rs | 39 + common/network-defaults/src/lib.rs | 30 +- common/network-defaults/src/mainnet.rs | 12 +- common/network-defaults/src/var_names.rs | 2 +- common/nym-id/src/error.rs | 4 +- common/nym-id/src/import_credential.rs | 58 +- common/nym_offline_compact_ecash/Cargo.toml | 64 + .../benchmarks_coin_indices_signatures.rs | 116 ++ .../benches/benchmarks_ecash_e2e.rs | 283 +++ .../benchmarks_expiration_date_signatures.rs | 102 ++ .../benches/benchmarks_group_operations.rs | 189 ++ .../src/common_types.rs | 97 ++ .../src/constants.rs | 28 + common/nym_offline_compact_ecash/src/error.rs | 125 ++ .../nym_offline_compact_ecash/src/helpers.rs | 37 + common/nym_offline_compact_ecash/src/lib.rs | 62 + .../src/proofs/mod.rs | 59 + .../src/proofs/proof_spend.rs | 397 +++++ .../src/proofs/proof_withdrawal.rs | 248 +++ .../src/scheme/aggregation.rs | 161 ++ .../src/scheme/coin_indices_signatures.rs | 439 +++++ .../src/scheme/expiration_date_signatures.rs | 500 ++++++ .../src/scheme/identify.rs | 574 +++++++ .../src/scheme/keygen.rs | 657 +++++++ .../src/scheme/mod.rs | 979 +++++++++++ .../src/scheme/setup.rs | 104 ++ .../src/scheme/withdrawal.rs | 480 ++++++ .../src/tests/e2e.rs | 135 ++ .../src/tests/helpers.rs | 178 ++ .../src/tests/mod.rs | 5 + .../nym_offline_compact_ecash/src/traits.rs | 140 ++ common/nym_offline_compact_ecash/src/utils.rs | 404 +++++ common/nymcoconut/benches/benchmarks.rs | 70 +- common/nymcoconut/src/lib.rs | 2 +- common/nymcoconut/src/utils.rs | 16 +- common/pemstore/src/lib.rs | 2 +- common/serde-helpers/Cargo.toml | 22 + common/serde-helpers/src/lib.rs | 33 + common/types/src/transaction.rs | 4 +- contracts/Cargo.lock | 232 ++- contracts/Cargo.toml | 7 +- .../coconut-dkg/schema/nym-coconut-dkg.json | 30 + contracts/coconut-dkg/schema/raw/query.json | 23 + .../raw/response_to_get_epoch_threshold.json | 7 + contracts/coconut-dkg/src/contract.rs | 13 +- .../coconut-dkg/src/epoch_state/queries.rs | 11 +- .../coconut-dkg/src/epoch_state/storage.rs | 7 +- .../transactions/advance_epoch_state.rs | 5 +- contracts/ecash/Cargo.toml | 34 + contracts/ecash/src/contract/helpers.rs | 79 + contracts/ecash/src/contract/mod.rs | 411 +++++ contracts/ecash/src/contract/test.rs | 89 + contracts/ecash/src/deposit.rs | 230 +++ contracts/ecash/src/helpers.rs | 121 ++ contracts/ecash/src/lib.rs | 14 + contracts/ecash/src/multitest.rs | 73 + contracts/ecash/src/support/mod.rs | 5 + contracts/ecash/src/support/tests.rs | 10 + deny.toml | 9 +- envs/local.env | 2 +- envs/qa.env | 2 +- envs/sandbox.env | 2 +- gateway/Cargo.toml | 23 +- gateway/gateway-requests/Cargo.toml | 7 +- gateway/gateway-requests/src/models.rs | 379 +---- .../src/registration/handshake/state.rs | 2 +- gateway/gateway-requests/src/types.rs | 62 +- .../20240624120000_ecash_changes.sql | 93 + gateway/src/commands/helpers.rs | 2 + gateway/src/commands/mod.rs | 2 +- gateway/src/commands/run.rs | 2 +- .../src/commands/setup_network_requester.rs | 2 +- gateway/src/config/mod.rs | 55 +- gateway/src/error.rs | 10 + gateway/src/helpers.rs | 3 +- gateway/src/http/mod.rs | 2 +- gateway/src/main.rs | 2 +- .../node/client_handling/active_clients.rs | 2 +- gateway/src/node/client_handling/bandwidth.rs | 54 +- .../client_handling/embedded_clients/mod.rs | 6 +- .../client_handling/websocket/common_state.rs | 7 +- .../connection_handler/authenticated.rs | 429 ++--- .../websocket/connection_handler/coconut.rs | 284 --- .../ecash/credential_sender.rs | 963 +++++++++++ .../ecash/double_spending.rs | 92 + .../connection_handler/ecash/error.rs | 83 + .../connection_handler/ecash/helpers.rs | 36 + .../websocket/connection_handler/ecash/mod.rs | 175 ++ .../connection_handler/ecash/state.rs | 257 +++ .../websocket/connection_handler/fresh.rs | 92 +- .../websocket/connection_handler/mod.rs | 65 +- .../client_handling/websocket/listener.rs | 32 +- .../receiver/connection_handler.rs | 2 +- .../node/mixnet_handling/receiver/listener.rs | 2 +- gateway/src/node/mod.rs | 45 +- gateway/src/node/storage/bandwidth.rs | 170 +- gateway/src/node/storage/error.rs | 6 + gateway/src/node/storage/mod.rs | 519 +++--- gateway/src/node/storage/models.rs | 50 +- gateway/src/node/storage/shared_keys.rs | 23 +- gateway/src/node/storage/tickets.rs | 358 ++++ nym-api/Cargo.toml | 13 +- .../20240708120000_ecash_tables.sql | 101 ++ nym-api/nym-api-requests/Cargo.toml | 8 +- .../nym-api-requests/src/coconut/models.rs | 287 ---- nym-api/nym-api-requests/src/constants.rs | 7 + .../src/{coconut => ecash}/helpers.rs | 17 +- .../src/{coconut => ecash}/mod.rs | 5 +- nym-api/nym-api-requests/src/ecash/models.rs | 447 +++++ nym-api/nym-api-requests/src/helpers.rs | 141 ++ nym-api/nym-api-requests/src/lib.rs | 4 +- nym-api/nym-api-requests/src/models.rs | 121 +- nym-api/nym-api-requests/src/nym_nodes.rs | 2 +- nym-api/src/coconut/api_routes/mod.rs | 393 ----- nym-api/src/coconut/comm.rs | 119 -- nym-api/src/coconut/deposit.rs | 340 ---- nym-api/src/coconut/helpers.rs | 64 - nym-api/src/coconut/keys/persistence.rs | 43 - nym-api/src/coconut/mod.rs | 65 - nym-api/src/coconut/state.rs | 229 --- nym-api/src/coconut/storage/manager.rs | 363 ---- nym-api/src/coconut/storage/mod.rs | 173 -- nym-api/src/coconut/storage/models.rs | 112 -- nym-api/src/ecash/api_routes/aggregation.rs | 81 + .../{coconut => ecash}/api_routes/helpers.rs | 8 +- nym-api/src/ecash/api_routes/issued.rs | 82 + nym-api/src/ecash/api_routes/mod.rs | 8 + .../src/ecash/api_routes/partial_signing.rs | 118 ++ nym-api/src/ecash/api_routes/spending.rs | 196 +++ nym-api/src/{coconut => ecash}/client.rs | 21 +- nym-api/src/ecash/comm.rs | 147 ++ nym-api/src/ecash/deposit.rs | 107 ++ nym-api/src/{coconut => ecash}/dkg/client.rs | 61 +- .../dkg/controller/error.rs | 22 +- .../{coconut => ecash}/dkg/controller/keys.rs | 29 +- .../{coconut => ecash}/dkg/controller/mod.rs | 10 +- nym-api/src/{coconut => ecash}/dkg/dealing.rs | 32 +- nym-api/src/{coconut => ecash}/dkg/helpers.rs | 6 +- .../{coconut => ecash}/dkg/key_derivation.rs | 44 +- .../dkg/key_finalization.rs | 8 +- .../{coconut => ecash}/dkg/key_validation.rs | 19 +- nym-api/src/{coconut => ecash}/dkg/mod.rs | 4 +- .../src/{coconut => ecash}/dkg/public_key.rs | 8 +- .../dkg/state/dealing_exchange.rs | 2 +- .../dkg/state/in_progress.rs | 0 .../dkg/state/key_derivation.rs | 0 .../dkg/state/key_finalization.rs | 0 .../dkg/state/key_validation.rs | 0 .../src/{coconut => ecash}/dkg/state/mod.rs | 106 +- .../dkg/state/registration.rs | 2 +- .../dkg/state/serde_helpers.rs | 0 nym-api/src/{coconut => ecash}/error.rs | 225 ++- nym-api/src/ecash/helpers.rs | 134 ++ nym-api/src/{coconut => ecash}/keys/mod.rs | 45 +- nym-api/src/ecash/keys/persistence.rs | 77 + nym-api/src/ecash/mod.rs | 47 + nym-api/src/ecash/state/auxiliary.rs | 45 + nym-api/src/ecash/state/bloom.rs | 2 + nym-api/src/ecash/state/global.rs | 33 + nym-api/src/ecash/state/helpers.rs | 160 ++ nym-api/src/ecash/state/local.rs | 107 ++ nym-api/src/ecash/state/mod.rs | 863 ++++++++++ nym-api/src/ecash/storage/helpers.rs | 52 + nym-api/src/ecash/storage/manager.rs | 809 +++++++++ nym-api/src/ecash/storage/mod.rs | 635 +++++++ nym-api/src/ecash/storage/models.rs | 198 +++ .../src/{coconut => ecash}/tests/fixtures.rs | 24 +- .../src/{coconut => ecash}/tests/helpers.rs | 6 +- .../tests/issued_credentials.rs | 53 +- nym-api/src/{coconut => ecash}/tests/mod.rs | 1051 +++--------- nym-api/src/main.rs | 12 +- nym-api/src/node_status_api/models.rs | 8 + .../src/nym_contract_cache/cache/refresher.rs | 4 +- nym-api/src/status/mod.rs | 4 +- nym-api/src/status/routes.rs | 2 +- nym-api/src/support/config/helpers.rs | 2 +- nym-api/src/support/http/mod.rs | 30 +- nym-api/src/support/nyxd/mod.rs | 142 +- nym-node/src/cli/commands/migrate.rs | 11 + nym-node/src/cli/commands/sign.rs | 2 - nym-node/src/config/entry_gateway.rs | 50 + nym-outfox/tests/unittests.rs | 18 +- nym-validator-rewarder/Cargo.toml | 3 +- .../migrations/03_use_deposit_id.sql | 28 + nym-validator-rewarder/src/error.rs | 29 +- .../rewarder/credential_issuance/monitor.rs | 60 +- .../src/rewarder/credential_issuance/types.rs | 4 +- .../src/rewarder/nyxd_client.rs | 37 +- .../src/rewarder/storage/manager.rs | 19 +- .../src/rewarder/storage/mod.rs | 9 +- nym-wallet/nym-wallet-types/src/network/qa.rs | 4 +- .../nym-wallet-types/src/network/sandbox.rs | 4 +- nym-wallet/src-tauri/Cargo.toml | 2 +- sdk/rust/nym-sdk/Cargo.toml | 1 + sdk/rust/nym-sdk/examples/bandwidth.rs | 6 +- sdk/rust/nym-sdk/examples/simple.rs | 1 + sdk/rust/nym-sdk/src/bandwidth.rs | 8 +- sdk/rust/nym-sdk/src/bandwidth/client.rs | 47 +- sdk/rust/nym-sdk/src/error.rs | 15 +- sdk/rust/nym-sdk/src/mixnet.rs | 2 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 14 +- .../ip-packet-router/src/cli/mod.rs | 5 + .../src/cli/show_ticketbooks.rs | 32 + .../network-requester/src/cli/mod.rs | 5 + .../src/cli/show_ticketbooks.rs | 32 + tools/internal/testnet-manager/Cargo.toml | 53 + tools/internal/testnet-manager/Makefile | 2 + tools/internal/testnet-manager/build.rs | 25 + .../dkg-bypass-contract/Cargo.toml | 21 + .../dkg-bypass-contract/Makefile | 5 + .../dkg-bypass-contract/src/contract.rs | 137 ++ .../dkg-bypass-contract/src/lib.rs | 4 + .../dkg-bypass-contract/src/msg.rs | 19 + .../migrations/01_initial_tables.sql | 40 + .../testnet-manager/src/cli/build_info.rs | 17 + .../testnet-manager/src/cli/bypass_dkg.rs | 50 + .../src/cli/initialise_new_network.rs | 48 + .../src/cli/initialise_post_dkg_network.rs | 76 + .../src/cli/load_network_details.rs | 36 + .../testnet-manager/src/cli/local_client.rs | 38 + .../src/cli/local_ecash_apis.rs | 80 + .../testnet-manager/src/cli/local_nodes.rs | 38 + .../testnet-manager/src/cli/migrate.rs | 20 + tools/internal/testnet-manager/src/cli/mod.rs | 108 ++ tools/internal/testnet-manager/src/error.rs | 106 ++ tools/internal/testnet-manager/src/helpers.rs | 153 ++ tools/internal/testnet-manager/src/main.rs | 26 + .../testnet-manager/src/manager/contract.rs | 283 +++ .../testnet-manager/src/manager/dkg_skip.rs | 473 +++++ .../testnet-manager/src/manager/local_apis.rs | 224 +++ .../src/manager/local_client.rs | 265 +++ .../src/manager/local_nodes.rs | 546 ++++++ .../testnet-manager/src/manager/mod.rs | 127 ++ .../testnet-manager/src/manager/network.rs | 200 +++ .../src/manager/network_init.rs | 701 ++++++++ .../src/manager/storage/manager.rs | 183 ++ .../src/manager/storage/mod.rs | 243 +++ .../src/manager/storage/models.rs | 77 + tools/nym-cli/src/coconut/mod.rs | 21 +- tools/nym-cli/src/main.rs | 6 +- .../src/validator/cosmwasm/generators/mod.rs | 4 +- wasm/zknym-lib/Cargo.toml | 3 +- wasm/zknym-lib/src/bandwidth_voucher.rs | 390 ++--- wasm/zknym-lib/src/error.rs | 8 +- .../coconut/mod.rs} | 53 +- .../src/generic_scheme/coconut/types.rs | 170 ++ .../zknym-lib/src/generic_scheme/ecash/mod.rs | 4 + .../src/generic_scheme/ecash/types.rs | 8 + wasm/zknym-lib/src/generic_scheme/mod.rs | 5 + wasm/zknym-lib/src/helpers.rs | 80 + wasm/zknym-lib/src/lib.rs | 7 +- wasm/zknym-lib/src/ticketbook.rs | 14 + wasm/zknym-lib/src/types.rs | 243 --- wasm/zknym-lib/src/vpn_api_client/types.rs | 7 - 362 files changed, 27327 insertions(+), 8756 deletions(-) create mode 100644 clients/native/src/commands/show_ticketbooks.rs create mode 100644 clients/socks5/src/commands/show_ticketbooks.rs delete mode 100644 common/bandwidth-controller/src/acquire/state.rs create mode 100644 common/client-core/src/cli_helpers/client_show_ticketbooks.rs delete mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs delete mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs create mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs create mode 100644 common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs delete mode 100644 common/commands/src/coconut/generate_freepass.rs rename common/commands/src/coconut/{import_credential.rs => import_ticket_book.rs} (100%) rename common/commands/src/coconut/{issue_credentials.rs => issue_ticket_book.rs} (65%) rename common/commands/src/coconut/{recover_credentials.rs => recover_ticket_book.rs} (69%) rename common/commands/src/validator/cosmwasm/generators/{coconut_bandwidth.rs => ecash_bandwidth.rs} (68%) create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/error.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/events.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs create mode 100644 common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs create mode 100644 common/credential-storage/migrations/20241104120001_add_ecash_tables.sql delete mode 100644 common/credential-storage/src/persistent_storage.rs create mode 100644 common/credential-storage/src/persistent_storage/helpers.rs create mode 100644 common/credential-storage/src/persistent_storage/mod.rs delete mode 100644 common/credential-utils/src/recovery_storage.rs delete mode 100644 common/credentials/src/coconut/bandwidth/freepass.rs delete mode 100644 common/credentials/src/coconut/bandwidth/issuance.rs delete mode 100644 common/credentials/src/coconut/bandwidth/issued.rs delete mode 100644 common/credentials/src/coconut/bandwidth/mod.rs delete mode 100644 common/credentials/src/coconut/bandwidth/voucher.rs delete mode 100644 common/credentials/src/coconut/utils.rs create mode 100644 common/credentials/src/ecash/bandwidth/issuance.rs create mode 100644 common/credentials/src/ecash/bandwidth/issued.rs create mode 100644 common/credentials/src/ecash/bandwidth/mod.rs create mode 100644 common/credentials/src/ecash/bandwidth/serialiser.rs rename common/credentials/src/{coconut => ecash}/mod.rs (100%) create mode 100644 common/credentials/src/ecash/utils.rs create mode 100644 common/ecash-double-spending/Cargo.toml create mode 100644 common/ecash-double-spending/src/lib.rs create mode 100644 common/ecash-time/Cargo.toml create mode 100644 common/ecash-time/src/lib.rs create mode 100644 common/network-defaults/src/ecash.rs create mode 100644 common/nym_offline_compact_ecash/Cargo.toml create mode 100644 common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs create mode 100644 common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs create mode 100644 common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs create mode 100644 common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs create mode 100644 common/nym_offline_compact_ecash/src/common_types.rs create mode 100644 common/nym_offline_compact_ecash/src/constants.rs create mode 100644 common/nym_offline_compact_ecash/src/error.rs create mode 100644 common/nym_offline_compact_ecash/src/helpers.rs create mode 100644 common/nym_offline_compact_ecash/src/lib.rs create mode 100644 common/nym_offline_compact_ecash/src/proofs/mod.rs create mode 100644 common/nym_offline_compact_ecash/src/proofs/proof_spend.rs create mode 100644 common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/aggregation.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/identify.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/keygen.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/mod.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/setup.rs create mode 100644 common/nym_offline_compact_ecash/src/scheme/withdrawal.rs create mode 100644 common/nym_offline_compact_ecash/src/tests/e2e.rs create mode 100644 common/nym_offline_compact_ecash/src/tests/helpers.rs create mode 100644 common/nym_offline_compact_ecash/src/tests/mod.rs create mode 100644 common/nym_offline_compact_ecash/src/traits.rs create mode 100644 common/nym_offline_compact_ecash/src/utils.rs create mode 100644 common/serde-helpers/Cargo.toml create mode 100644 common/serde-helpers/src/lib.rs create mode 100644 contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json create mode 100644 contracts/ecash/Cargo.toml create mode 100644 contracts/ecash/src/contract/helpers.rs create mode 100644 contracts/ecash/src/contract/mod.rs create mode 100644 contracts/ecash/src/contract/test.rs create mode 100644 contracts/ecash/src/deposit.rs create mode 100644 contracts/ecash/src/helpers.rs create mode 100644 contracts/ecash/src/lib.rs create mode 100644 contracts/ecash/src/multitest.rs create mode 100644 contracts/ecash/src/support/mod.rs create mode 100644 contracts/ecash/src/support/tests.rs create mode 100644 gateway/migrations/20240624120000_ecash_changes.sql delete mode 100644 gateway/src/node/client_handling/websocket/connection_handler/coconut.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs create mode 100644 gateway/src/node/storage/tickets.rs create mode 100644 nym-api/migrations/20240708120000_ecash_tables.sql delete mode 100644 nym-api/nym-api-requests/src/coconut/models.rs create mode 100644 nym-api/nym-api-requests/src/constants.rs rename nym-api/nym-api-requests/src/{coconut => ecash}/helpers.rs (69%) rename nym-api/nym-api-requests/src/{coconut => ecash}/mod.rs (59%) create mode 100644 nym-api/nym-api-requests/src/ecash/models.rs create mode 100644 nym-api/nym-api-requests/src/helpers.rs delete mode 100644 nym-api/src/coconut/api_routes/mod.rs delete mode 100644 nym-api/src/coconut/comm.rs delete mode 100644 nym-api/src/coconut/deposit.rs delete mode 100644 nym-api/src/coconut/helpers.rs delete mode 100644 nym-api/src/coconut/keys/persistence.rs delete mode 100644 nym-api/src/coconut/mod.rs delete mode 100644 nym-api/src/coconut/state.rs delete mode 100644 nym-api/src/coconut/storage/manager.rs delete mode 100644 nym-api/src/coconut/storage/mod.rs delete mode 100644 nym-api/src/coconut/storage/models.rs create mode 100644 nym-api/src/ecash/api_routes/aggregation.rs rename nym-api/src/{coconut => ecash}/api_routes/helpers.rs (81%) create mode 100644 nym-api/src/ecash/api_routes/issued.rs create mode 100644 nym-api/src/ecash/api_routes/mod.rs create mode 100644 nym-api/src/ecash/api_routes/partial_signing.rs create mode 100644 nym-api/src/ecash/api_routes/spending.rs rename nym-api/src/{coconut => ecash}/client.rs (85%) create mode 100644 nym-api/src/ecash/comm.rs create mode 100644 nym-api/src/ecash/deposit.rs rename nym-api/src/{coconut => ecash}/dkg/client.rs (82%) rename nym-api/src/{coconut => ecash}/dkg/controller/error.rs (79%) rename nym-api/src/{coconut => ecash}/dkg/controller/keys.rs (79%) rename nym-api/src/{coconut => ecash}/dkg/controller/mod.rs (97%) rename nym-api/src/{coconut => ecash}/dkg/dealing.rs (97%) rename nym-api/src/{coconut => ecash}/dkg/helpers.rs (94%) rename nym-api/src/{coconut => ecash}/dkg/key_derivation.rs (96%) rename nym-api/src/{coconut => ecash}/dkg/key_finalization.rs (96%) rename nym-api/src/{coconut => ecash}/dkg/key_validation.rs (96%) rename nym-api/src/{coconut => ecash}/dkg/mod.rs (97%) rename nym-api/src/{coconut => ecash}/dkg/public_key.rs (96%) rename nym-api/src/{coconut => ecash}/dkg/state/dealing_exchange.rs (94%) rename nym-api/src/{coconut => ecash}/dkg/state/in_progress.rs (100%) rename nym-api/src/{coconut => ecash}/dkg/state/key_derivation.rs (100%) rename nym-api/src/{coconut => ecash}/dkg/state/key_finalization.rs (100%) rename nym-api/src/{coconut => ecash}/dkg/state/key_validation.rs (100%) rename nym-api/src/{coconut => ecash}/dkg/state/mod.rs (76%) rename nym-api/src/{coconut => ecash}/dkg/state/registration.rs (98%) rename nym-api/src/{coconut => ecash}/dkg/state/serde_helpers.rs (100%) rename nym-api/src/{coconut => ecash}/error.rs (50%) create mode 100644 nym-api/src/ecash/helpers.rs rename nym-api/src/{coconut => ecash}/keys/mod.rs (60%) create mode 100644 nym-api/src/ecash/keys/persistence.rs create mode 100644 nym-api/src/ecash/mod.rs create mode 100644 nym-api/src/ecash/state/auxiliary.rs create mode 100644 nym-api/src/ecash/state/bloom.rs create mode 100644 nym-api/src/ecash/state/global.rs create mode 100644 nym-api/src/ecash/state/helpers.rs create mode 100644 nym-api/src/ecash/state/local.rs create mode 100644 nym-api/src/ecash/state/mod.rs create mode 100644 nym-api/src/ecash/storage/helpers.rs create mode 100644 nym-api/src/ecash/storage/manager.rs create mode 100644 nym-api/src/ecash/storage/mod.rs create mode 100644 nym-api/src/ecash/storage/models.rs rename nym-api/src/{coconut => ecash}/tests/fixtures.rs (93%) rename nym-api/src/{coconut => ecash}/tests/helpers.rs (96%) rename nym-api/src/{coconut => ecash}/tests/issued_credentials.rs (81%) rename nym-api/src/{coconut => ecash}/tests/mod.rs (59%) create mode 100644 nym-validator-rewarder/migrations/03_use_deposit_id.sql create mode 100644 service-providers/ip-packet-router/src/cli/show_ticketbooks.rs create mode 100644 service-providers/network-requester/src/cli/show_ticketbooks.rs create mode 100644 tools/internal/testnet-manager/Cargo.toml create mode 100644 tools/internal/testnet-manager/Makefile create mode 100644 tools/internal/testnet-manager/build.rs create mode 100644 tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml create mode 100644 tools/internal/testnet-manager/dkg-bypass-contract/Makefile create mode 100644 tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs create mode 100644 tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs create mode 100644 tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs create mode 100644 tools/internal/testnet-manager/migrations/01_initial_tables.sql create mode 100644 tools/internal/testnet-manager/src/cli/build_info.rs create mode 100644 tools/internal/testnet-manager/src/cli/bypass_dkg.rs create mode 100644 tools/internal/testnet-manager/src/cli/initialise_new_network.rs create mode 100644 tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs create mode 100644 tools/internal/testnet-manager/src/cli/load_network_details.rs create mode 100644 tools/internal/testnet-manager/src/cli/local_client.rs create mode 100644 tools/internal/testnet-manager/src/cli/local_ecash_apis.rs create mode 100644 tools/internal/testnet-manager/src/cli/local_nodes.rs create mode 100644 tools/internal/testnet-manager/src/cli/migrate.rs create mode 100644 tools/internal/testnet-manager/src/cli/mod.rs create mode 100644 tools/internal/testnet-manager/src/error.rs create mode 100644 tools/internal/testnet-manager/src/helpers.rs create mode 100644 tools/internal/testnet-manager/src/main.rs create mode 100644 tools/internal/testnet-manager/src/manager/contract.rs create mode 100644 tools/internal/testnet-manager/src/manager/dkg_skip.rs create mode 100644 tools/internal/testnet-manager/src/manager/local_apis.rs create mode 100644 tools/internal/testnet-manager/src/manager/local_client.rs create mode 100644 tools/internal/testnet-manager/src/manager/local_nodes.rs create mode 100644 tools/internal/testnet-manager/src/manager/mod.rs create mode 100644 tools/internal/testnet-manager/src/manager/network.rs create mode 100644 tools/internal/testnet-manager/src/manager/network_init.rs create mode 100644 tools/internal/testnet-manager/src/manager/storage/manager.rs create mode 100644 tools/internal/testnet-manager/src/manager/storage/mod.rs create mode 100644 tools/internal/testnet-manager/src/manager/storage/models.rs rename wasm/zknym-lib/src/{generic_scheme.rs => generic_scheme/coconut/mod.rs} (88%) create mode 100644 wasm/zknym-lib/src/generic_scheme/coconut/types.rs create mode 100644 wasm/zknym-lib/src/generic_scheme/ecash/mod.rs create mode 100644 wasm/zknym-lib/src/generic_scheme/ecash/types.rs create mode 100644 wasm/zknym-lib/src/generic_scheme/mod.rs create mode 100644 wasm/zknym-lib/src/helpers.rs create mode 100644 wasm/zknym-lib/src/ticketbook.rs diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 02803c4327..dbe39819ec 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -42,7 +42,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-20.04] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index 4347e5f333..14d0b63eae 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-20.04] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: @@ -58,6 +58,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/Cargo.lock b/Cargo.lock index 6cce424e43..3b0583fa59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "addr" version = "0.15.6" @@ -20,9 +32,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -74,7 +86,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom", "once_cell", "version_check", ] @@ -177,9 +189,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -196,9 +208,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "argon2" @@ -254,7 +266,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -265,23 +277,24 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "async-tungstenite" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e9efbe14612da0a19fb983059a0b621e9cf6225d7018ecab4f9988215540dc" +checksum = "3609af4bbf701ddaf1f6bb4e6257dff4ff8932327d0e685d3f653724c258b1ac" dependencies = [ "futures-io", "futures-util", "log", "pin-project-lite", - "rustls-native-certs", + "rustls-native-certs 0.7.0", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", - "tungstenite", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", ] [[package]] @@ -347,7 +360,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -459,9 +472,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -547,6 +560,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -639,16 +658,29 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "bloomfilter" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0bdbcf2078e0ba8a74e1fe0cf36f54054a04485759b61dfd60b174658e9607" +dependencies = [ + "bit-vec", + "getrandom", + "siphasher", +] + [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" dependencies = [ "digest 0.9.0", "ff", "group", "pairing", "rand_core 0.6.4", + "serde", + "serdect 0.3.0-pre.0", "subtle 2.5.0", "zeroize", ] @@ -720,9 +752,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -764,9 +796,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "celes" @@ -791,9 +823,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha" @@ -896,9 +928,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -906,45 +938,45 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", + "clap_lex 0.7.1", "strsim 0.11.1", ] [[package]] name = "clap_complete" -version = "4.5.2" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", ] [[package]] name = "clap_complete_fig" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110" +checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -958,9 +990,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "cloudabi" @@ -1009,6 +1041,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "comfy-table" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" +dependencies = [ + "crossterm 0.27.0", + "strum 0.26.3", + "strum_macros 0.26.4", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1018,6 +1062,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "console-api" version = "0.5.0" @@ -1135,19 +1192,19 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.34.1", ] [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.37.0", ] [[package]] @@ -1157,7 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmos-sdk-proto 0.20.0", "ecdsa", "eyre", "k256", @@ -1166,17 +1223,17 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.34.1", "thiserror", ] [[package]] name = "cosmrs" -version = "0.15.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmos-sdk-proto 0.22.0-pre", "ecdsa", "eyre", "k256", @@ -1185,7 +1242,7 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", ] @@ -1257,6 +1314,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-storage" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8935079712688b8d8d4c036378b38b49d13692621c6fcba96700fadfd5126a18" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1283,9 +1350,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1316,6 +1383,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.7", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "criterion-plot" version = "0.5.0" @@ -1328,9 +1421,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1365,9 +1458,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1378,8 +1471,8 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio 0.8.11", - "parking_lot 0.12.2", + "mio", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", @@ -1394,13 +1487,26 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio 0.8.11", - "parking_lot 0.12.2", + "mio", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "parking_lot 0.12.3", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1558,7 +1664,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1683,12 +1789,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -1707,16 +1813,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.63", + "strsim 0.11.1", + "syn 2.0.66", ] [[package]] @@ -1732,13 +1838,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.20.8", + "darling_core 0.20.9", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1780,6 +1886,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "der" version = "0.7.9" @@ -1841,7 +1959,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1915,6 +2033,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "dkg-bypass-contract" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "nym-coconut-dkg-common", + "nym-contracts-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1943,7 +2084,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", + "serdect 0.2.0", "signature", "spki", ] @@ -2004,9 +2145,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elliptic-curve" @@ -2023,11 +2164,17 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -2093,14 +2240,14 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "explorer-api" -version = "1.1.37" +version = "1.1.36" dependencies = [ "chrono", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "humantime-serde", "isocountry", - "itertools 0.12.1", + "itertools 0.13.0", "log", "maxminddb", "nym-bin-common", @@ -2181,6 +2328,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2214,9 +2373,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment" -version = "0.10.18" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.0", "pear", @@ -2238,12 +2397,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flate2" version = "1.0.30" @@ -2400,7 +2553,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2473,19 +2626,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -2495,7 +2635,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -2523,9 +2663,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -2837,12 +2977,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -2856,9 +2996,9 @@ checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "httpcodec" @@ -2903,9 +3043,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", @@ -2953,7 +3093,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -2982,7 +3122,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2990,9 +3130,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -3031,6 +3171,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3049,12 +3307,14 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -3065,13 +3325,15 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfbcff6ae46750b15cc594bfd277b188cbddcfdc1817848f97f03f26f8625b9e" +version = "0.4.2" +source = "git+https://github.com/TiemenSch/rust-indexed-db?branch=update-uuid#9745d015707008b0c410115d787014a6d1af2efb" dependencies = [ + "accessory", "cfg-if", + "delegate-display", + "fancy_constructor", "js-sys", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3099,6 +3361,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inlinable_string" version = "0.1.15" @@ -3153,9 +3428,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -3281,6 +3556,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3403,9 +3687,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -3436,9 +3720,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -3448,9 +3732,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lioness" @@ -3464,6 +3748,12 @@ dependencies = [ "keystream", ] +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -3511,6 +3801,53 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "matchers" version = "0.1.0" @@ -3577,9 +3914,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -3592,22 +3929,10 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - [[package]] name = "mix-fetch-wasm" version = "1.3.0-rc.0" @@ -3750,9 +4075,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -3789,7 +4114,7 @@ dependencies = [ "inotify", "kqueue", "libc", - "mio 0.8.11", + "mio", "walkdir", "windows-sys 0.45.0", ] @@ -3859,16 +4184,24 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "nym-api" -version = "1.1.41" +version = "1.1.40" dependencies = [ "anyhow", "async-trait", + "bincode", "bip39", + "bloomfilter", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", "console-subscriber", "cosmwasm-std", "cw-utils", @@ -3879,21 +4212,25 @@ dependencies = [ "futures", "getset", "humantime-serde", - "itertools 0.12.1", + "itertools 0.13.0", "k256", "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", "nym-coconut", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-dkg", + "nym-ecash-contract-common", + "nym-ecash-double-spending", + "nym-ecash-time", "nym-gateway-client", "nym-inclusion-probability", "nym-mixnet-contract-common", @@ -3935,19 +4272,24 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.5.1", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", + "nym-compact-ecash", "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-mixnet-contract-common", "nym-node-requests", "rocket", "schemars", "serde", + "serde-helpers", "serde_json", - "tendermint", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", "time", "ts-rs", ] @@ -4016,11 +4358,12 @@ version = "0.1.0" dependencies = [ "bip39", "log", - "nym-coconut", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", "nym-network-defaults", "nym-validator-client", "rand 0.8.5", @@ -4033,7 +4376,7 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "const-str", @@ -4057,7 +4400,7 @@ name = "nym-bity-integration" version = "0.1.0" dependencies = [ "anyhow", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "k256", "nym-cli-commands", @@ -4069,13 +4412,13 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.39" +version = "1.1.38" dependencies = [ "anyhow", "base64 0.13.1", "bip39", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "dotenvy", @@ -4101,9 +4444,9 @@ dependencies = [ "bip39", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", - "comfy-table", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "clap 4.5.7", + "comfy-table 6.2.0", + "cosmrs 0.17.0-pre", "cosmwasm-std", "csv", "cw-utils", @@ -4116,7 +4459,6 @@ dependencies = [ "nym-bandwidth-controller", "nym-bin-common", "nym-client-core", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", "nym-config", "nym-contracts-common", @@ -4125,6 +4467,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-id", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -4148,10 +4491,10 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.38" +version = "1.1.37" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "log", @@ -4191,7 +4534,8 @@ dependencies = [ "base64 0.21.7", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", + "comfy-table 7.1.1", "futures", "gloo-timers", "http-body-util", @@ -4207,6 +4551,7 @@ dependencies = [ "nym-country-group", "nym-credential-storage", "nym-crypto", + "nym-ecash-time", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", @@ -4231,7 +4576,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4260,7 +4605,7 @@ name = "nym-client-core-gateways-storage" version = "0.1.0" dependencies = [ "async-trait", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-crypto", "nym-gateway-requests", @@ -4327,13 +4672,13 @@ version = "0.5.0" dependencies = [ "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "digest 0.9.0", "doc-comment", "ff", - "getrandom 0.2.15", + "getrandom", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-dkg", "nym-pemstore", "rand 0.8.5", @@ -4368,6 +4713,28 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "bs58 0.5.1", + "cfg-if", + "criterion 0.5.1", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-pemstore", + "rand 0.8.5", + "rayon", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -4409,7 +4776,12 @@ name = "nym-credential-storage" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "log", + "nym-compact-ecash", + "nym-credentials", + "nym-ecash-time", + "serde", "sqlx", "thiserror", "tokio", @@ -4423,12 +4795,14 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-coconut", + "nym-compact-ecash", "nym-config", "nym-credential-storage", "nym-credentials", + "nym-ecash-time", "nym-validator-client", "thiserror", + "time", "tokio", ] @@ -4438,11 +4812,14 @@ version = "0.1.0" dependencies = [ "bincode", "bls12_381", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-api-requests", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-network-defaults", "nym-validator-client", "rand 0.8.5", "serde", @@ -4456,9 +4833,12 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-coconut", + "nym-compact-ecash", + "nym-ecash-time", + "rand 0.8.5", "serde", "thiserror", + "time", ] [[package]] @@ -4494,7 +4874,7 @@ dependencies = [ "bitvec", "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "ff", "group", "lazy_static", @@ -4510,6 +4890,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "nym-multisig-contract-common", + "thiserror", +] + +[[package]] +name = "nym-ecash-double-spending" +version = "0.1.0" +dependencies = [ + "bit-vec", + "bloomfilter", + "nym-network-defaults", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "nym-compact-ecash", + "time", +] + [[package]] name = "nym-execute" version = "0.1.0" @@ -4562,9 +4973,12 @@ dependencies = [ "anyhow", "async-trait", "bip39", + "bloomfilter", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", + "cosmwasm-std", + "cw-utils", "dashmap", "defguard_wireguard_rs", "dirs 4.0.0", @@ -4572,7 +4986,6 @@ dependencies = [ "futures", "humantime-serde", "ipnetwork 0.16.0", - "log", "nym-api-requests", "nym-authenticator", "nym-bin-common", @@ -4580,6 +4993,8 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-double-spending", "nym-gateway-requests", "nym-ip-packet-router", "nym-mixnet-client", @@ -4598,6 +5013,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "si-scale", "sqlx", "subtle-encoding", "thiserror", @@ -4606,6 +5022,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", + "tracing", "url", "zeroize", ] @@ -4615,7 +5032,7 @@ name = "nym-gateway-client" version = "0.1.0" dependencies = [ "futures", - "getrandom 0.2.15", + "getrandom", "gloo-utils 0.2.0", "log", "nym-bandwidth-controller", @@ -4636,7 +5053,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4651,7 +5068,7 @@ dependencies = [ "bs58 0.5.1", "futures", "generic-array 0.14.7", - "log", + "nym-compact-ecash", "nym-credentials", "nym-credentials-interface", "nym-crypto", @@ -4662,7 +5079,8 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tungstenite", + "tracing", + "tungstenite 0.20.1", "wasmtimer", "zeroize", ] @@ -4725,7 +5143,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "nym-bin-common", "nym-credential-storage", "nym-id", @@ -4767,7 +5185,7 @@ dependencies = [ "bincode", "bs58 0.5.1", "bytes", - "clap 4.5.4", + "clap 4.5.7", "etherparse", "futures", "log", @@ -4862,7 +5280,7 @@ dependencies = [ "anyhow", "axum 0.7.5", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "dirs 4.0.0", @@ -4962,13 +5380,13 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.39" +version = "1.1.38" dependencies = [ "addr", "anyhow", "async-trait", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "humantime-serde", @@ -5013,14 +5431,14 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.5" +version = "1.1.4" dependencies = [ "anyhow", "bip39", "bs58 0.5.1", "cargo_metadata", "celes", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "humantime-serde", @@ -5167,7 +5585,7 @@ name = "nym-nr-query" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-network-defaults", @@ -5193,10 +5611,10 @@ dependencies = [ "blake3", "chacha20", "chacha20poly1305", - "criterion", + "criterion 0.4.0", "curve25519-dalek 4.1.2", "fastrand 2.1.0", - "getrandom 0.2.15", + "getrandom", "log", "rand 0.8.5", "rayon", @@ -5244,7 +5662,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pretty_env_logger", "rand 0.8.5", "reqwest 0.12.4", @@ -5255,6 +5673,7 @@ dependencies = [ "tokio-util", "toml 0.5.11", "url", + "zeroize", ] [[package]] @@ -5276,10 +5695,10 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.38" +version = "1.1.37" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-client-core", @@ -5548,7 +5967,7 @@ dependencies = [ "aes-gcm", "argon2", "generic-array 0.14.7", - "getrandom 0.2.15", + "getrandom", "rand 0.8.5", "serde", "serde_json", @@ -5611,11 +6030,11 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.7", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "eyre", "hmac", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-config", "nym-crypto", @@ -5644,7 +6063,7 @@ dependencies = [ "bip32", "bip39", "colored", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -5654,27 +6073,29 @@ dependencies = [ "eyre", "flate2", "futures", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-api-requests", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", + "nym-ecash-contract-common", "nym-group-contract-common", "nym-http-api-client", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", "nym-vesting-contract-common", - "prost 0.12.4", + "prost 0.12.6", "reqwest 0.12.4", "serde", "serde_json", "sha2 0.9.9", "tendermint-rpc", "thiserror", + "time", "tokio", "ts-rs", "url", @@ -5688,18 +6109,19 @@ version = "0.1.0" dependencies = [ "anyhow", "bip39", - "clap 4.5.4", + "clap 4.5.7", "cosmwasm-std", "futures", "humantime 2.1.0", "humantime-serde", "nym-bin-common", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-credentials", "nym-crypto", + "nym-ecash-time", "nym-network-defaults", "nym-task", "nym-validator-client", @@ -5734,7 +6156,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", "nym-config", @@ -5791,11 +6213,11 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.4" +version = "0.1.3" dependencies = [ "anyhow", "bytes", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "flate2", "futures", @@ -5825,14 +6247,14 @@ version = "0.1.0" dependencies = [ "async-trait", "const_format", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "futures", "humantime 2.1.0", "serde", "sha2 0.10.8", "sqlx", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", "time", @@ -5845,9 +6267,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -6051,9 +6473,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.10", @@ -6123,14 +6545,14 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "peg" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "8a625d12ad770914cbf7eff6f9314c3ef803bfe364a1b20bc36ddf56673e71e5" dependencies = [ "peg-macros", "peg-runtime", @@ -6138,9 +6560,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "f241d42067ed3ab6a4fece1db720838e1418f36d868585a27931f95d6bc03582" dependencies = [ "peg-runtime", "proc-macro2", @@ -6149,9 +6571,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "pem" @@ -6201,7 +6623,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6232,7 +6654,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6271,9 +6693,9 @@ checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -6284,15 +6706,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -6336,6 +6758,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -6394,9 +6822,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -6409,7 +6837,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "version_check", "yansi", ] @@ -6424,7 +6852,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "protobuf", "thiserror", ] @@ -6441,12 +6869,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.5", + "prost-derive 0.12.6", ] [[package]] @@ -6464,15 +6892,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6486,11 +6914,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost 0.12.4", + "prost 0.12.6", ] [[package]] @@ -6501,9 +6929,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psl" -version = "2.1.39" +version = "2.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b320cda4ad7e8f4269fa415754418f83b38c666a5e2e99ea48825b274a373f3" +checksum = "ecec637c2e9d0c8c4bf78df069a53a103ebe3dbd0dc7eff1d60c1006a1c97254" dependencies = [ "psl-types", ] @@ -6561,7 +6989,7 @@ dependencies = [ "libc", "rand_chacha 0.1.1", "rand_core 0.4.2", - "rand_hc 0.1.0", + "rand_hc", "rand_isaac", "rand_jitter", "rand_os", @@ -6570,19 +6998,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", -] - [[package]] name = "rand" version = "0.8.5" @@ -6604,16 +7019,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -6644,9 +7049,6 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] [[package]] name = "rand_core" @@ -6654,7 +7056,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom", ] [[package]] @@ -6676,15 +7078,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "rand_isaac" version = "0.1.1" @@ -6818,7 +7211,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.15", + "getrandom", "libredox", "thiserror", ] @@ -6840,19 +7233,19 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -6866,13 +7259,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -6883,9 +7276,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -6901,7 +7294,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -6911,7 +7304,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -6968,7 +7361,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.1", + "webpki-roots 0.26.2", "winreg 0.52.0", ] @@ -7005,7 +7398,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -7040,7 +7433,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -7071,7 +7464,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.63", + "syn 2.0.66", "unicode-xid", "version_check", ] @@ -7103,7 +7496,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "indexmap 2.2.6", "log", "memchr", @@ -7168,7 +7561,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.63", + "syn 2.0.66", "walkdir", ] @@ -7252,7 +7645,7 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle 2.5.0", "zeroize", ] @@ -7269,6 +7662,19 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -7306,9 +7712,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -7317,9 +7723,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -7329,9 +7735,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safer-ffi" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e0f9180bdf2f29dd5c731789ba0e68f65233c822ece645d288ec7d112e6333" +checksum = "44abae8773dc41fb96af52696b834b1f4c806006b456b22ee3602f7b061e3ad0" dependencies = [ "inventory", "libc", @@ -7346,9 +7752,9 @@ dependencies = [ [[package]] name = "safer_ffi-proc_macros" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b37c0f75bc1a994ff8c8290b77005ea09951b59f6855f8c42667a57ed3285" +checksum = "6c9d4117a8a72f9b615169d4d720d79e74931f74003c73cc2f3927c700156ddf" dependencies = [ "macro_rules_attribute", "prettyplease", @@ -7377,9 +7783,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -7390,14 +7796,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals 0.29.0", - "syn 2.0.63", + "serde_derive_internals 0.29.1", + "syn 2.0.66", ] [[package]] @@ -7432,7 +7838,7 @@ dependencies = [ "der", "generic-array 0.14.7", "pkcs8", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] @@ -7504,13 +7910,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-helpers" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "bs58 0.5.1", + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -7553,13 +7968,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7570,18 +7985,18 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7613,7 +8028,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7661,10 +8076,10 @@ version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.20.8", + "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7690,6 +8105,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791ef964bfaba6be28a5c3f0c56836e17cb711ac009ca1074b9c735a3ebf240a" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7757,7 +8182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] @@ -7780,6 +8205,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -7890,11 +8321,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] @@ -7992,7 +8422,7 @@ name = "ssl-inject" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "hex", "tokio", ] @@ -8006,6 +8436,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "state" version = "0.6.0" @@ -8017,13 +8453,13 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -8062,6 +8498,12 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "strum_macros" version = "0.23.1" @@ -8098,7 +8540,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", ] [[package]] @@ -8141,9 +8596,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -8162,6 +8617,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sysinfo" version = "0.27.8" @@ -8221,9 +8687,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -8257,8 +8723,8 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", "ripemd", "serde", "serde_bytes", @@ -8268,22 +8734,53 @@ dependencies = [ "signature", "subtle 2.5.0", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.34.1", + "time", + "zeroize", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.12.6", + "prost-types 0.12.6", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.5.0", + "subtle-encoding", + "tendermint-proto 0.37.0", "time", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.34.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", - "toml 0.5.11", + "tendermint 0.37.0", + "toml 0.8.14", "url", ] @@ -8297,8 +8794,24 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost 0.12.6", + "prost-types 0.12.6", "serde", "serde_bytes", "subtle-encoding", @@ -8307,18 +8820,19 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" dependencies = [ "async-trait", "async-tungstenite", "bytes", "flex-error", "futures", - "getrandom 0.2.15", + "getrandom", "peg", "pin-project", + "rand 0.8.5", "reqwest 0.11.27", "semver 1.0.23", "serde", @@ -8326,15 +8840,15 @@ dependencies = [ "serde_json", "subtle 2.5.0", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-config", - "tendermint-proto", + "tendermint-proto 0.37.0", "thiserror", "time", "tokio", "tracing", "url", - "uuid 0.8.2", + "uuid", "walkdir", ] @@ -8347,6 +8861,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "testnet-manager" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "bs58 0.5.1", + "clap 4.5.7", + "console", + "cw-utils", + "dkg-bypass-contract", + "indicatif", + "nym-bin-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-pemstore", + "nym-validator-client", + "nym-vesting-contract-common", + "rand 0.8.5", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror", + "time", + "tokio", + "toml 0.8.14", + "tracing", + "url", + "zeroize", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -8355,22 +8908,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8439,6 +8992,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8466,21 +9029,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.1", - "parking_lot 0.12.2", + "mio", + "num_cpus", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -8495,13 +9059,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8575,12 +9139,12 @@ dependencies = [ [[package]] name = "tokio-tun" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d79912ba514490b1f5a574b585e19082bd2a6b238970c87c57a66bd77206b5" +checksum = "68f5381752d5832fc811f89d54fc334951aa435022f494190ba7151661f206df" dependencies = [ "libc", - "nix 0.28.0", + "nix 0.29.0", "thiserror", "tokio", ] @@ -8596,7 +9160,7 @@ dependencies = [ "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.20.1", "webpki-roots 0.25.4", ] @@ -8681,7 +9245,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.13", ] [[package]] @@ -8699,7 +9263,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -8789,7 +9353,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8947,7 +9511,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "termcolor", ] @@ -8974,7 +9538,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8998,6 +9562,27 @@ dependencies = [ "webpki-roots 0.24.0", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -9069,6 +9654,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -9077,9 +9668,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -9138,12 +9729,12 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.0", "percent-encoding", "serde", ] @@ -9161,10 +9752,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" @@ -9188,7 +9791,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9209,17 +9812,11 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" - -[[package]] -name = "uuid" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.15", + "getrandom", "serde", "wasm-bindgen", ] @@ -9282,12 +9879,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9315,7 +9906,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -9349,7 +9940,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9382,7 +9973,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9450,13 +10041,12 @@ dependencies = [ name = "wasm-utils" version = "0.1.0" dependencies = [ - "console_error_panic_hook", "futures", - "getrandom 0.2.15", + "getrandom", "gloo-net", "gloo-utils 0.2.0", "js-sys", - "tungstenite", + "tungstenite 0.20.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -9470,7 +10060,7 @@ checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-utils", "slab", "wasm-bindgen", @@ -9522,9 +10112,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" dependencies = [ "rustls-pki-types", ] @@ -9804,9 +10394,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -9851,6 +10441,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -9892,6 +10494,30 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.34" @@ -9909,7 +10535,28 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", ] [[package]] @@ -9929,7 +10576,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", ] [[package]] @@ -9951,20 +10620,21 @@ dependencies = [ "anyhow", "async-trait", "bs58 0.5.1", - "getrandom 0.2.15", + "getrandom", "js-sys", "nym-bin-common", "nym-coconut", + "nym-compact-ecash", "nym-credentials", "nym-crypto", "nym-http-api-client", - "rand 0.7.3", + "rand 0.8.5", "reqwest 0.12.4", "serde", "thiserror", "tokio", "tsify", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-utils", "wasmtimer", diff --git a/Cargo.toml b/Cargo.toml index d4e4381d82..d520a16185 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", + "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/group-contract", @@ -47,6 +48,8 @@ members = [ "common/credentials-interface", "common/crypto", "common/dkg", + "common/ecash-double-spending", + "common/ecash-time", "common/execute", "common/exit-policy", "common/http-api-client", @@ -59,6 +62,7 @@ members = [ "common/node-tester-utils", "common/nonexhaustive-delayqueue", "common/nymcoconut", + "common/nym_offline_compact_ecash", "common/nym-id", "common/nym-metrics", "common/nymsphinx", @@ -74,6 +78,7 @@ members = [ "common/nymsphinx/types", "common/nyxd-scraper", "common/pemstore", + "common/serde-helpers", "common/socks5-client-core", "common/socks5/proxy-helpers", "common/socks5/requests", @@ -120,6 +125,8 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", + "tools/internal/testnet-manager", + "tools/internal/testnet-manager/dkg-bypass-contract", ] default-members = [ @@ -139,6 +146,7 @@ exclude = [ "explorer", "contracts", "nym-wallet", + "nym-vpn/ui/src-tauri", "cpu-cycles", "sdk/ffi/cpp", ] @@ -163,8 +171,13 @@ axum-extra = "0.9.3" base64 = "0.21.4" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } + +# can we unify those? +bit-vec = "0.7.0" bitvec = "1.0.0" + blake3 = "1.3.1" +bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.5.0" @@ -216,11 +229,11 @@ httpcodec = "0.2.3" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = "1.3.1" -indexed_db_futures = "0.3.0" inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.16" isocountry = "0.3.2" +itertools = "0.13.0" k256 = "0.13" lazy_static = "1.4.0" ledger-transport = "0.10.0" @@ -305,7 +318,8 @@ prometheus = { version = "0.13.0" } # coconut/DKG related # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork # as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "feature/gt-serialization-0.8.0" } +# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125 +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } group = { version = "0.13.0", default-features = false } ff = { version = "0.13.0", default-features = false } @@ -328,16 +342,22 @@ cw-controllers = { version = "=1.1.0" } # cosmrs-related bip32 = { version = "0.5.1", default-features = false } -# temporarily using a fork again (yay.) because we need staking and slashing support -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } -#cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } # unfortuntely we need a fork by yours truly to get the staking support -tendermint = "0.34" # same version as used by cosmrs -tendermint-rpc = "0.34" # same version as used by cosmrs +# temporarily using a fork again (yay.) because we need staking and slashing support (which are already on main but not released) +# plus response message parsing (which is, as of the time of writing this message, waiting to get merged) +#cosmrs = { path = "../cosmos-rust-fork/cosmos-rust/cosmrs" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } # unfortuntely we need a fork by yours truly to get the staking support +tendermint = "0.37.0" # same version as used by cosmrs +tendermint-rpc = "0.37.0" # same version as used by cosmrs prost = { version = "0.12", default-features = false } # wasm-related dependencies gloo-utils = "0.2.0" gloo-net = "0.5.0" + +# use a separate branch due to feature unification failures +# this is blocked until the upstream removes outdates `wasm_bindgen` feature usage +# indexed_db_futures = "0.4.1" +indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", branch = "update-uuid" } js-sys = "0.3.69" serde-wasm-bindgen = "0.6.5" tsify = "0.4.5" @@ -345,7 +365,6 @@ wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.39" wasmtimer = "0.2.0" web-sys = "0.3.69" -itertools = "0.12.0" # Profile settings for individual crates diff --git a/Makefile b/Makefile index ca56edb30b..f5c323db7f 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ clippy: sdk-wasm-lint # Build contracts ready for deploy # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract +CONTRACTS=vesting_contract mixnet_contract nym_ecash CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index c8f3bb0c0d..31aa4ae8ec 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod import_credential; pub(crate) mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliNativeClient; @@ -84,6 +85,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -116,6 +120,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/native/src/commands/show_ticketbooks.rs b/clients/native/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..2518a50ff2 --- /dev/null +++ b/clients/native/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 59ff639e1c..34f0738a12 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -30,6 +30,7 @@ mod import_credential; pub mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliSocks5Client; @@ -88,6 +89,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -123,6 +127,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/socks5/src/commands/show_ticketbooks.rs b/clients/socks5/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..41a37ef6eb --- /dev/null +++ b/clients/socks5/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index b122849639..e56476afa6 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -14,13 +14,14 @@ thiserror = { workspace = true } url = { workspace = true } zeroize = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-ecash-time = { path = "../ecash-time" } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] } nym-network-defaults = { path = "../network-defaults" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.nym-validator-client] path = "../client-libs/validator-client" diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index 4f2431b45b..fadb2581bf 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -1,87 +1,117 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StorableIssuedCredential; +use crate::utils::{get_coin_index_signatures, get_expiration_date_signatures}; +use log::info; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential}; -use nym_credentials::coconut::utils::obtain_aggregate_signature; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient; +use nym_credentials::ecash::bandwidth::IssuanceTicketBook; +use nym_credentials::ecash::utils::obtain_aggregate_wallet; +use nym_credentials::IssuedTicketBook; +use nym_crypto::asymmetric::identity; +use nym_ecash_time::{ecash_default_expiration_date, Date}; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use nym_validator_client::nyxd::Coin; +use nym_validator_client::nyxd::contract_traits::EcashSigningClient; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; -use state::State; -use zeroize::Zeroizing; -pub mod state; - -pub async fn deposit(client: &C, amount: Coin) -> Result +pub async fn make_deposit( + client: &C, + client_id: &[u8], + expiration: Option, +) -> Result where - C: CoconutBandwidthSigningClient + Sync, + C: EcashSigningClient + Sync, { let mut rng = OsRng; let signing_key = identity::PrivateKey::new(&mut rng); - let encryption_key = encryption::PrivateKey::new(&mut rng); + let expiration = expiration.unwrap_or_else(ecash_default_expiration_date); - let tx_hash = client - .deposit( - amount.clone(), - CredentialType::Voucher.to_string(), - signing_key.public_key().to_base58_string(), - encryption_key.public_key().to_base58_string(), - None, - ) - .await? - .transaction_hash; + let result = client + .make_ticketbook_deposit(signing_key.public_key().to_base58_string(), None) + .await?; - let voucher = - IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key); + let deposit_id = result.parse_singleton_u32_contract_data()?; - let state = State { voucher }; + info!("our ticketbook deposit has been stored under id {deposit_id}"); - Ok(state) + Ok(IssuanceTicketBook::new_with_expiration( + deposit_id, + client_id, + signing_key, + expiration, + )) } -pub async fn get_bandwidth_voucher( - state: &State, +pub async fn query_and_persist_required_global_signatures( + storage: &S, + epoch_id: EpochId, + expiration_date: Date, + apis: Vec, +) -> Result<(), BandwidthControllerError> +where + S: Storage, + ::StorageError: Send + Sync + 'static, +{ + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures(storage, epoch_id, expiration_date, apis.clone()).await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + Ok(()) +} + +pub async fn get_ticket_book( + issuance_data: &IssuanceTicketBook, client: &C, storage: &St, -) -> Result<(), BandwidthControllerError> + apis: Option>, +) -> Result where C: DkgQueryClient + Send + Sync, St: Storage, ::StorageError: Send + Sync + 'static, { - // temporary - assert!(state.voucher.typ().is_voucher()); - let epoch_id = client.get_current_epoch().await?.epoch_id; let threshold = client .get_current_epoch_threshold() .await? .ok_or(BandwidthControllerError::NoThreshold)?; - let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?; - - let signature = - obtain_aggregate_signature(&state.voucher, &coconut_api_clients, threshold).await?; - let issued = state.voucher.to_issued_credential(signature, epoch_id); - - // make sure the data gets zeroized after persisting it - let credential_data = Zeroizing::new(issued.pack_v1()); - let storable = StorableIssuedCredential { - serialization_revision: issued.current_serialization_revision(), - credential_data: credential_data.as_ref(), - credential_type: issued.typ().to_string(), - epoch_id: epoch_id - .try_into() - .expect("our epoch is has run over u32::MAX!"), + let apis = match apis { + Some(apis) => apis, + None => all_ecash_api_clients(client, epoch_id).await?, }; + log::info!("Querying wallet signatures"); + let wallet = obtain_aggregate_wallet(issuance_data, &apis, threshold).await?; + info!("managed to obtain sufficient number of partial signatures!"); + + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures( + storage, + epoch_id, + issuance_data.expiration_date(), + apis.clone(), + ) + .await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + + let issued = issuance_data.to_issued_ticketbook(wallet, epoch_id); + + info!("persisting the ticketbook into the storage..."); storage - .insert_issued_credential(storable) + .insert_issued_ticketbook(&issued) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?; + Ok(issued) } diff --git a/common/bandwidth-controller/src/acquire/state.rs b/common/bandwidth-controller/src/acquire/state.rs deleted file mode 100644 index 68945289b7..0000000000 --- a/common/bandwidth-controller/src/acquire/state.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; - -pub struct State { - pub voucher: IssuanceBandwidthCredential, -} - -impl State { - pub fn new(voucher: IssuanceBandwidthCredential) -> Self { - State { voucher } - } -} diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 44832aaab4..01619b53ed 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_coconut::CoconutError; use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialsError; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::error::ValidatorClientError; use thiserror::Error; @@ -16,7 +16,7 @@ pub enum BandwidthControllerError { Nyxd(#[from] nym_validator_client::nyxd::error::NyxdError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), @@ -28,8 +28,8 @@ pub enum BandwidthControllerError { #[error(transparent)] StorageError(#[from] StorageError), - #[error("Coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ecash error - {0}")] + EcashError(#[from] CompactEcashError), #[error("Validator client error - {0}")] ValidatorError(#[from] ValidatorClientError), @@ -51,4 +51,15 @@ pub enum BandwidthControllerError { #[error("can't handle recovering storage with revision {stored}. {expected} was expected")] UnsupportedCredentialStorageRevision { stored: u8, expected: u8 }, + + #[error("did not receive a valid response for aggregated data ({typ}) from ANY nym-api")] + ExhaustedApiQueries { typ: String }, +} + +impl BandwidthControllerError { + pub fn credential_storage_error( + source: impl std::error::Error + Send + Sync + 'static, + ) -> Self { + BandwidthControllerError::CredentialStorageError(Box::new(source)) + } } diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 91201b9a92..4485009b68 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -1,16 +1,24 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + use crate::error::BandwidthControllerError; -use crate::utils::stored_credential_to_issued_bandwidth; -use log::{debug, error, warn}; +use crate::utils::{ + get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures, + ApiClientsWrapper, +}; +use log::error; +use nym_credential_storage::models::RetrievedTicketbook; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_credentials::IssuedBandwidthCredential; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, NymPayInfo, VerificationKeyAuth, +}; +use nym_ecash_time::Date; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; @@ -35,13 +43,20 @@ pub struct PreparedCredential { /// could use correct verification key for validation. pub epoch_id: EpochId, - /// The database id of the stored credential. - pub credential_id: i64, + /// Auxiliary metadata associated with the withdrawn credential + pub metadata: PreparedCredentialMetadata, } -pub struct RetrievedCredential { - pub credential: IssuedBandwidthCredential, - pub credential_id: i64, +#[derive(Copy, Clone)] +pub struct PreparedCredentialMetadata { + /// The database id of the stored credential. + pub ticketbook_id: i64, + + /// The number of tickets withdrawn in this credential + pub tickets_withdrawn: u32, + + /// The amount of tickets used INCLUDING those tickets that JUST got withdrawn + pub used_tickets: u32, } impl BandwidthController { @@ -50,111 +65,155 @@ impl BandwidthController { } /// Tries to retrieve one of the stored, unused credentials that hasn't yet expired. - /// It marks any retrieved intermediate credentials as expired. - pub async fn get_next_usable_credential( + pub async fn get_next_usable_ticketbook( &self, - gateway_id: &str, - ) -> Result + tickets: u32, + ) -> Result where ::StorageError: Send + Sync + 'static, { - loop { - let Some(maybe_next) = self - .storage - .get_next_unspent_credential(gateway_id) - .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))? - else { - return Err(BandwidthControllerError::NoCredentialsAvailable); - }; - let id = maybe_next.id; + let Some(ticketbook) = self + .storage + .get_next_unspent_usable_ticketbook(tickets) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + else { + return Err(BandwidthControllerError::NoCredentialsAvailable); + }; - // try to deserialize it - let valid_credential = match stored_credential_to_issued_bandwidth(maybe_next) { - // check if it has already expired - Ok(credential) => match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(_) => { - debug!("credential {id} is a bandwidth voucher"); - credential - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("credential {id} is a free pass"); - if freepass_info.expired() { - warn!("the free pass (id: {id}) has already expired! The expiration was set to {}", freepass_info.expiry_date()); - self.storage.mark_expired(id).await.map_err(|err| { - BandwidthControllerError::CredentialStorageError(Box::new(err)) - })?; - continue; - } - credential - } - }, - Err(err) => { - error!("failed to deserialize credential with id {id}: {err}. it may need to be manually removed from the storage"); - return Err(err); - } - }; - return Ok(RetrievedCredential { - credential: valid_credential, - credential_id: id, - }); - } + Ok(ticketbook) } - pub fn storage(&self) -> &St { - &self.storage + pub async fn attempt_revert_ticket_usage( + &self, + info: PreparedCredentialMetadata, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .attempt_revert_ticketbook_withdrawal( + info.ticketbook_id, + info.used_tickets, + info.tickets_withdrawn, + ) + .await + .map_err(BandwidthControllerError::credential_storage_error) } async fn get_aggregate_verification_key( &self, epoch_id: EpochId, - ) -> Result + apis: &mut ApiClientsWrapper, + ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?; - Ok(obtain_aggregate_verification_key(&coconut_api_clients)?) + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await } - pub async fn prepare_bandwidth_credential( + async fn get_coin_index_signatures( &self, - gateway_id: &str, + epoch_id: EpochId, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await + } + + async fn get_expiration_date_signatures( + &self, + epoch_id: EpochId, + expiration_date: Date, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await + } + + async fn prepare_ecash_ticket_inner( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, + mut retrieved_ticketbook: RetrievedTicketbook, + ) -> Result + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); + let expiration_date = retrieved_ticketbook.ticketbook.expiration_date(); + let mut api_clients = Default::default(); + + let verification_key = self + .get_aggregate_verification_key(epoch_id, &mut api_clients) + .await?; + let expiration_signatures = self + .get_expiration_date_signatures(epoch_id, expiration_date, &mut api_clients) + .await?; + let coin_indices_signatures = self + .get_coin_index_signatures(epoch_id, &mut api_clients) + .await?; + + let pay_info = NymPayInfo::generate(provider_pk); + + let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending( + &verification_key, + pay_info.into(), + &coin_indices_signatures, + &expiration_signatures, + tickets_to_spend as u64, + )?; + Ok(spend_request) + } + + pub async fn prepare_ecash_ticket( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_credential = self.get_next_usable_credential(gateway_id).await?; + let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?; - let epoch_id = retrieved_credential.credential.epoch_id(); - let credential_id = retrieved_credential.credential_id; + let ticketbook_id = retrieved_ticketbook.ticketbook_id; + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); - let verification_key = self.get_aggregate_verification_key(epoch_id).await?; + let used_tickets = + retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend; + let metadata = PreparedCredentialMetadata { + ticketbook_id, + tickets_withdrawn: tickets_to_spend, + used_tickets, + }; - let spend_request = retrieved_credential - .credential - .prepare_for_spending(&verification_key)?; - - Ok(PreparedCredential { - data: spend_request, - epoch_id, - credential_id, - }) - } - - pub async fn consume_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), BandwidthControllerError> - where - ::StorageError: Send + Sync + 'static, - { - self.storage - .consume_coconut_credential(id, gateway_id) + match self + .prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + { + Ok(data) => Ok(PreparedCredential { + data, + epoch_id, + metadata, + }), + Err(err) => { + error!("failed to prepare credential spending request. attempting to revert withdrawal..."); + self.attempt_revert_ticket_usage(metadata).await?; + Err(err) + } + } } } diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 89891dba48..0d2427b2f8 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -2,21 +2,180 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StoredIssuedCredential; -use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; -use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential; +use log::warn; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, +}; +use nym_ecash_time::Date; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use nym_validator_client::EcashApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::fmt::Display; +use std::future::Future; -pub fn stored_credential_to_issued_bandwidth( - cred: StoredIssuedCredential, -) -> Result { - if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION { - return Err( - BandwidthControllerError::UnsupportedCredentialStorageRevision { - stored: cred.serialization_revision, - expected: CURRENT_SERIALIZATION_REVISION, - }, - ); +// it really doesn't need the RwLock because it's never moved across tasks, +// but we need all the Send/Sync action +#[derive(Default)] +pub(crate) struct ApiClientsWrapper(Option>); + +impl ApiClientsWrapper { + pub(crate) async fn get_or_init( + &mut self, + epoch_id: EpochId, + dkg_client: &C, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + { + if let Some(cached) = &self.0 { + return Ok(cached.clone()); + } + + let clients = all_ecash_api_clients(dkg_client, epoch_id).await?; + + // technically we don't have to be cloning all the clients here, but it's way simpler than + // dealing with locking and whatnot given the performance penalty is negligible + self.0 = Some(clients.clone()); + Ok(clients) } - - Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?) +} + +pub(crate) async fn query_random_apis_until_success( + mut apis: Vec, + f: F, + typ: impl Into, +) -> Result +where + F: Fn(EcashApiClient) -> U, + U: Future>, + E: Display, +{ + // try apis in pseudorandom way to remove any bias towards the first registered dealer + apis.shuffle(&mut thread_rng()); + + for api in apis { + let disp = api.to_string(); + match f(api).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!("failed to obtain valid response from API {disp}: {err}") + } + } + } + Err(BandwidthControllerError::ExhaustedApiQueries { typ: typ.into() }) +} + +pub(crate) async fn get_aggregate_verification_key( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_master_verification_key(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let master_vk = query_random_apis_until_success( + ecash_apis, + |api| async move { api.api_client.master_verification_key(Some(epoch_id)).await }, + format!("aggregated verification key for epoch {epoch_id}"), + ) + .await? + .key; + + // store the retrieved key + storage + .insert_master_verification_key(epoch_id, &master_vk) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(master_vk) +} + +pub(crate) async fn get_coin_index_signatures( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_coin_index_signatures(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let index_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_coin_indices_signatures(Some(epoch_id)) + .await + }, + format!("aggregated coin index signatures for epoch {epoch_id}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_coin_index_signatures(epoch_id, &index_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(index_sigs) +} + +pub(crate) async fn get_expiration_date_signatures( + storage: &St, + epoch_id: EpochId, + expiration_date: Date, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_expiration_date_signatures(expiration_date) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let expiration_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_expiration_date_signatures(Some(expiration_date)) + .await + }, + format!("aggregated coin index signatures for date {expiration_date}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_expiration_date_signatures(epoch_id, expiration_date, &expiration_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(expiration_sigs) } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 1d7c34d51c..92250213c8 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -14,6 +14,7 @@ base64 = { workspace = true } bs58 = { workspace = true } cfg-if = { workspace = true } clap = { workspace = true, optional = true } +comfy-table = { version = "7.1.1", optional = true } futures = { workspace = true } humantime-serde = { workspace = true } log = { workspace = true } @@ -50,6 +51,7 @@ nym-network-defaults = { path = "../network-defaults" } nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] } nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } +nym-ecash-time = { path = "../ecash-time" } ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] @@ -112,7 +114,7 @@ tempfile = { workspace = true } [features] default = [] -cli = ["clap"] +cli = ["clap", "comfy-table"] fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] diff --git a/common/client-core/src/cli_helpers/client_show_ticketbooks.rs b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs new file mode 100644 index 0000000000..9c6e2e71ef --- /dev/null +++ b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs @@ -0,0 +1,127 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::error::ClientCoreError; +use nym_credential_storage::models::BasicTicketbookInformation; +use nym_credential_storage::storage::Storage; +use nym_ecash_time::ecash_today; +use nym_network_defaults::TICKET_BANDWIDTH_VALUE; +use serde::{Deserialize, Serialize}; +use time::Date; + +#[derive(Serialize, Deserialize)] +pub struct AvailableTicketbook { + pub id: i64, + pub expiration: Date, + pub issued_tickets: u32, + pub claimed_tickets: u32, + pub ticket_size: u64, +} + +impl AvailableTicketbook { + #[cfg(feature = "cli")] + fn table_row(&self) -> comfy_table::Row { + let ecash_today = ecash_today().date(); + + let issued = self.issued_tickets; + let si_issued = si_scale::helpers::bibytes2((issued as u64 * self.ticket_size) as f64); + + let claimed = self.claimed_tickets; + let si_claimed = si_scale::helpers::bibytes2((claimed as u64 * self.ticket_size) as f64); + + let remaining = issued - claimed; + let si_remaining = + si_scale::helpers::bibytes2((remaining as u64 * self.ticket_size) as f64); + let si_size = si_scale::helpers::bibytes2(self.ticket_size as f64); + + let expiration = if self.expiration <= ecash_today { + comfy_table::Cell::new(format!("EXPIRED ON {}", self.expiration)) + .fg(comfy_table::Color::Red) + .add_attribute(comfy_table::Attribute::Bold) + } else { + comfy_table::Cell::new(self.expiration.to_string()) + }; + + vec![ + comfy_table::Cell::new(self.id.to_string()), + expiration, + comfy_table::Cell::new(format!("{issued} ({si_issued})")), + comfy_table::Cell::new(format!("{claimed} ({si_claimed})")), + comfy_table::Cell::new(format!("{remaining} ({si_remaining})")), + comfy_table::Cell::new(si_size), + ] + .into() + } +} + +impl From for AvailableTicketbook { + fn from(value: BasicTicketbookInformation) -> Self { + AvailableTicketbook { + id: value.id, + expiration: value.expiration_date, + issued_tickets: value.total_tickets, + claimed_tickets: value.used_tickets, + ticket_size: TICKET_BANDWIDTH_VALUE, + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct AvailableTicketbooks(Vec); + +#[cfg(feature = "cli")] +impl std::fmt::Display for AvailableTicketbooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut table = comfy_table::Table::new(); + table.set_header(vec![ + "id", + "expiration", + "issued tickets (bandwidth)", + "claimed tickets (bandwidth)", + "remaining tickets (bandwidth)", + "ticket size", + ]); + + for ticketbook in &self.0 { + table.add_row(ticketbook.table_row()); + } + + writeln!(f, "{table}")?; + Ok(()) + } +} + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonShowTicketbooksArgs { + /// Id of client that is going to display the ticketbook information + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, +} + +pub async fn show_ticketbooks(args: A) -> Result +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let credentials_store = + nym_credential_storage::initialise_persistent_storage(&paths.credentials_database).await; + let ticketbooks = credentials_store + .get_ticketbooks_info() + .await + .map_err(|err| ClientCoreError::CredentialStoreError { + source: Box::new(err), + })?; + + Ok(AvailableTicketbooks( + ticketbooks.into_iter().map(Into::into).collect(), + )) +} diff --git a/common/client-core/src/cli_helpers/mod.rs b/common/client-core/src/cli_helpers/mod.rs index 5ab51b8b4c..a3660a60e6 100644 --- a/common/client-core/src/cli_helpers/mod.rs +++ b/common/client-core/src/cli_helpers/mod.rs @@ -6,6 +6,7 @@ pub mod client_import_credential; pub mod client_init; pub mod client_list_gateways; pub mod client_run; +pub mod client_show_ticketbooks; pub mod client_switch_gateway; pub mod traits; mod types; diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index b6197fb490..0862911d93 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -3,10 +3,12 @@ use async_trait::async_trait; use log::{debug, error}; +use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::forwarding::packet::MixPacket; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use thiserror::Error; @@ -111,8 +113,9 @@ impl RemoteGateway { impl GatewayTransceiver for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { fn gateway_identity(&self) -> identity::PublicKey { self.gateway_client.gateway_identity() @@ -126,8 +129,9 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GatewaySender for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { self.gateway_client diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index bdfecfda5e..bcaca3763c 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -63,6 +63,11 @@ pub enum ClientCoreError { source: Box, }, + #[error("experienced a failure with our credentials storage: {source}")] + CredentialStoreError { + source: Box, + }, + #[error("the gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 91c320bb46..4ba1341bb9 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -23,12 +23,12 @@ use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; -use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; +use nym_network_defaults::REMAINING_BANDWIDTH_THRESHOLD; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; - +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; use tungstenite::protocol::Message; @@ -83,7 +83,7 @@ impl GatewayConfig { pub struct GatewayClient { authenticated: bool, disabled_credentials_mode: bool, - bandwidth_remaining: i64, + bandwidth_remaining: Arc, gateway_address: String, gateway_identity: identity::PublicKey, local_identity: Arc, @@ -122,7 +122,7 @@ impl GatewayClient { GatewayClient { authenticated: false, disabled_credentials_mode: true, - bandwidth_remaining: 0, + bandwidth_remaining: Arc::new(AtomicI64::new(0)), gateway_address: config.gateway_listener, gateway_identity: config.gateway_identity, local_identity, @@ -182,7 +182,7 @@ impl GatewayClient { } pub fn remaining_bandwidth(&self) -> i64 { - self.bandwidth_remaining + self.bandwidth_remaining.load(Ordering::Acquire) } #[cfg(not(target_arch = "wasm32"))] @@ -259,7 +259,7 @@ impl GatewayClient { self.authenticated = false; for i in 1..self.reconnection_attempts { - info!("attempt {}...", i); + info!("reconnection attempt {}...", i); if self.try_reconnect().await.is_ok() { info!("managed to reconnect!"); return Ok(()); @@ -269,7 +269,7 @@ impl GatewayClient { } // final attempt (done separately to be able to return a proper error) - info!("attempt {}", self.reconnection_attempts); + info!("reconnection attempt {}", self.reconnection_attempts); match self.try_reconnect().await { Ok(_) => { info!("managed to reconnect!"); @@ -537,7 +537,8 @@ impl GatewayClient { } => { self.check_gateway_protocol(protocol_version)?; self.authenticated = status; - self.bandwidth_remaining = bandwidth_remaining; + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Release); self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); self.task_client.send_status_msg(Box::new( @@ -576,44 +577,54 @@ impl GatewayClient { } } - async fn claim_coconut_bandwidth( + async fn claim_ecash_bandwidth( &mut self, credential: CredentialSpendingData, ) -> Result<(), GatewayClientError> { let mut rng = OsRng; let iv = IV::new_random(&mut rng); - let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2( + let msg = ClientControlRequest::new_enc_ecash_credential( credential, self.shared_key.as_ref().unwrap(), iv, ) .into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => Err(GatewayClientError::UnexpectedResponse), }?; + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Relaxed); Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => Err(GatewayClientError::UnexpectedResponse), }?; - + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Release); Ok(()) } + fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + self.bandwidth_controller.as_ref().unwrap() + } + pub async fn claim_bandwidth(&mut self) -> Result<(), GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, ::StorageError: Send + Sync + 'static, { + // TODO: make it configurable + const TICKETS_TO_SPEND: u32 = 1; + if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } @@ -641,49 +652,42 @@ impl GatewayClient { negotiated_protocol: Some(gateway_protocol), }); } - - let gateway_id = self.gateway_identity().to_base58_string(); - let prepared_credential = self - .bandwidth_controller - .as_ref() - .unwrap() - .prepare_bandwidth_credential(&gateway_id) + .unchecked_bandwidth_controller() + .prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND) .await?; - self.claim_coconut_bandwidth(prepared_credential.data) - .await?; - self.bandwidth_controller - .as_ref() - .unwrap() - .consume_credential(prepared_credential.credential_id, &gateway_id) - .await?; - - Ok(()) - } - - fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { - packets - .iter() - .map(|packet| packet.packet().len()) - .sum::() as i64 + match self.claim_ecash_bandwidth(prepared_credential.data).await { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to claim ecash bandwidth with the gateway... attempting to revert storage withdrawal"); + self.unchecked_bandwidth_controller() + .attempt_revert_ticket_usage(prepared_credential.metadata) + .await?; + Err(err) + } + } } pub async fn batch_send_mix_packets( &mut self, packets: Vec, - ) -> Result<(), GatewayClientError> { + ) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { debug!("Sending {} mix packets", packets.len()); if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - self.estimate_required_bandwidth(&packets), - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -742,19 +746,20 @@ impl GatewayClient { } // TODO: possibly make responses optional - pub async fn send_mix_packet( - &mut self, - mix_packet: MixPacket, - ) -> Result<(), GatewayClientError> { + pub async fn send_mix_packet(&mut self, mix_packet: MixPacket) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if (mix_packet.packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - mix_packet.packet().len() as i64, - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -808,6 +813,7 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), + self.bandwidth_remaining.clone(), self.task_client.clone(), ) } @@ -848,10 +854,9 @@ impl GatewayClient { self.establish_connection().await?; } let shared_key = self.perform_initial_authentication().await?; - - if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { - info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \ - Stop the process now if you don't want that to happen.", TOKENS_TO_BURN); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."); self.claim_bandwidth().await?; } @@ -888,7 +893,7 @@ impl GatewayClient { GatewayClient { authenticated: false, disabled_credentials_mode: true, - bandwidth_remaining: 0, + bandwidth_remaining: Arc::new(AtomicI64::new(0)), gateway_address: gateway_listener.to_string(), gateway_identity, local_identity, diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 386ad5b147..128f6919e6 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -79,6 +79,7 @@ impl PartiallyDelegated { fn recover_received_plaintexts( ws_msgs: Vec, shared_key: &SharedKeys, + bandwidth_remaining: Arc, ) -> Result>, GatewayClientError> { let mut plaintexts = Vec::with_capacity(ws_msgs.len()); for ws_msg in ws_msgs { @@ -104,7 +105,11 @@ impl PartiallyDelegated { { ServerResponse::Send { remaining_bandwidth, - } => maybe_log_bandwidth(remaining_bandwidth), + } => { + maybe_log_bandwidth(remaining_bandwidth); + bandwidth_remaining + .store(remaining_bandwidth, std::sync::atomic::Ordering::Release) + } ServerResponse::Error { message } => { error!("gateway failure: {message}"); return Err(GatewayClientError::GatewayError(message)); @@ -129,8 +134,10 @@ impl PartiallyDelegated { ws_msgs: Vec, packet_router: &PacketRouter, shared_key: &SharedKeys, + bandwidth_remaining: Arc, ) -> Result<(), GatewayClientError> { - let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?; + let plaintexts = + Self::recover_received_plaintexts(ws_msgs, shared_key, bandwidth_remaining)?; packet_router.route_received(plaintexts) } @@ -138,6 +145,7 @@ impl PartiallyDelegated { conn: WsConn, mut packet_router: PacketRouter, shared_key: Arc, + bandwidth_remaining: Arc, mut shutdown: TaskClient, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and @@ -169,7 +177,7 @@ impl PartiallyDelegated { Ok(msgs) => msgs }; - if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) { + if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref(), bandwidth_remaining.clone()) { log::error!("Route socket messages failed: {err}"); break Err(err) } diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 32f371eb23..a9a348a28e 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -17,6 +17,7 @@ nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } serde = { workspace = true, features = ["derive"] } @@ -26,9 +27,10 @@ thiserror = { workspace = true } log = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } +time = { workspace = true, features = ["formatting"] } futures = { workspace = true } -nym-coconut = { path = "../../nymcoconut" } +nym-compact-ecash = { path = "../../nym_offline_compact_ecash" } nym-network-defaults = { path = "../../network-defaults" } nym-api-requests = { path = "../../../nym-api/nym-api-requests" } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b7b1e62c23..32cfd9ae2f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -8,10 +8,14 @@ use crate::{ nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, ValidatorClientError, }; -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody, - VerifyCredentialResponse, +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::models::{ @@ -19,8 +23,10 @@ use nym_api_requests::models::{ RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::nym_nodes::SkimmedNode; +use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_network_defaults::NymNetworkDetails; +use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; @@ -29,7 +35,7 @@ pub use nym_mixnet_contract_common::{ }; // re-export the type to not break existing imports -pub use crate::coconut::CoconutApiClient; +pub use crate::coconut::EcashApiClient; #[cfg(feature = "http-client")] use crate::rpc::http_client; @@ -375,24 +381,73 @@ impl NymApiClient { Ok(self.nym_api.blind_sign(request_body).await?) } - pub async fn verify_bandwidth_credential( + pub async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { + Ok(self.nym_api.verify_ecash_ticket(request_body).await?) + } + + pub async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { Ok(self .nym_api - .verify_bandwidth_credential(request_body) + .batch_redeem_ecash_tickets(request_body) .await?) } - pub async fn free_pass_nonce(&self) -> Result { - Ok(self.nym_api.free_pass_nonce().await?) + pub async fn spent_credentials_filter( + &self, + ) -> Result { + Ok(self.nym_api.double_spending_filter_v1().await?) } - pub async fn issue_free_pass_credential( + pub async fn partial_expiration_date_signatures( &self, - request: &FreePassRequest, - ) -> Result { - Ok(self.nym_api.free_pass(request).await?) + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .partial_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .partial_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .global_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .global_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + Ok(self.nym_api.master_verification_key(epoch_id).await?) } } diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 66f5d0e05a..09ce560b1c 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -4,26 +4,40 @@ use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; use crate::nyxd::error::NyxdError; use crate::NymApiClient; -use nym_coconut::{Base58, CoconutError, VerificationKey}; use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::error::CompactEcashError; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use std::fmt::{Display, Formatter}; use thiserror::Error; use url::Url; // TODO: it really doesn't feel like this should live in this crate. #[derive(Clone)] -pub struct CoconutApiClient { +pub struct EcashApiClient { pub api_client: NymApiClient, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, pub cosmos_address: cosmrs::AccountId, } +impl Display for EcashApiClient { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[id: {}] {} @ {}", + self.node_id, + self.cosmos_address, + self.api_client.api_url() + ) + } +} + // TODO: this should be using the coconut error // (which is in different crate; perhaps this client should be moved there?) #[derive(Debug, Error)] -pub enum CoconutApiError { +pub enum EcashApiError { // TODO: ask @BN whether this is a correct error message #[error("the provided key share hasn't been verified")] UnverifiedShare, @@ -43,7 +57,7 @@ pub enum CoconutApiError { #[error("the provided verification key is malformed: {source}")] MalformedVerificationKey { #[from] - source: CoconutError, + source: CompactEcashError, }, #[error("the provided account address is malformed: {source}")] @@ -53,29 +67,29 @@ pub enum CoconutApiError { }, } -impl TryFrom for CoconutApiClient { - type Error = CoconutApiError; +impl TryFrom for EcashApiClient { + type Error = EcashApiError; fn try_from(share: ContractVKShare) -> Result { if !share.verified { - return Err(CoconutApiError::UnverifiedShare); + return Err(EcashApiError::UnverifiedShare); } let url_address = Url::parse(&share.announce_address)?; - Ok(CoconutApiClient { + Ok(EcashApiClient { api_client: NymApiClient::new(url_address), - verification_key: VerificationKey::try_from_bs58(&share.share)?, + verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, node_id: share.node_index, cosmos_address: share.owner.as_str().parse()?, }) } } -pub async fn all_coconut_api_clients( +pub async fn all_ecash_api_clients( client: &C, epoch_id: EpochId, -) -> Result, CoconutApiError> +) -> Result, EcashApiError> where C: DkgQueryClient + Sync + Send, { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index d439a86848..11bdb3d745 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -7,7 +7,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ValidatorClientError { - #[error("nym api request failed - {source}")] + #[error("nym api request failed: {source}")] NymAPIError { #[from] source: nym_api::error::NymAPIError, @@ -19,7 +19,7 @@ pub enum ValidatorClientError { #[error("One of the provided URLs was malformed - {0}")] MalformedUrlProvided(#[from] url::ParseError), - #[error("nyxd request failed - {0}")] + #[error("nyxd request failed: {0}")] NyxdError(#[from] crate::nyxd::error::NyxdError), #[error("No validator API url has been provided")] diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 5af96ffe23..725b86450b 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -15,7 +15,7 @@ pub use crate::error::ValidatorClientError; pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; pub use client::NymApiClient; -pub use client::{Client, CoconutApiClient, Config}; +pub use client::{Client, Config, EcashApiClient}; pub use nym_api_requests::*; pub use nym_http_api_client::UserAgent; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 8c2f46d1d1..b9b3d91cd2 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -2,16 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nym_api::error::NymAPIError; -use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; +use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + VerifyEcashTicketBody, +}; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_http_api_client::{ApiClient, NO_PARAMS}; +use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use time::format_description::BorrowedFormatItem; +use time::Date; + +pub mod error; +pub mod routes; + +use nym_api_requests::ecash::VerificationKeyResponse; pub use nym_api_requests::{ - coconut::{ + ecash::{ models::{ EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody, - IssuedCredentialResponse, IssuedCredentialsResponse, + IssuedCredentialResponse, IssuedCredentialsResponse, SpentCredentialsResponse, }, BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, - VerifyCredentialBody, VerifyCredentialResponse, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerifyEcashCredentialBody, }, models::{ ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, @@ -22,18 +39,12 @@ pub use nym_api_requests::{ }, }; pub use nym_coconut_dkg_common::types::EpochId; -use nym_http_api_client::{ApiClient, NO_PARAMS}; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; - -pub mod error; -pub mod routes; - -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::FreePassRequest; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; pub use nym_http_api_client::Client; +pub fn rfc_3339_date() -> Vec> { + time::format_description::parse("[year]-[month]-[day]").unwrap() +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { @@ -420,36 +431,6 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn free_pass_nonce(&self) -> Result { - self.get_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS_NONCE, - ], - NO_PARAMS, - ) - .await - } - - async fn free_pass( - &self, - request: &FreePassRequest, - ) -> Result { - self.post_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS, - ], - NO_PARAMS, - request, - ) - .await - } - async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -457,9 +438,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_BLIND_SIGN, + routes::ECASH_ROUTES, + routes::ECASH_BLIND_SIGN, ], NO_PARAMS, request_body, @@ -467,16 +447,15 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn verify_bandwidth_credential( + async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, + routes::ECASH_ROUTES, + routes::VERIFY_ECASH_TICKET, ], NO_PARAMS, request_body, @@ -484,6 +463,139 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { + self.post_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::BATCH_REDEEM_ECASH_TICKETS, + ], + NO_PARAMS, + request_body, + ) + .await + } + + async fn double_spending_filter_v1(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::DOUBLE_SPENDING_FILTER_V1, + ], + NO_PARAMS, + ) + .await + } + + async fn partial_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::ecash::MASTER_VERIFICATION_KEY, + ], + ¶ms, + ) + .await + } + async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -491,9 +603,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_EPOCH_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_EPOCH_CREDENTIALS, &dkg_epoch.to_string(), ], NO_PARAMS, @@ -508,9 +619,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIAL, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIAL, &credential_id.to_string(), ], NO_PARAMS, @@ -525,9 +635,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIALS, ], NO_PARAMS, &CredentialsRequestBody { diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index cc2a17d018..c6a50db513 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -12,16 +12,27 @@ pub const DETAILED: &str = "detailed"; pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; -pub const COCONUT_ROUTES: &str = "coconut"; -pub const BANDWIDTH: &str = "bandwidth"; +pub const DOUBLE_SPENDING_FILTER_V1: &str = "double-spending-filter-v1"; -pub const COCONUT_FREE_PASS: &str = "free-pass"; -pub const COCONUT_FREE_PASS_NONCE: &str = "free-pass-nonce"; -pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; -pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; -pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials"; -pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential"; -pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials"; +pub const ECASH_ROUTES: &str = "ecash"; + +pub use ecash::*; +pub mod ecash { + pub const ECASH_BLIND_SIGN: &str = "blind-sign"; + pub const VERIFY_ECASH_TICKET: &str = "verify-ecash-ticket"; + pub const BATCH_REDEEM_ECASH_TICKETS: &str = "batch-redeem-ecash-tickets"; + pub const PARTIAL_EXPIRATION_DATE_SIGNATURES: &str = "partial-expiration-date-signatures"; + pub const GLOBAL_EXPIRATION_DATE_SIGNATURES: &str = "aggregated-expiration-date-signatures"; + pub const PARTIAL_COIN_INDICES_SIGNATURES: &str = "partial-coin-indices-signatures"; + pub const GLOBAL_COIN_INDICES_SIGNATURES: &str = "aggregated-coin-indices-signatures"; + pub const MASTER_VERIFICATION_KEY: &str = "master-verification-key"; + pub const ECASH_EPOCH_CREDENTIALS: &str = "epoch-credentials"; + pub const ECASH_ISSUED_CREDENTIAL: &str = "issued-credential"; + pub const ECASH_ISSUED_CREDENTIALS: &str = "issued-credentials"; + + pub const EXPIRATION_DATE_PARAM: &str = "expiration_date"; + pub const EPOCH_ID_PARAM: &str = "epoch_id"; +} pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs deleted file mode 100644 index 1737edf613..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::collect_paged; -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::error::NyxdError; -use crate::nyxd::CosmWasmClient; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::msg::QueryMsg as CoconutBandwidthQueryMsg; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse, -}; -use serde::Deserialize; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthQueryClient { - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>; - - async fn get_spent_credential( - &self, - blinded_serial_number: String, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - }) - .await - } - - async fn get_all_spent_credential_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetAllSpentCredentials { - limit, - start_after, - }) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait PagedCoconutBandwidthQueryClient: CoconutBandwidthQueryClient { - async fn get_all_spent_credentials(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_spent_credential_paged, spend_credentials) - } -} - -#[async_trait] -impl PagedCoconutBandwidthQueryClient for T where T: CoconutBandwidthQueryClient {} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthQueryClient for C -where - C: CosmWasmClient + NymContractsProvider + Send + Sync, -{ - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>, - { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - self.query_contract_smart(coconut_bandwidth_contract_address, &query) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::IgnoreValue; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_query_variants_are_covered( - client: C, - msg: CoconutBandwidthQueryMsg, - ) { - match msg { - CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - } => client.get_spent_credential(blinded_serial_number).ignore(), - CoconutBandwidthQueryMsg::GetAllSpentCredentials { limit, start_after } => client - .get_all_spent_credential_paged(start_after, limit) - .ignore(), - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs deleted file mode 100644 index 2402dc078f..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::cosmwasm_client::types::ExecuteResult; -use crate::nyxd::error::NyxdError; -use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; -use crate::signing::signer::OfflineSigner; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialData; -use nym_coconut_bandwidth_contract_common::{ - deposit::DepositData, msg::ExecuteMsg as CoconutBandwidthExecuteMsg, -}; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthSigningClient { - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result; - - async fn deposit( - &self, - amount: Coin, - info: String, - verification_key: String, - encryption_key: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::DepositFunds { - data: DepositData::new(info, verification_key, encryption_key), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::Deposit".to_string(), - vec![amount], - ) - .await - } - - async fn spend_credential( - &self, - funds: Coin, - blinded_serial_number: String, - gateway_cosmos_address: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::SpendCredential { - data: SpendCredentialData::new( - funds.into(), - blinded_serial_number, - gateway_cosmos_address, - ), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::SpendCredential".to_string(), - vec![], - ) - .await - } - - async fn release_funds( - &self, - amount: Coin, - fee: Option, - ) -> Result { - self.execute_coconut_bandwidth_contract( - fee, - CoconutBandwidthExecuteMsg::ReleaseFunds { - funds: amount.into(), - }, - "CoconutBandwidth::ReleaseFunds".to_string(), - vec![], - ) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthSigningClient for C -where - C: SigningCosmWasmClient + NymContractsProvider + Sync, - NyxdError: From<::Error>, -{ - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; - - self.execute( - signer_address, - coconut_bandwidth_contract_address, - &msg, - fee, - memo, - funds, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_execute_variants_are_covered( - client: C, - msg: CoconutBandwidthExecuteMsg, - ) { - match msg { - CoconutBandwidthExecuteMsg::DepositFunds { data } => client - .deposit( - mock_coin(), - data.deposit_info().to_string(), - data.identity_key().to_string(), - data.encryption_key().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::SpendCredential { data } => client - .spend_credential( - mock_coin(), - data.blinded_serial_number().to_string(), - data.gateway_cosmos_address().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::ReleaseFunds { funds } => { - client.release_funds(funds.into(), None).ignore() - } - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index c85f85c399..4d8fd3237c 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -51,6 +51,11 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result, NyxdError> { + let request = DkgQueryMsg::GetEpochThreshold { epoch_id }; + self.query_dkg_contract(request).await + } + async fn get_registered_dealer_details( &self, address: &AccountId, @@ -256,6 +261,9 @@ mod tests { DkgQueryMsg::GetCurrentEpochThreshold {} => { client.get_current_epoch_threshold().ignore() } + DkgQueryMsg::GetEpochThreshold { epoch_id } => { + client.get_epoch_threshold(epoch_id).ignore() + } DkgQueryMsg::GetRegisteredDealer { dealer_address, epoch_id, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs new file mode 100644 index 0000000000..7c4c6c6b4e --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs @@ -0,0 +1,123 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmwasm_std::Coin; +use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg; +use serde::Deserialize; + +pub use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, PagedBlacklistedAccountResponse, +}; +pub use nym_ecash_contract_common::deposit::{ + Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashQueryClient { + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_blacklisted_account( + &self, + public_key: String, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistedAccount { public_key }) + .await + } + + async fn get_blacklist_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistPaged { start_after, limit }) + .await + } + + async fn get_required_deposit_amount(&self) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {}) + .await + } + + async fn get_deposit(&self, deposit_id: u32) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDeposit { deposit_id }) + .await + } + + async fn get_deposits_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit }) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedEcashQueryClient: EcashQueryClient { + async fn get_all_blacklisted_accounts(&self) -> Result, NyxdError> { + collect_paged!(self, get_blacklist_paged, accounts) + } + + async fn get_all_deposits(&self) -> Result, NyxdError> { + collect_paged!(self, get_deposits_paged, deposits) + } +} + +#[async_trait] +impl PagedEcashQueryClient for T where T: EcashQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + self.query_contract_smart(ecash_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_ecash_contract_common::msg::QueryMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: EcashQueryMsg, + ) { + match msg { + EcashQueryMsg::GetBlacklistedAccount { public_key } => { + client.get_blacklisted_account(public_key).ignore() + } + QueryMsg::GetBlacklistPaged { limit, start_after } => { + client.get_blacklist_paged(start_after, limit).ignore() + } + QueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(), + QueryMsg::GetDepositsPaged { limit, start_after } => { + client.get_deposits_paged(start_after, limit).ignore() + } + QueryMsg::GetRequiredDepositAmount {} => client.get_required_deposit_amount().ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs new file mode 100644 index 0000000000..98a3e2675b --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs @@ -0,0 +1,124 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_ecash_contract_common::events::TICKET_BOOK_VALUE; +use nym_ecash_contract_common::msg::ExecuteMsg as EcashExecuteMsg; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashSigningClient { + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn make_ticketbook_deposit( + &self, + public_key: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::DepositTicketBookFunds { + identity_key: public_key, + }; + let amount = Coin::new(TICKET_BOOK_VALUE, "unym"); + self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![amount]) + .await + } + + async fn request_ticket_redemption( + &self, + commitment_bs58: String, + number_of_tickets: u16, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + }; + self.execute_ecash_contract(fee, req, Default::default(), vec![]) + .await + } + + async fn propose_for_blacklist( + &self, + public_key: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::ProposeToBlacklist { public_key }; + self.execute_ecash_contract(fee, req, "Ecash::ProposeToBlacklist".to_string(), vec![]) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + let signer_address = &self.signer_addresses()?[0]; + + self.execute( + signer_address, + ecash_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_ecash_contract_common::msg::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: EcashExecuteMsg, + ) { + match msg { + EcashExecuteMsg::DepositTicketBookFunds { identity_key } => client + .make_ticketbook_deposit(identity_key.to_string(), None) + .ignore(), + EcashExecuteMsg::AddToBlacklist { public_key: _ } => unimplemented!(), //no add to blacklist method on client + EcashExecuteMsg::ProposeToBlacklist { public_key } => { + client.propose_for_blacklist(public_key, None).ignore() + } + ExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + } => client + .request_ticket_redemption(commitment_bs58, number_of_tickets, None) + .ignore(), + ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index d58da6fbd3..8210c5b476 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -8,34 +8,32 @@ use std::str::FromStr; // TODO: all of those could/should be derived via a macro // query clients -pub mod coconut_bandwidth_query_client; pub mod dkg_query_client; +pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; pub mod vesting_query_client; // signing clients -pub mod coconut_bandwidth_signing_client; pub mod dkg_signing_client; +pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; pub mod vesting_signing_client; // re-export query traits -pub use coconut_bandwidth_query_client::{ - CoconutBandwidthQueryClient, PagedCoconutBandwidthQueryClient, -}; pub use dkg_query_client::{DkgQueryClient, PagedDkgQueryClient}; +pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits -pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; pub use dkg_signing_client::DkgSigningClient; +pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; @@ -48,7 +46,7 @@ pub trait NymContractsProvider { fn vesting_contract_address(&self) -> Option<&AccountId>; // coconut-related - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId>; + fn ecash_contract_address(&self) -> Option<&AccountId>; fn dkg_contract_address(&self) -> Option<&AccountId>; fn group_contract_address(&self) -> Option<&AccountId>; fn multisig_contract_address(&self) -> Option<&AccountId>; @@ -59,7 +57,7 @@ pub struct TypedNymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, @@ -78,8 +76,8 @@ impl TryFrom for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, - coconut_bandwidth_contract_address: value - .coconut_bandwidth_contract_address + ecash_contract_address: value + .ecash_contract_address .map(|addr| addr.parse()) .transpose()?, group_contract_address: value diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs index 57c6ce2bf7..6094219401 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs @@ -6,7 +6,7 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cw3::{ - ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail, + ProposalListResponse, ProposalResponse, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, }; use cw_utils::ThresholdResponse; @@ -134,6 +134,28 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient { Ok(voters) } + + async fn get_all_votes(&self, proposal_id: u64) -> Result, NyxdError> { + let mut votes = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .list_votes(proposal_id, start_after.take(), None) + .await?; + + let last_voter = paged_response.votes.last().map(|vote| vote.voter.clone()); + votes.append(&mut paged_response.votes); + + if let Some(start_after_res) = last_voter { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(votes) + } } #[async_trait] diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs index f7249f02ea..ae28cf0ef3 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs @@ -31,15 +31,15 @@ pub trait MultisigSigningClient: NymContractsProvider { voucher_value: Coin, fee: Option, ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() + let ecash_contract_address = self + .ecash_contract_address() .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; let release_funds_req = CoconutBandwidthExecuteMsg::ReleaseFunds { funds: voucher_value.into(), }; let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: coconut_bandwidth_contract_address.to_string(), + contract_addr: ecash_contract_address.to_string(), msg: to_binary(&release_funds_req)?, funds: vec![], }); diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 2a26c97155..8d0bb0fb4f 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -2,31 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::cosmwasm_client::client_traits::CosmWasmClient; -use crate::nyxd::cosmwasm_client::helpers::{compress_wasm_code, CheckResponse}; +use crate::nyxd::cosmwasm_client::helpers::{ + compress_wasm_code, parse_msg_responses, CheckResponse, +}; use crate::nyxd::cosmwasm_client::logs::parse_raw_logs; use crate::nyxd::cosmwasm_client::types::*; use crate::nyxd::error::NyxdError; use crate::nyxd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER}; +use crate::nyxd::helpers::find_tx_attribute; use crate::nyxd::{Coin, GasAdjustable, GasPrice, TxResponse}; use crate::signing::signer::OfflineSigner; use crate::signing::tx_signer::TxSigner; use crate::signing::SignerData; use async_trait::async_trait; use cosmrs::bank::MsgSend; +use cosmrs::cosmwasm::{MsgClearAdmin, MsgUpdateAdmin}; use cosmrs::distribution::MsgWithdrawDelegatorReward; use cosmrs::feegrant::{ AllowedMsgAllowance, BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance, }; use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; -use cosmrs::tendermint::abci::{Event, EventAttribute}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; - use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; @@ -52,20 +54,6 @@ fn single_unspecified_signer_auth( } .auth_info(empty_fee()) } -// Searches in events for an event of the given event type which contains an -// attribute for with the given key. -fn find_attribute<'a>( - events: &'a [Event], - event_type: &str, - attr_key: &str, -) -> Option<&'a EventAttribute> { - events - .iter() - .find(|attr| attr.kind == event_type)? - .attributes - .iter() - .find(|attr| attr.key == attr_key) -} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -132,8 +120,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -143,9 +130,8 @@ where // 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 = find_attribute(&events, "store_code", "code_id") + let code_id = find_tx_attribute(&tx_res, "store_code", "code_id") .unwrap() - .value .parse() .unwrap(); @@ -156,7 +142,7 @@ where compressed_checksum, code_id, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -198,8 +184,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -208,16 +193,15 @@ where // 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 = find_attribute(&events, "instantiate", "_contract_address") + let contract_address = find_tx_attribute(&tx_res, "instantiate", "_contract_address") .unwrap() - .value .parse() .unwrap(); Ok(InstantiateResult { contract_address, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -231,7 +215,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgUpdateAdmin { + let change_admin_msg = MsgUpdateAdmin { sender: sender_address.clone(), new_admin: new_admin.clone(), contract: contract_address.clone(), @@ -263,7 +247,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgClearAdmin { + let change_admin_msg = MsgClearAdmin { sender: sender_address.clone(), contract: contract_address.clone(), } @@ -355,10 +339,11 @@ where gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), }; + Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -401,8 +386,8 @@ where }; Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -731,167 +716,3 @@ where )?) } } - -// a temporary bypass until https://github.com/cosmos/cosmos-rust/pull/419 is merged -mod sealed { - pub mod cosmwasm { - use cosmrs::{proto, tx::Msg, AccountId, ErrorReport, Result}; - - /// MsgUpdateAdmin sets a new admin for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// NewAdmin address to be set - pub new_admin: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgUpdateAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdmin; - } - - impl TryFrom for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - MsgUpdateAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgUpdateAdmin> for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: &proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - Ok(MsgUpdateAdmin { - sender: proto.sender.parse()?, - new_admin: proto.new_admin.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin::from(&msg) - } - } - - impl From<&MsgUpdateAdmin> for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: &MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - sender: msg.sender.to_string(), - new_admin: msg.new_admin.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgUpdateAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdminResponse {} - - impl Msg for MsgUpdateAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse; - } - - impl TryFrom for MsgUpdateAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse, - ) -> Result { - Ok(MsgUpdateAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - fn from( - _msg: MsgUpdateAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {} - } - } - - /// MsgClearAdmin removes any admin stored for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgClearAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdmin; - } - - impl TryFrom for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - MsgClearAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgClearAdmin> for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: &proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - Ok(MsgClearAdmin { - sender: proto.sender.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin::from(&msg) - } - } - - impl From<&MsgClearAdmin> for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: &MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin { - sender: msg.sender.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgClearAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdminResponse {} - - impl Msg for MsgClearAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdminResponse; - } - - impl TryFrom for MsgClearAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgClearAdminResponse, - ) -> Result { - Ok(MsgClearAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - fn from( - _msg: MsgClearAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - proto::cosmwasm::wasm::v1::MsgClearAdminResponse {} - } - } - } -} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 1718ab2f02..c437125b16 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -2,9 +2,87 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::error::NyxdError; +use cosmrs::abci::TxMsgData; +use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; +use log::error; +use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +pub use cosmrs::abci::MsgResponse; + +pub fn parse_msg_responses(data: Bytes) -> Vec { + // it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre + // the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails, + // use the bytes directly instead + let data = if let Ok(decoded) = base64::decode(&data) { + decoded + } else { + error!("failed to base64-decode the 'data' field of the TxResponse - has the chain been upgraded and introduced some breaking changes?"); + data.into() + }; + + match TxMsgData::try_from(data) { + Ok(tx_msg_data) => tx_msg_data.msg_responses, + Err(err) => { + error!("failed to parse tx responses - has the chain been upgraded and introduced some breaking changes? the error was {err}"); + Vec::new() + } + } +} + +// requires there's a single response message +pub trait ToSingletonContractData: Sized { + fn parse_singleton_u32_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 4 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 4, + }); + } + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + } + + fn parse_singleton_u64_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 8 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 8, + }); + } + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn to_singleton_contract_data(&self) -> Result, NyxdError>; +} + +impl ToSingletonContractData for ExecuteResult { + fn to_singleton_contract_data(&self) -> Result, NyxdError> { + if self.msg_responses.len() != 1 { + return Err(NyxdError::UnexpectedNumberOfMsgResponses { + got: self.msg_responses.len(), + }); + } + + self.msg_responses[0].to_contract_response_data() + } +} + +pub trait ToContractResponseData: Sized { + fn to_contract_response_data(&self) -> Result, NyxdError>; +} + +impl ToContractResponseData for MsgResponse { + fn to_contract_response_data(&self) -> Result, NyxdError> { + Ok(self.try_decode_as::()?.data) + } +} + pub(crate) trait CheckResponse: Sized { fn check_response(self) -> Result; } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs index 24180d8466..4a31a1fc7e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs @@ -3,10 +3,11 @@ use crate::nyxd::error::NyxdError; use itertools::Itertools; +use nym_ecash_contract_common::events::PROPOSAL_ID_ATTRIBUTE_NAME; use serde::{Deserialize, Serialize}; -pub use nym_coconut_bandwidth_contract_common::event_attributes::*; pub use nym_coconut_dkg_common::event_attributes::*; +pub use nym_ecash_contract_common::event_attributes::*; // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as their logs @@ -33,6 +34,25 @@ pub fn find_attribute<'a>( .find(|attr| attr.key == attribute_key) } +/// Search for the proposal id in the given log. It'll be in the LAST wasm event, with attribute key "proposal_id" +pub fn find_proposal_id(logs: &[Log]) -> Result { + let maybe_attributes = logs + .iter() + .rev() + .flat_map(|log| log.events.iter()) + .find(|event| event.ty == "wasm") + .ok_or(NyxdError::ComswasmEventNotFound)? + .attributes + .iter() + .find(|attr| attr.key == PROPOSAL_ID_ATTRIBUTE_NAME); + let attribute = maybe_attributes.ok_or(NyxdError::ComswasmAttributeNotFound)?; + + attribute + .value + .parse::() + .map_err(|_| NyxdError::DeserializationError("proposal_id".into())) +} + // these two functions were separated so that the internal logic could actually be tested fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { // From Cosmos SDK > 0.50 onwards, log field is not populated @@ -49,7 +69,7 @@ fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { Ok(logs) } -pub fn parse_raw_logs(raw: String) -> Result, NyxdError> { +pub fn parse_raw_logs>(raw: S) -> Result, NyxdError> { parse_raw_str_logs(raw.as_ref()) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index 544f6aaf0d..3ededcb929 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -23,6 +23,8 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::query::Query; use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; +pub use helpers::{ToContractResponseData, ToSingletonContractData}; + #[cfg(feature = "http-client")] use crate::http_client; #[cfg(feature = "http-client")] diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index b10f9b65ec..564a17441e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -30,6 +30,7 @@ use prost::Message; use serde::Serialize; pub use cosmrs::abci::GasInfo; +pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; @@ -240,7 +241,7 @@ pub struct UploadResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct InstantiateOptions { /// The funds that are transferred from the sender to the newly created contract. /// The funds are transferred as part of the message execution after the contract address is @@ -262,6 +263,11 @@ impl InstantiateOptions { admin, } } + + pub fn with_admin(mut self, admin: AccountId) -> Self { + self.admin = Some(admin); + self + } } #[derive(Debug, Serialize)] @@ -307,7 +313,7 @@ pub struct MigrateResult { pub struct ExecuteResult { pub logs: Vec, - pub data: Vec, + pub msg_responses: Vec, pub events: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index 905d484b72..c71ed596ad 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -32,6 +32,12 @@ pub enum NyxdError { #[error("There was an issue on the cosmrs side: {0}")] CosmrsErrorReport(#[from] cosmrs::ErrorReport), + #[error("cosmwasm event not found")] + ComswasmEventNotFound, + + #[error("cosmwasm attribute not found")] + ComswasmAttributeNotFound, + #[error("Failed to derive account address")] AccountDerivationError, @@ -142,6 +148,12 @@ pub enum NyxdError { #[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")] UnexpectedBech32Prefix { got: String, expected: String }, + + #[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")] + UnexpectedNumberOfMsgResponses { got: usize }, + + #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] + MalformedResponseData { got: usize, expected: usize }, } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/client-libs/validator-client/src/nyxd/helpers.rs b/common/client-libs/validator-client/src/nyxd/helpers.rs index 85999eac6f..cec865252a 100644 --- a/common/client-libs/validator-client/src/nyxd/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/helpers.rs @@ -3,11 +3,16 @@ use crate::nyxd::TxResponse; +// Searches in events for an event of the given event type which contains an +// attribute for with the given key. pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option { let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?; - let attribute = event - .attributes - .iter() - .find(|attr| attr.key == attribute_key)?; - Some(attribute.value.clone()) + let attribute = event.attributes.iter().find(|&attr| { + if let Ok(key_str) = attr.key_str() { + key_str == attribute_key + } else { + false + } + })?; + Some(attribute.value_str().ok().map(|str| str.to_string())).flatten() } diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 608fb2b2b0..fd3e40c379 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -245,8 +245,8 @@ impl NyxdClient { self.config.contracts.vesting_contract_address = Some(address); } - pub fn set_coconut_bandwidth_contract_address(&mut self, address: AccountId) { - self.config.contracts.coconut_bandwidth_contract_address = Some(address); + pub fn set_ecash_contract_address(&mut self, address: AccountId) { + self.config.contracts.ecash_contract_address = Some(address); } pub fn set_multisig_contract_address(&mut self, address: AccountId) { @@ -267,11 +267,8 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.vesting_contract_address.as_ref() } - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId> { - self.config - .contracts - .coconut_bandwidth_contract_address - .as_ref() + fn ecash_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.ecash_contract_address.as_ref() } fn dkg_contract_address(&self) -> Option<&AccountId> { @@ -384,6 +381,14 @@ where } } + pub fn mix_coin(&self, amount: u128) -> Coin { + Coin::new(amount, &self.config.chain_details.mix_denom.base) + } + + pub fn mix_coins(&self, amount: u128) -> Vec { + vec![self.mix_coin(amount)] + } + pub fn cw_address(&self) -> Addr { // the call to unchecked is fine here as we're converting directly from `AccountId` // which must have been a valid bech32 address diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 0649e4e876..2045cf0d2a 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -43,9 +43,9 @@ nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } -nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-sphinx = { path = "../../common/nymsphinx" } nym-client-core = { path = "../../common/client-core" } nym-config = { path = "../../common/config" } diff --git a/common/commands/src/coconut/generate_freepass.rs b/common/commands/src/coconut/generate_freepass.rs deleted file mode 100644 index 6ea535ebfe..0000000000 --- a/common/commands/src/coconut/generate_freepass.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use anyhow::{anyhow, bail}; -use clap::ArgGroup; -use clap::Parser; -use futures::StreamExt; -use log::{error, info}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credential_utils::utils::block_until_coconut_is_available; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::{ - obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential, -}; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::CosmWasmClient; -use nym_validator_client::signing::AccountData; -use nym_validator_client::CoconutApiClient; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::sync::Arc; -use time::format_description::well_known::Rfc3339; -use time::OffsetDateTime; -use zeroize::Zeroizing; - -fn parse_rfc3339_expiration_date(raw: &str) -> Result { - OffsetDateTime::parse(raw, &Rfc3339) -} - -#[derive(Debug, Parser)] -#[clap(group(ArgGroup::new("expiration").required(true)))] -pub struct Args { - /// Specifies the expiration date of the free pass(es) - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration", value_parser = parse_rfc3339_expiration_date)] - pub(crate) expiration_date: Option, - - /// The expiration of the free pass(es) expresses as unix timestamp. - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration")] - pub(crate) expiration_timestamp: Option, - - /// The number of free passes to issue - #[clap(long, default_value = "1")] - pub(crate) amount: u64, - - /// Path to the output directory for generated free passes. - #[clap(long)] - pub(crate) output_dir: PathBuf, -} - -async fn get_freepass( - api_clients: Vec, - aggregate_vk: &VerificationKey, - threshold: u64, - epoch_id: EpochId, - signing_account: &AccountData, - expiration_date: OffsetDateTime, -) -> anyhow::Result { - let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date)); - let signing_data = issuance_pass.prepare_for_signing(); - - let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new())); - - futures::stream::iter(api_clients) - .for_each_concurrent(None, |client| async { - // move the client into the block - let client = client; - let api_url = client.api_client.api_url(); - - info!("contacting {api_url} for blinded free pass"); - - match issuance_pass - .obtain_partial_freepass_credential( - &client.api_client, - signing_account, - &client.verification_key, - signing_data.clone(), - ) - .await - { - Ok(partial_credential) => { - credential_shares - .lock() - .await - .push((partial_credential, client.node_id).into()); - } - Err(err) => { - error!("failed to obtain partial free pass from {api_url}: {err}") - } - } - }) - .await; - - // SAFETY: the futures have completed, so we MUST have the only arc reference - #[allow(clippy::unwrap_used)] - let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner(); - - if credential_shares.len() < threshold as usize { - bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len()); - } - - let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?; - Ok(issuance_pass.into_issued_credential(signature, epoch_id)) -} - -pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - let address = client.address(); - - if !args.output_dir.is_dir() { - bail!("the provided output directory is not a directory!"); - } - - if args.output_dir.read_dir()?.next().is_some() { - bail!("the provided output directory is not empty!"); - } - - let Some(bandwidth_contract) = client.coconut_bandwidth_contract_address() else { - bail!("the bandwidth contract address is not set") - }; - - let Some(bandwidth_admin) = client - .get_contract(bandwidth_contract) - .await - .map(|c| c.contract_info.admin)? - else { - bail!("the bandwidth contract doesn't have any admin set") - }; - - // sanity checks since nym-apis will reject invalid requests anyway - if address != bandwidth_admin { - bail!("the provided mnemonic does not correspond to the current admin of the bandwidth contract") - } - - let expiration_date = match args.expiration_date { - Some(date) => date, - // SAFETY: one of those arguments must have been set - None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?, - }; - - let now = OffsetDateTime::now_utc(); - - if expiration_date > now + MAX_FREE_PASS_VALIDITY { - bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})") - } - - if expiration_date < now { - bail!("the provided free pass expiry is set in the past!") - } - - // issuance start - block_until_coconut_is_available(&client).await?; - - let signing_account = client.signing_account()?; - - let epoch_id = client.get_current_epoch().await?.epoch_id; - let threshold = client - .get_current_epoch_threshold() - .await? - .ok_or(anyhow!("no threshold available"))?; - let api_clients = all_coconut_api_clients(&client, epoch_id).await?; - - if api_clients.len() < threshold as usize { - bail!( - "we have only {} api clients available while the minimum threshold is {threshold}", - api_clients.len() - ) - } - let aggregate_vk = obtain_aggregate_verification_key(&api_clients)?; - - for i in 0..args.amount { - let human_index = i + 1; - info!("trying to obtain free pass {human_index}/{}", args.amount); - let free_pass = get_freepass( - api_clients.clone(), - &aggregate_vk, - threshold, - epoch_id, - &signing_account, - expiration_date, - ) - .await?; - let credential_data = Zeroizing::new(free_pass.pack_v1()); - let output = args.output_dir.join(format!("freepass_{i}.nym")); - info!("saving the freepass to '{}'", output.display()); - File::create(output)?.write_all(&credential_data)?; - } - - Ok(()) -} diff --git a/common/commands/src/coconut/import_credential.rs b/common/commands/src/coconut/import_ticket_book.rs similarity index 100% rename from common/commands/src/coconut/import_credential.rs rename to common/commands/src/coconut/import_ticket_book.rs diff --git a/common/commands/src/coconut/issue_credentials.rs b/common/commands/src/coconut/issue_ticket_book.rs similarity index 65% rename from common/commands/src/coconut/issue_credentials.rs rename to common/commands/src/coconut/issue_ticket_book.rs index 9533af614e..a1d2f0f8ef 100644 --- a/common/commands/src/coconut/issue_credentials.rs +++ b/common/commands/src/coconut/issue_ticket_book.rs @@ -7,7 +7,7 @@ use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; use nym_credential_utils::utils; -use nym_validator_client::nyxd::Coin; +use nym_crypto::asymmetric::identity; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -15,21 +15,9 @@ pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, - - /// The amount of utokens the credential will hold. - #[clap(long, default_value = "0")] - pub(crate) amount: u64, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, } pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - if args.amount == 0 { - bail!("did not specify credential amount") - } - let loaded = CommonConfigsWrapper::try_load(args.client_config)?; if let Ok(id) = loaded.try_get_id() { @@ -40,16 +28,18 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { bail!("the loaded config does not have a credentials store information") }; + let Ok(private_id_key) = loaded.try_get_private_id_key() else { + bail!("the loaded config does not have a public id key information") + }; + println!( "using credentials store at '{}'", credentials_store.display() ); - let denom = &client.current_chain_details().mix_denom.base; - let coin = Coin::new(args.amount as u128, denom); - let persistent_storage = initialise_persistent_storage(credentials_store).await; - utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?; + let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?; + utils::issue_credential(&client, &persistent_storage, &private_id_key.to_bytes()).await?; Ok(()) } diff --git a/common/commands/src/coconut/mod.rs b/common/commands/src/coconut/mod.rs index 700c7d521f..74421dd42b 100644 --- a/common/commands/src/coconut/mod.rs +++ b/common/commands/src/coconut/mod.rs @@ -3,22 +3,20 @@ use clap::{Args, Subcommand}; -pub mod generate_freepass; -pub mod import_credential; -pub mod issue_credentials; -pub mod recover_credentials; +pub mod import_ticket_book; +pub mod issue_ticket_book; +pub mod recover_ticket_book; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct Coconut { +pub struct Ecash { #[clap(subcommand)] - pub command: CoconutCommands, + pub command: EcashCommands, } #[derive(Debug, Subcommand)] -pub enum CoconutCommands { - GenerateFreepass(generate_freepass::Args), - IssueCredentials(issue_credentials::Args), - RecoverCredentials(recover_credentials::Args), - ImportCredential(import_credential::Args), +pub enum EcashCommands { + IssueTicketBook(issue_ticket_book::Args), + RecoverTicketBook(recover_ticket_book::Args), + ImportTicketBook(import_ticket_book::Args), } diff --git a/common/commands/src/coconut/recover_credentials.rs b/common/commands/src/coconut/recover_ticket_book.rs similarity index 69% rename from common/commands/src/coconut/recover_credentials.rs rename to common/commands/src/coconut/recover_ticket_book.rs index 025ea68c2c..8bd5c8c960 100644 --- a/common/commands/src/coconut/recover_credentials.rs +++ b/common/commands/src/coconut/recover_ticket_book.rs @@ -6,7 +6,7 @@ use crate::utils::CommonConfigsWrapper; use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; -use nym_credential_utils::{recovery_storage, utils}; +use nym_credential_utils::utils; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -14,10 +14,6 @@ pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, } pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { @@ -37,12 +33,9 @@ pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { ); let persistent_storage = initialise_persistent_storage(credentials_store).await; - let recovery_storage = recovery_storage::RecoveryStorage::new(args.recovery_dir)?; - let recovered = - utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?; + let recovered = utils::recover_deposits(&client, &persistent_storage).await?; - // TODO: denom? - println!("recovered {recovered} worth of credentials"); + println!("recovered {recovered} ticketbooks"); Ok(()) } diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 25797c71e9..0d6e40ffbc 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -123,6 +123,21 @@ impl CommonConfigsWrapper { } } + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + match self { + CommonConfigsWrapper::NymClients(cfg) => Ok(cfg + .storage_paths + .inner + .keys + .private_identity_key_file + .clone()), + CommonConfigsWrapper::NymApi(_cfg) => { + todo!() //SW this will depend on the new network monitor structure. Ping @Drazen + } + CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_private_id_key(), + } + } + pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result { match self { CommonConfigsWrapper::NymClients(cfg) => { @@ -225,4 +240,17 @@ impl UnknownConfigWrapper { bail!("no 'credentials_database_path' field present in the config") } } + + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + let id_val = self + .find_value("keys.private_identity_key_file") + .ok_or_else(|| { + anyhow!("no 'keys.private_identity_key_file' field present in the config") + })?; + if let toml::Value::String(pub_id_key) = id_val { + Ok(pub_id_key.parse()?) + } else { + bail!("no 'keys.private_identity_key_file' field present in the config") + } + } } diff --git a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs similarity index 68% rename from common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs rename to common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs index 3deb1efe01..c3632b1130 100644 --- a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs +++ b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs @@ -6,17 +6,20 @@ use std::str::FromStr; use clap::Parser; use log::{debug, info}; -use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg; +use nym_ecash_contract_common::msg::InstantiateMsg; use nym_validator_client::nyxd::AccountId; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub pool_addr: String, + pub group_addr: Option, #[clap(long)] pub multisig_addr: Option, + #[clap(long)] + pub holding_account: AccountId, + #[clap(long)] pub mix_denom: Option, } @@ -26,8 +29,15 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); + let group_addr = args.group_addr.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS) + .expect("Multisig address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting multisig address to AccountId") + }); + let multisig_addr = args.multisig_addr.unwrap_or_else(|| { - let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) + let address = std::env::var(nym_network_defaults::var_names::MULTISIG_CONTRACT_ADDRESS) .expect("Multisig address has to be set"); AccountId::from_str(address.as_str()) .expect("Failed converting multisig address to AccountId") @@ -38,7 +48,8 @@ pub async fn generate(args: Args) { }); let instantiate_msg = InstantiateMsg { - pool_addr: args.pool_addr, + holding_account: args.holding_account.to_string(), + group_addr: group_addr.to_string(), multisig_addr: multisig_addr.to_string(), mix_denom, }; diff --git a/common/commands/src/validator/cosmwasm/generators/mod.rs b/common/commands/src/validator/cosmwasm/generators/mod.rs index 85bd5a93e2..8829ec60fe 100644 --- a/common/commands/src/validator/cosmwasm/generators/mod.rs +++ b/common/commands/src/validator/cosmwasm/generators/mod.rs @@ -3,8 +3,8 @@ use clap::{Args, Subcommand}; -pub mod coconut_bandwidth; pub mod coconut_dkg; +pub mod ecash_bandwidth; pub mod mixnet; pub mod multisig; pub mod vesting; @@ -18,7 +18,7 @@ pub struct GenerateMessage { #[derive(Debug, Subcommand)] pub enum GenerateMessageCommands { - CoconutBandwidth(coconut_bandwidth::Args), + EcashBandwidth(ecash_bandwidth::Args), CoconutDKG(coconut_dkg::Args), Mixnet(mixnet::Args), Multisig(multisig::Args), diff --git a/common/commands/src/validator/cosmwasm/generators/multisig.rs b/common/commands/src/validator/cosmwasm/generators/multisig.rs index 37f05a5e71..90abae9e28 100644 --- a/common/commands/src/validator/cosmwasm/generators/multisig.rs +++ b/common/commands/src/validator/cosmwasm/generators/multisig.rs @@ -22,7 +22,7 @@ pub struct Args { pub max_voting_period: u64, #[clap(long)] - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, #[clap(long)] pub coconut_dkg_contract_address: Option, @@ -33,14 +33,12 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); - let coconut_bandwidth_contract_address = - args.coconut_bandwidth_contract_address.unwrap_or_else(|| { - let address = - std::env::var(nym_network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS) - .expect("Coconut bandwidth contract address has to be set"); - AccountId::from_str(address.as_str()) - .expect("Failed converting bandwidth contract address to AccountId") - }); + let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS) + .expect("Coconut bandwidth contract address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting bandwidth contract address to AccountId") + }); let coconut_dkg_contract_address = args.coconut_dkg_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS) @@ -58,7 +56,7 @@ pub async fn generate(args: Args) { max_voting_period: Duration::Time(args.max_voting_period), executor: None, proposal_deposit: None, - coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(), + coconut_bandwidth_contract_address: ecash_contract_address.to_string(), coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(), }; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 0b3da8a552..f9d1dc0bb0 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -87,6 +87,9 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(u64))] GetCurrentEpochThreshold {}, + #[cfg_attr(feature = "schema", returns(u64))] + GetEpochThreshold { epoch_id: EpochId }, + #[cfg_attr(feature = "schema", returns(StateAdvanceResponse))] CanAdvanceState {}, diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs index 39d5c0007a..a8fdf9b816 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Event; +use std::str::FromStr; /// Looks up value of particular attribute in the provided event. If it fails to find it, /// the function panics. @@ -31,6 +32,23 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option { None } +pub fn try_find_attribute( + events: &[Event], + event_name: &str, + key: &str, +) -> Option> +where + T: FromStr, +{ + for event in events { + if event.ty == event_name { + let value = may_find_attribute(event, key)?; + return Some(value.parse()); + } + } + None +} + pub trait OptionallyAddAttribute { fn add_optional_attribute( self, diff --git a/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml new file mode 100644 index 0000000000..54bc53fe18 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nym-ecash-contract-common" +version = "0.1.0" +edition = "2021" +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bs58.workspace = true +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true, optional = true } +nym-multisig-contract-common = { path = "../multisig-contract" } +thiserror.workspace = true +cw-utils = { workspace = true } +cw-controllers = { workspace = true } + + +[features] +schema = ["cw2"] diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs new file mode 100644 index 0000000000..00b6dfc6ae --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct BlacklistedAccount { + pub public_key: String, + pub info: Blacklisting, +} + +impl From<(String, Blacklisting)> for BlacklistedAccount { + fn from((public_key, info): (String, Blacklisting)) -> Self { + BlacklistedAccount { public_key, info } + } +} + +#[cw_serde] +pub struct Blacklisting { + pub proposal_id: u64, + pub finalized_at_height: Option, +} + +impl Blacklisting { + pub fn new(proposal_id: u64) -> Self { + Blacklisting { + proposal_id, + finalized_at_height: None, + } + } +} + +impl BlacklistedAccount { + pub fn public_key(&self) -> &str { + &self.public_key + } +} + +#[cw_serde] +pub struct PagedBlacklistedAccountResponse { + pub accounts: Vec, + pub per_page: usize, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +impl PagedBlacklistedAccountResponse { + pub fn new( + accounts: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedBlacklistedAccountResponse { + accounts, + per_page, + start_next_after, + } + } +} + +#[cw_serde] +pub struct BlacklistedAccountResponse { + pub account: Option, +} + +impl BlacklistedAccountResponse { + pub fn new(account: Option) -> Self { + BlacklistedAccountResponse { account } + } +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs new file mode 100644 index 0000000000..86efdbd5fa --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::EcashContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdError, StdResult}; + +pub type DepositId = u32; + +#[cw_serde] +pub struct Deposit { + pub bs58_encoded_ed25519_pubkey: String, +} + +impl Deposit { + pub fn new(bs58_encoded_ed25519_pubkey: String) -> Self { + Deposit { + bs58_encoded_ed25519_pubkey, + } + } + + pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> { + let mut ed25519_pubkey_bytes = [0u8; 32]; + bs58::decode(raw) + .onto(&mut ed25519_pubkey_bytes) + .map_err(|_| EcashContractError::MalformedEd25519Identity)?; + + Ok(ed25519_pubkey_bytes) + } + + pub fn encode_pubkey_bytes(raw: &[u8]) -> String { + bs58::encode(raw).into_string() + } + + pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> { + Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey) + } + + pub fn try_from_bytes(bytes: &[u8]) -> StdResult { + if bytes.len() != 32 { + return Err(StdError::generic_err("malformed deposit data")); + } + + Ok(Deposit { + bs58_encoded_ed25519_pubkey: Self::encode_pubkey_bytes(bytes), + }) + } +} + +#[cw_serde] +pub struct DepositResponse { + pub id: DepositId, + + pub deposit: Option, +} + +#[cw_serde] +pub struct DepositData { + pub id: DepositId, + + pub deposit: Deposit, +} + +impl From<(DepositId, Deposit)> for DepositData { + fn from((id, deposit): (DepositId, Deposit)) -> Self { + DepositData { id, deposit } + } +} + +#[cw_serde] +pub struct PagedDepositsResponse { + pub deposits: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs new file mode 100644 index 0000000000..3c7778d553 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs @@ -0,0 +1,68 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, StdError}; +use cw_controllers::AdminError; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum EcashContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Invalid deposit")] + InvalidDeposit(#[from] PaymentError), + + #[error("received wrong amount for deposit. got: {received}. required: {amount}")] + WrongAmount { received: u128, amount: u128 }, + + #[error("There aren't enough funds in the contract")] + NotEnoughFunds, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error("could not find proposal id inside the multisig reply SubMsg")] + MissingProposalId, + + // realistically this should NEVER be thrown + #[error("the proposal id returned by the multisig contract could not be parsed into an u64")] + MalformedProposalId, + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("Unauthorized")] + Unauthorized, + + #[error("Failed to parse {value} into a valid SemVer version: {error_message}")] + SemVerFailure { + value: String, + error_message: String, + }, + + #[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")] + InvalidReplyId { id: u64 }, + + #[error("reached the maximum of 255 different deposit types")] + MaximumDepositTypesReached, + + #[error("compressed deposit info {typ} does not corresponds to any known type")] + UnknownCompressedDepositInfoType { typ: u8 }, + + #[error("deposit info {typ} does not corresponds to any previously seen type")] + UnknownDepositInfoType { typ: String }, + + #[error("the provided ed25519 identity was malformed")] + MalformedEd25519Identity, + + #[error("the required deposit amount has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")] + DepositAmountChanged { at_init: Coin, current: Coin }, + + #[error("the e-cash ticket value has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")] + TicketValueChanged { at_init: Coin, current: Coin }, + + #[error("the provided tickets redemption commitment is malformed")] + MalformedRedemptionCommitment, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs new file mode 100644 index 0000000000..80f5daf68a --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs new file mode 100644 index 0000000000..79892aa2b0 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs @@ -0,0 +1,18 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// event types +pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds"; + +// a 'wasm-' prefix is added to all cosmwasm events +pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds"; + +pub const DEPOSIT_ID: &str = "deposit-id"; + +pub const TICKET_BOOK_VALUE: u128 = 50_000_000; +pub const TICKET_VALUE: u128 = 50_000; + +pub const WASM_EVENT_NAME: &str = "wasm"; +pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id"; +pub const BLACKLIST_PROPOSAL_REPLY_ID: u64 = 7759; +pub const REDEMPTION_PROPOSAL_REPLY_ID: u64 = 2137; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs new file mode 100644 index 0000000000..10e07c1009 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod blacklist; +pub mod deposit; +pub mod error; +pub mod event_attributes; +pub mod events; +pub mod msg; +pub mod redeem_credential; + +pub use error::EcashContractError; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs new file mode 100644 index 0000000000..4b5c965978 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs @@ -0,0 +1,76 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; + +#[cfg(feature = "schema")] +use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse}; +#[cfg(feature = "schema")] +use crate::deposit::{DepositResponse, PagedDepositsResponse}; +#[cfg(feature = "schema")] +use cosmwasm_schema::QueryResponses; +#[cfg(feature = "schema")] +use cosmwasm_std::Coin; + +#[cw_serde] +pub struct InstantiateMsg { + pub holding_account: String, + pub multisig_addr: String, + pub group_addr: String, + pub mix_denom: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Used by clients to request ticket books from the signers + DepositTicketBookFunds { + identity_key: String, + }, + + /// Used by gateways to batch redeem tokens from the spent tickets + RequestRedemption { + commitment_bs58: String, + number_of_tickets: u16, + }, + + /// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account + RedeemTickets { + n: u16, + gw: String, + }, + // SpendCredential { + // serial_number: String, + // gateway_cosmos_address: String, + // }, + ProposeToBlacklist { + public_key: String, + }, + AddToBlacklist { + public_key: String, + }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))] + GetBlacklistedAccount { public_key: String }, + + #[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))] + GetBlacklistPaged { + limit: Option, + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(Coin))] + GetRequiredDepositAmount {}, + + #[cfg_attr(feature = "schema", returns(DepositResponse))] + GetDeposit { deposit_id: u32 }, + + #[cfg_attr(feature = "schema", returns(PagedDepositsResponse))] + GetDepositsPaged { + limit: Option, + start_after: Option, + }, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs new file mode 100644 index 0000000000..76bf86f5a8 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: to be moved to multisig +pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption"; diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs index ead941cad0..39ba25b38c 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs @@ -47,4 +47,10 @@ pub enum ContractError { #[error("{0}")] Deposit(#[from] DepositError), + + #[error("the provided redemption digest does not have valid base58 encoding or is not 32 bytes long")] + MalformedRedemptionDigest, + + #[error("the provided redemption proposal data is malformed and can't be decoded")] + MalformedRedemptionProposalData, } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 056d94b12b..b822d49db6 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -8,22 +8,31 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"]} +serde = { workspace = true, features = ["derive"], optional = true } +tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } +nym-credentials = { path = "../credentials" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } + [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true -features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] workspace = true -features = [ "rt-multi-thread", "net", "signal", "fs" ] +features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[features] +persistent-storage = ["bincode", "serde"] \ No newline at end of file diff --git a/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql new file mode 100644 index 0000000000..10b585d096 --- /dev/null +++ b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql @@ -0,0 +1,66 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DROP TABLE coconut_credentials; + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_key BLOB NOT NULL +); + +CREATE TABLE coin_indices_signatures +( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + + +CREATE TABLE ecash_ticketbook +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + -- the actual crypto data of the ticketbook (wallet, keys, etc.) + ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date), + + -- for each ticketbook we MUST have corresponding coin index signatures + epoch_id INTEGER NOT NULL REFERENCES coin_indices_signatures(epoch_id), + + -- the initial number of tickets the wallet has been created for + total_tickets INTEGER NOT NULL, + + -- how many tickets have been used so far (the `l` value of the wallet) + used_tickets INTEGER NOT NULL +); + +-- data for ticketbooks that have an associated deposit, but failed to get issued +CREATE TABLE pending_issuance +( + deposit_id INTEGER NOT NULL PRIMARY KEY, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + pending_ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date) +); \ No newline at end of file diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 78d18f3b88..998c25cdeb 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,23 +1,34 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{CredentialUsage, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use zeroize::Zeroizing; #[derive(Clone)] -pub struct CoconutCredentialManager { - inner: Arc>, +pub struct MemoryEcachTicketbookManager { + inner: Arc>, } #[derive(Default)] -struct CoconutCredentialManagerInner { - credentials: Vec, - credential_usage: Vec, +struct EcashCredentialManagerInner { + ticketbooks: HashMap, + pending: HashMap, + master_vk: HashMap, + coin_indices_sigs: HashMap>, + expiration_date_sigs: HashMap>, _next_id: i64, } -impl CoconutCredentialManagerInner { +impl EcashCredentialManagerInner { fn next_id(&mut self) -> i64 { let next = self._next_id; self._next_id += 1; @@ -25,108 +36,209 @@ impl CoconutCredentialManagerInner { } } -impl CoconutCredentialManager { +// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround +fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook { + let ser = book.pack(); + let data = Zeroizing::new(ser.data); + IssuedTicketBook::try_unpack(&data, None).unwrap() +} + +impl MemoryEcachTicketbookManager { /// Creates new empty instance of the `CoconutCredentialManager`. pub fn new() -> Self { - CoconutCredentialManager { + MemoryEcachTicketbookManager { inner: Default::default(), } } - pub async fn insert_issued_credential( - &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], - epoch_id: u32, - ) { - let mut inner = self.inner.write().await; - let id = inner.next_id(); - inner.credentials.push(StoredIssuedCredential { - id, - serialization_revision, - credential_data: credential_data.to_vec(), - credential_type, - epoch_id, - expired: false, - }) - } - - async fn bandwidth_voucher_spent(&self, id: i64) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id) - } - - async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id) - } - - /// Tries to retrieve one of the stored, unused credentials. - pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "BandwidthVoucher") - { - if !self.bandwidth_voucher_spent(credential.id).await { - return Some(credential.clone()); - } - } - None - } - - pub async fn get_next_unspect_freepass( - &self, - gateway_id: &str, - ) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "FreeBandwidthPass") - { - if credential.expired { - continue; - } - if !self.freepass_spent(credential.id, gateway_id).await { - return Some(credential.clone()); - } - } - None - } - - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) { + pub(crate) async fn cleanup_expired(&self) { let mut guard = self.inner.write().await; - guard.credential_usage.push(CredentialUsage { - credential_id: id, - gateway_id_bs58: gateway_id.to_string(), - }); + + let mut to_remove = Vec::new(); + + for t in guard.ticketbooks.values() { + if t.ticketbook.expired() { + to_remove.push(t.ticketbook_id); + } + } + + for id in to_remove { + guard.ticketbooks.remove(&id); + } } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) { - let mut creds = self.inner.write().await; - if let Some(cred) = creds.credentials.get_mut(id as usize) { - cred.expired = true; + pub async fn get_next_unspent_ticketbook_and_update( + &self, + tickets: u32, + ) -> Option { + let mut guard = self.inner.write().await; + + for t in guard.ticketbooks.values_mut() { + if !t.ticketbook.expired() + && t.ticketbook.spent_tickets() + tickets as u64 + <= t.ticketbook.params_total_tickets() + { + t.ticketbook + .update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64); + return Some(RetrievedTicketbook { + ticketbook_id: t.ticketbook_id, + ticketbook: hack_clone_ticketbook(&t.ticketbook), + }); + } } + + None + } + + pub(crate) async fn revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> bool { + let mut guard = self.inner.write().await; + + let Some(book) = guard.ticketbooks.get_mut(&ticketbook_id) else { + return false; + }; + + if book.ticketbook.spent_tickets() == expected_current_total_spent as u64 { + book.ticketbook + .update_spent_tickets(book.ticketbook.spent_tickets() - withdrawn as u64); + true + } else { + false + } + } + + pub(crate) async fn insert_pending_ticketbook(&self, ticketbook: &IssuanceTicketBook) { + let mut guard = self.inner.write().await; + + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let id = ticketbook.deposit_id() as i64; + guard.pending.insert( + id, + RetrievedPendingTicketbook { + pending_id: ticketbook.deposit_id() as i64, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_pending_ticketbooks(&self) -> Vec { + let guard = self.inner.read().await; + + let mut pending = Vec::new(); + + for p in guard.pending.values() { + // 🫠 + let ser = p.pending_ticketbook.pack(); + let data = Zeroizing::new(ser.data); + pending.push(RetrievedPendingTicketbook { + pending_id: p.pending_id, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }) + } + + pending + } + + pub(crate) async fn remove_pending_ticketbook(&self, pending_id: i64) { + let mut guard = self.inner.write().await; + + guard.pending.remove(&pending_id); + } + + pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) { + let mut guard = self.inner.write().await; + let id = guard.next_id(); + + // hehe, that's hacky AF, but it works as a **TEMPORARY** workaround + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + guard.ticketbooks.insert( + id, + RetrievedTicketbook { + ticketbook_id: id, + ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_ticketbooks_info(&self) -> Vec { + let guard = self.inner.read().await; + + guard + .ticketbooks + .values() + .map(|t| BasicTicketbookInformation { + id: t.ticketbook_id, + expiration_date: t.ticketbook.expiration_date(), + epoch_id: t.ticketbook.epoch_id() as u32, + total_tickets: t.ticketbook.spent_tickets() as u32, + used_tickets: t.ticketbook.params_total_tickets() as u32, + }) + .collect() + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Option { + let guard = self.inner.read().await; + + guard.master_vk.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) { + let mut guard = self.inner.write().await; + + guard.master_vk.insert(epoch_id, key.clone()); + } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Option> { + let guard = self.inner.read().await; + + guard.coin_indices_sigs.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) { + let mut guard = self.inner.write().await; + + guard.coin_indices_sigs.insert(epoch_id, sigs.to_vec()); + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Option> { + let guard = self.inner.read().await; + + guard.expiration_date_sigs.get(&expiration_date).cloned() + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + _epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) { + let mut guard = self.inner.write().await; + + guard + .expiration_date_sigs + .insert(expiration_date, sigs.to_vec()); } } diff --git a/common/credential-storage/src/backends/mod.rs b/common/credential-storage/src/backends/mod.rs index 2cd1334af3..bdb8eb6d9c 100644 --- a/common/credential-storage/src/backends/mod.rs +++ b/common/credential-storage/src/backends/mod.rs @@ -2,5 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 pub mod memory; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod sqlite; diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 4d4face955..a3b8a00349 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -1,116 +1,279 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::StoredIssuedCredential; +use crate::models::{ + BasicTicketbookInformation, RawExpirationDateSignatures, StoredIssuedTicketbook, + StoredPendingTicketbook, +}; +use nym_ecash_time::Date; +use sqlx::{Executor, Sqlite, Transaction}; #[derive(Clone)] -pub struct CoconutCredentialManager { +pub struct SqliteEcashTicketbookManager { connection_pool: sqlx::SqlitePool, } -impl CoconutCredentialManager { - /// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool. +impl SqliteEcashTicketbookManager { + /// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool. /// /// # Arguments /// /// * `connection_pool`: database connection pool to use. pub fn new(connection_pool: sqlx::SqlitePool) -> Self { - CoconutCredentialManager { connection_pool } + SqliteEcashTicketbookManager { connection_pool } } - pub async fn insert_issued_credential( + pub(crate) async fn cleanup_expired(&self, deadline: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", + deadline + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn begin_storage_tx(&self) -> Result, sqlx::Error> { + self.connection_pool.begin().await + } + + pub(crate) async fn insert_pending_ticketbook( &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], + serialisation_revision: u8, + deposit_id: u32, + data: &[u8], + expiration_date: Date, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO pending_issuance + (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date) + VALUES (?, ?, ?, ?) + "#, + deposit_id, + serialisation_revision, + data, + expiration_date, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_new_ticketbook( + &self, + serialisation_revision: u8, + data: &[u8], + expiration_date: Date, epoch_id: u32, + total_tickets: u32, + used_tickets: u32, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired) - VALUES (?, ?, ?, ?, false) + INSERT INTO ecash_ticketbook + (serialization_revision, ticketbook_data, expiration_date, epoch_id, total_tickets, used_tickets) + VALUES (?, ?, ?, ?, ?, ?) "#, - serialization_revision, credential_type, credential_data, epoch_id + serialisation_revision, + data, + expiration_date, + epoch_id, + total_tickets, + used_tickets, ).execute(&self.connection_pool).await?; + Ok(()) } - pub async fn get_next_unspect_freepass( + pub(crate) async fn get_ticketbooks_info( &self, - gateway_id: &str, - ) -> Result, sqlx::Error> { - // get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id + ) -> Result, sqlx::Error> { sqlx::query_as( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id - AND credential_usage.gateway_id_bs58 == ?) - ORDER BY coconut_credentials.id - LIMIT 1 - "#, + SELECT id, expiration_date, epoch_id, total_tickets, used_tickets + FROM ecash_ticketbook + "#, ) - .bind(gateway_id) - .fetch_optional(&self.connection_pool) + .fetch_all(&self.connection_pool) .await } - pub async fn get_next_unspect_bandwidth_voucher( + pub(crate) async fn decrease_used_ticketbook_tickets( &self, - ) -> Result, sqlx::Error> { - // get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id - sqlx::query_as( + ticketbook_id: i64, + reverted_spent: u32, + expected_current_total_spent: u32, + ) -> Result { + // the 'AND' clause will ensure this will only be executed if nobody else interacted with the row + let affected = sqlx::query!( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "BandwidthVoucher" - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id) - ORDER BY coconut_credentials.id - LIMIT 1 + UPDATE ecash_ticketbook + SET used_tickets = used_tickets - ? + WHERE id = ? + AND used_tickets = ? "#, + reverted_spent, + ticketbook_id, + expected_current_total_spent ) - .fetch_optional(&self.connection_pool) - .await + .execute(&self.connection_pool) + .await? + .rows_affected(); + Ok(affected > 0) } - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - /// * `gateway_id`: id of the gateway that received the credential - pub async fn consume_coconut_credential( + pub(crate) async fn get_pending_ticketbooks( &self, - id: i64, - gateway_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM pending_issuance") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn remove_pending_ticketbook( + &self, + pending_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( - "INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)", - id, - gateway_id + "DELETE FROM pending_issuance WHERE deposit_id = ?", + pending_id ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) -> Result<(), sqlx::Error> { + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { sqlx::query!( - "UPDATE coconut_credentials SET expired = TRUE WHERE id = ?", - id + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data ) .execute(&self.connection_pool) .await?; Ok(()) } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM coin_indices_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} + +pub(crate) async fn get_next_unspent_ticketbook<'a, E>( + executor: E, + deadline: Date, + tickets: u32, +) -> Result, sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query_as( + r#" + SELECT * + FROM ecash_ticketbook + WHERE used_tickets + ? <= total_tickets + AND expiration_date >= ? + ORDER BY expiration_date ASC + LIMIT 1 + "#, + ) + .bind(tickets) + .bind(deadline) + .fetch_optional(executor) + .await +} + +pub(crate) async fn increase_used_ticketbook_tickets<'a, E>( + executor: E, + ticketbook_id: i64, + extra_spent: u32, +) -> Result<(), sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query!( + "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", + extra_spent, + ticketbook_id + ) + .execute(executor) + .await?; + Ok(()) } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index ead11edcf8..e1a18942c2 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -1,26 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::{self, Debug, Formatter}; - -use crate::backends::memory::CoconutCredentialManager; +use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use crate::storage::Storage; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::fmt::{self, Debug, Formatter}; pub type EphemeralCredentialStorage = EphemeralStorage; // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub struct EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager, + storage_manager: MemoryEcachTicketbookManager, } impl Default for EphemeralStorage { fn default() -> Self { EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager::new(), + storage_manager: MemoryEcachTicketbookManager::new(), } } } @@ -35,55 +39,135 @@ impl Debug for EphemeralStorage { impl Storage for EphemeralStorage { type StorageError = StorageError; - async fn insert_issued_credential<'a>( + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + self.storage_manager.cleanup_expired().await; + Ok(()) + } + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_pending_ticketbook(ticketbook) .await; Ok(()) } - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } + ticketbook: &IssuedTicketBook, + ) -> Result<(), StorageError> { + self.storage_manager.insert_new_ticketbook(ticketbook).await; + Ok(()) + } + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_pending_ticketbooks().await) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() + .storage_manager + .get_next_unspent_ticketbook_and_update(tickets) .await) } - async fn consume_coconut_credential( + async fn attempt_revert_ticketbook_withdrawal( &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await; + ticketbook_id: i64, + previous_total_spent: u32, + withdrawn: u32, + ) -> Result { + Ok(self + .storage_manager + .revert_ticketbook_withdrawal(ticketbook_id, previous_total_spent, withdrawn) + .await) + } + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + Ok(self + .storage_manager + .get_master_verification_key(epoch_id) + .await) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_master_verification_key(epoch_id, key) + .await; Ok(()) } - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await; + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_coin_index_signatures(epoch_id) + .await) + } + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id, data) + .await; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures(epoch_id, expiration_date, data) + .await; Ok(()) } } diff --git a/common/credential-storage/src/error.rs b/common/credential-storage/src/error.rs index 42b94c2fc4..665e4db638 100644 --- a/common/credential-storage/src/error.rs +++ b/common/credential-storage/src/error.rs @@ -9,6 +9,9 @@ pub enum StorageError { #[error("Database experienced an internal error - {0}")] InternalDatabaseError(#[from] sqlx::Error), + #[error("experienced internal storage error due to database inconsistency: {reason}")] + DatabaseInconsistency { reason: String }, + #[cfg(not(target_arch = "wasm32"))] #[error("Failed to perform database migration - {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), @@ -19,6 +22,17 @@ pub enum StorageError { #[error("No unused credential in database. You need to buy at least one")] NoCredential, + #[error("No signatures for epoch {epoch_id} in the database")] + NoSignatures { epoch_id: i64 }, + #[error("Database unique constraint violation. Is the credential already imported?")] ConstraintUnique, } + +impl StorageError { + pub fn database_inconsistency>(reason: S) -> StorageError { + StorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/common/credential-storage/src/lib.rs b/common/credential-storage/src/lib.rs index c27e904192..e320926175 100644 --- a/common/credential-storage/src/lib.rs +++ b/common/credential-storage/src/lib.rs @@ -5,21 +5,20 @@ use crate::ephemeral_storage::EphemeralStorage; -#[cfg(not(target_arch = "wasm32"))] -use crate::persistent_storage::PersistentStorage; -#[cfg(not(target_arch = "wasm32"))] -use std::path::Path; - mod backends; pub mod ephemeral_storage; pub mod error; pub mod models; -#[cfg(not(target_arch = "wasm32"))] + +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod persistent_storage; + pub mod storage; -#[cfg(not(target_arch = "wasm32"))] -pub async fn initialise_persistent_storage>(path: P) -> PersistentStorage { +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] +pub async fn initialise_persistent_storage>( + path: P, +) -> crate::persistent_storage::PersistentStorage { match persistent_storage::PersistentStorage::init(path).await { Err(err) => panic!("failed to initialise credential storage - {err}"), Ok(storage) => storage, diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 49c004ece0..c400631645 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -1,44 +1,62 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use zeroize::{Zeroize, ZeroizeOnDrop}; -// #[derive(Clone)] -// pub struct CoconutCredential { -// #[allow(dead_code)] -// pub id: i64, -// pub voucher_value: String, -// pub voucher_info: String, -// pub serial_number: String, -// pub binding_number: String, -// pub signature: String, -// pub epoch_id: String, -// pub consumed: bool, -// } +pub struct RetrievedTicketbook { + pub ticketbook_id: i64, + pub ticketbook: IssuedTicketBook, +} + +pub struct RetrievedPendingTicketbook { + pub pending_id: i64, + pub pending_ticketbook: IssuanceTicketBook, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct BasicTicketbookInformation { + pub id: i64, + pub expiration_date: Date, + pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, +} #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] #[derive(Zeroize, ZeroizeOnDrop, Clone)] -pub struct StoredIssuedCredential { +pub struct StoredIssuedTicketbook { pub id: i64, pub serialization_revision: u8, - pub credential_data: Vec, - pub credential_type: String, + + pub ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, pub epoch_id: u32, - pub expired: bool, -} -pub struct StorableIssuedCredential<'a> { - pub serialization_revision: u8, - pub credential_data: &'a [u8], - pub credential_type: String, - - pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, } #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] -pub struct CredentialUsage { - pub credential_id: i64, - pub gateway_id_bs58: String, +#[derive(Zeroize, ZeroizeOnDrop, Clone)] +pub struct StoredPendingTicketbook { + pub deposit_id: i64, + + pub serialization_revision: u8, + + pub pending_ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, } diff --git a/common/credential-storage/src/persistent_storage.rs b/common/credential-storage/src/persistent_storage.rs deleted file mode 100644 index cd2bd0170f..0000000000 --- a/common/credential-storage/src/persistent_storage.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::backends::sqlite::CoconutCredentialManager; -use crate::error::StorageError; -use crate::storage::Storage; - -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; -use async_trait::async_trait; -use log::{debug, error}; -use sqlx::ConnectOptions; -use std::path::Path; - -// note that clone here is fine as upon cloning the same underlying pool will be used -#[derive(Clone)] -pub struct PersistentStorage { - coconut_credential_manager: CoconutCredentialManager, -} - -impl PersistentStorage { - /// Initialises `PersistentStorage` using the provided path. - /// - /// # Arguments - /// - /// * `database_path`: path to the database. - pub async fn init>(database_path: P) -> Result { - debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() - ); - - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() - .filename(database_path) - .create_if_missing(true); - - opts.disable_statement_logging(); - - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { - Ok(db) => db, - Err(err) => { - error!("Failed to connect to SQLx database: {err}"); - return Err(err.into()); - } - }; - - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { - error!("Failed to perform migration on the SQLx database: {err}"); - return Err(err.into()); - } - - Ok(PersistentStorage { - coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()), - }) - } -} - -#[async_trait] -impl Storage for PersistentStorage { - type StorageError = StorageError; - - async fn insert_issued_credential<'a>( - &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), Self::StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) - .await - .map_err(|err| { - // There is one error we want to handle specifically. - // Check if database_error is `SqliteError` with code 2067 which - // means UNIQUE constraint violation - if let Some(db_error) = err.as_database_error() { - if db_error.code().map_or(false, |code| code == "2067") { - StorageError::ConstraintUnique - } else { - err.into() - } - } else { - err.into() - } - }) - } - - async fn get_next_unspent_credential( - &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await?; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } - - Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() - .await?) - } - - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await?; - - Ok(()) - } - - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await?; - - Ok(()) - } -} diff --git a/common/credential-storage/src/persistent_storage/helpers.rs b/common/credential-storage/src/persistent_storage/helpers.rs new file mode 100644 index 0000000000..614800d609 --- /dev/null +++ b/common/credential-storage/src/persistent_storage/helpers.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::StorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +// storage serialiser used for non-critical data, such as global expiration signatures or master verification keys, +// i.e. data that could always be queried for again if malformed +fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs new file mode 100644 index 0000000000..4ad915db15 --- /dev/null +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -0,0 +1,307 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::backends::sqlite::{ + get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, +}; +use crate::error::StorageError; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::persistent_storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::storage::Storage; +use async_trait::async_trait; +use log::{debug, error}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::{ecash_today, Date, EcashTime}; +use sqlx::ConnectOptions; +use std::path::Path; +use zeroize::Zeroizing; + +mod helpers; + +// note that clone here is fine as upon cloning the same underlying pool will be used +#[derive(Clone)] +pub struct PersistentStorage { + storage_manager: SqliteEcashTicketbookManager, +} + +impl PersistentStorage { + /// Initialises `PersistentStorage` using the provided path. + /// + /// # Arguments + /// + /// * `database_path`: path to the database. + pub async fn init>(database_path: P) -> Result { + debug!( + "Attempting to connect to database {:?}", + database_path.as_ref().as_os_str() + ); + + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to perform migration on the SQLx database: {err}"); + return Err(err.into()); + } + + Ok(PersistentStorage { + storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + }) + } +} + +#[async_trait] +impl Storage for PersistentStorage { + type StorageError = StorageError; + + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + let ecash_yesterday = ecash_today().date().previous_day().unwrap(); + self.storage_manager + .cleanup_expired(ecash_yesterday) + .await?; + Ok(()) + } + + async fn insert_pending_ticketbook( + &self, + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_pending_ticketbook( + serialisation_revision, + ticketbook.deposit_id(), + &data, + ticketbook.expiration_date(), + ) + .await?; + + Ok(()) + } + + async fn insert_issued_ticketbook( + &self, + ticketbook: &IssuedTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_new_ticketbook( + serialisation_revision, + &data, + ticketbook.expiration_date(), + ticketbook.epoch_id() as u32, + ticketbook.params_total_tickets() as u32, + ticketbook.spent_tickets() as u32, + ) + .await?; + + Ok(()) + } + + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await?) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + let pending = self + .storage_manager + .get_pending_ticketbooks() + .await? + .into_iter() + .map(|p| { + IssuanceTicketBook::try_unpack(&p.pending_ticketbook_data, p.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored pending ticketbook: {err}" + )) + }) + .map(|pending_ticketbook| RetrievedPendingTicketbook { + pending_id: p.deposit_id, + pending_ticketbook, + }) + }) + .collect::>()?; + Ok(pending) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await?; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { + let deadline = ecash_today().ecash_date(); + let mut tx = self.storage_manager.begin_storage_tx().await?; + + // we don't want ticketbooks with expiration in the past + let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else { + // make sure to finish our tx + tx.commit().await?; + return Ok(None); + }; + + let mut deserialised = + IssuedTicketBook::try_unpack(&raw.ticketbook_data, raw.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored ticketbook: {err}" + )) + })?; + + increase_used_ticketbook_tickets(&mut tx, raw.id, tickets).await?; + tx.commit().await?; + + // set the number of spent tickets on the crypto object + // TODO: I don't like how that's required and can be easily missed, + // perhaps we shouldn't be storing the `IssuedTicketBook` data in the db, + // but all of its fields instead? + deserialised.update_spent_tickets(raw.used_tickets as u64); + Ok(Some(RetrievedTicketbook { + ticketbook_id: raw.id, + ticketbook: deserialised, + })) + } + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result { + Ok(self + .storage_manager + .decrease_used_ticketbook_tickets( + ticketbook_id, + withdrawn, + expected_current_total_spent, + ) + .await?) + } + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + Ok(self + .storage_manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id as i64, &serialise_coin_index_signatures(sigs)) + .await?; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_expiration_date_signatures( + &raw.serialised_signatures, + )?)) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures( + epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(sigs), + ) + .await?; + Ok(()) + } +} diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 7880a3722e..8b3ebc74ec 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,42 +1,93 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use std::error::Error; +// for future reference, if you want to make a query for "how much bandwidth do we have left" +// do something along the lines of +// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date +// then for each calculate the diff total_tickets - used_tickets and multiply the result by the size of the ticket #[async_trait] pub trait Storage: Send + Sync { type StorageError: Error; - async fn insert_issued_credential<'a>( + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError>; + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, + ticketbook: &IssuanceTicketBook, ) -> Result<(), Self::StorageError>; - /// Tries to retrieve one of the stored, unused credentials, - /// that is also not marked as expired - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError>; - - /// Marks as consumed in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Id of the credential to be consumed. - /// * `gateway_id`: id of the gateway that received the credential. - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, + ticketbook: &IssuedTicketBook, ) -> Result<(), Self::StorageError>; - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError>; + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError>; + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError>; + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>; + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError>; + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError>; + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError>; + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError>; + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError>; + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError>; + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-utils/Cargo.toml b/common/credential-utils/Cargo.toml index 232c9cdb1f..3e2d534fa2 100644 --- a/common/credential-utils/Cargo.toml +++ b/common/credential-utils/Cargo.toml @@ -10,11 +10,13 @@ license.workspace = true log = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +time.workspace = true nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-coconut = { path = "../nymcoconut" } nym-credentials = { path = "../../common/credentials" } -nym-credential-storage = { path = "../../common/credential-storage" } +nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-config = { path = "../../common/config" } nym-client-core = { path = "../../common/client-core" } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-ecash-time = { path = "../../common/ecash-time" } \ No newline at end of file diff --git a/common/credential-utils/src/errors.rs b/common/credential-utils/src/errors.rs index 6407929a99..456f542180 100644 --- a/common/credential-utils/src/errors.rs +++ b/common/credential-utils/src/errors.rs @@ -3,6 +3,7 @@ use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use std::num::ParseIntError; use thiserror::Error; @@ -17,15 +18,30 @@ pub enum Error { #[error(transparent)] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), + #[error(transparent)] + EcashApiError(#[from] EcashApiError), + #[error(transparent)] Nyxd(#[from] NyxdError), #[error(transparent)] Credential(#[from] CredentialError), - #[error("Could not use shared storage: {0}")] - SharedStorageError(#[from] StorageError), + #[error("could not use shared storage: {0}")] + SharedStorageError(Box), #[error("failed to parse credential value: {0}")] MalformedCredentialValue(#[from] ParseIntError), } + +impl Error { + pub fn storage_error(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Error::SharedStorageError(Box::new(source)) + } +} + +impl From for Error { + fn from(value: StorageError) -> Self { + Self::storage_error(value) + } +} diff --git a/common/credential-utils/src/lib.rs b/common/credential-utils/src/lib.rs index 6e5fbc3158..60bd6d2bcf 100644 --- a/common/credential-utils/src/lib.rs +++ b/common/credential-utils/src/lib.rs @@ -1,5 +1,4 @@ pub mod errors; -pub mod recovery_storage; pub mod utils; pub use errors::{Error, Result}; diff --git a/common/credential-utils/src/recovery_storage.rs b/common/credential-utils/src/recovery_storage.rs deleted file mode 100644 index 0344a3ad86..0000000000 --- a/common/credential-utils/src/recovery_storage.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::errors::Result; -use log::error; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use std::fs::{create_dir_all, read_dir, File}; -use std::io::{Read, Write}; -use std::path::PathBuf; - -pub const DUMPED_VOUCHER_EXTENSION: &str = "credentialrecovery"; - -pub struct RecoveryStorage { - recovery_dir: PathBuf, -} - -impl RecoveryStorage { - pub fn new(recovery_dir: PathBuf) -> Result { - create_dir_all(&recovery_dir)?; - Ok(Self { recovery_dir }) - } - - pub fn unconsumed_vouchers(&self) -> Result> { - let entries = read_dir(&self.recovery_dir)?; - - let mut paths = vec![]; - for entry in entries.flatten() { - let path = entry.path(); - if let Some(extension) = path.extension() { - if extension == DUMPED_VOUCHER_EXTENSION { - paths.push(path) - } - } - } - - let mut vouchers = vec![]; - for path in paths { - if let Ok(mut file) = File::open(&path) { - let mut buff = Vec::new(); - if file.read_to_end(&mut buff).is_ok() { - match IssuanceBandwidthCredential::try_from_recovered_bytes(&buff) { - Ok(voucher) => vouchers.push(voucher), - Err(err) => { - error!("failed to parse the voucher at {}: {err}", path.display()) - } - } - } - } - } - - Ok(vouchers) - } - - pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String { - let prefix = voucher.typ().to_string(); - let suffix = voucher.blinded_serial_number_bs58(); - format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}") - } - - pub fn insert_voucher(&self, voucher: &IssuanceBandwidthCredential) -> Result { - let file_name = Self::voucher_filename(voucher); - let file_path = self.recovery_dir.join(file_name); - let mut file = File::create(&file_path)?; - let buff = voucher.to_recovery_bytes(); - file.write_all(&buff)?; - - Ok(file_path) - } - - pub fn remove_voucher(&self, file_name: String) -> Result<()> { - let file_path = self.recovery_dir.join(file_name); - Ok(std::fs::remove_file(file_path)?) - } -} diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index 33a0b20541..7c77b077b5 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -1,74 +1,75 @@ use crate::errors::{Error, Result}; -use crate::recovery_storage::RecoveryStorage; use log::*; -use nym_bandwidth_controller::acquire::state::State; +use nym_bandwidth_controller::acquire::{ + get_ticket_book, query_and_persist_required_global_signatures, +}; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::DEFAULT_DATA_DIR; use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_credentials::coconut::bandwidth::CredentialType; +use nym_credential_storage::storage::Storage; +use nym_ecash_time::ecash_default_expiration_date; +use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nyxd::contract_traits::{ - dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient, + dkg_query_client::EpochState, DkgQueryClient, EcashSigningClient, }; -use nym_validator_client::nyxd::Coin; use std::path::PathBuf; -use std::time::{Duration, SystemTime}; +use std::time::Duration; +use time::OffsetDateTime; -pub async fn issue_credential( - client: &C, - amount: Coin, - persistent_storage: &PersistentStorage, - recovery_storage_path: PathBuf, -) -> Result<()> +pub async fn issue_credential(client: &C, storage: &S, client_id: &[u8]) -> Result<()> where - C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync, + C: DkgQueryClient + EcashSigningClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let recovery_storage = setup_recovery_storage(recovery_storage_path).await; - - block_until_coconut_is_available(client).await?; + block_until_ecash_is_available(client).await?; info!("Starting to deposit funds, don't kill the process"); - if let Ok(recovered_amount) = - recover_credentials(client, &recovery_storage, persistent_storage).await - { - if recovered_amount != 0 { - info!( - "Recovered credentials in the amount of {}", - recovered_amount - ); + if let Ok(recovered_ticketbooks) = recover_deposits(client, storage).await { + if recovered_ticketbooks != 0 { + info!("managed to recover {recovered_ticketbooks} ticket books. no need to make fresh deposit"); return Ok(()); } }; - let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?; + let epoch_id = client.get_current_epoch().await?.epoch_id; + let apis = all_ecash_api_clients(client, epoch_id).await?; + let ticketbook_expiration = ecash_default_expiration_date(); - if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage) + // make sure we have all required coin indices and expiration date signatures before attempting the deposit + query_and_persist_required_global_signatures( + storage, + epoch_id, + ticketbook_expiration, + apis.clone(), + ) + .await?; + + let issuance_data = nym_bandwidth_controller::acquire::make_deposit( + client, + client_id, + Some(ticketbook_expiration), + ) + .await?; + info!("Deposit done"); + + if get_ticket_book(&issuance_data, client, storage, Some(apis)) .await .is_err() { - warn!("Failed to obtain credential. Dumping recovery data.",); - match recovery_storage.insert_voucher(&state.voucher) { - Ok(file_path) => { - warn!("Dumped recovery data to {}. Try using recovery mode to convert it to a credential", file_path.to_str().unwrap()); - } - Err(e) => { - error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e) - } - } + error!("failed to obtain credential. saving recovery data..."); - return Err(Error::Credential( - nym_credentials::error::Error::BandwidthCredentialError, - )); + storage.insert_pending_ticketbook(&issuance_data).await.inspect_err(|err| { + let deposit = issuance_data.deposit_id(); + error!("could not save the recovery data for deposit {deposit}: {err}. the data will unfortunately get lost") + }).map_err(Error::storage_error)? } - info!("Succeeded adding a credential with amount {amount}"); + info!("Succeeded adding a ticketbook"); Ok(()) } -pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage { - RecoveryStorage::new(recovery_dir).expect("") -} - pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage { let data_dir = client_home_directory.join(DEFAULT_DATA_DIR); let paths = CommonClientPaths::new_base(data_dir); @@ -77,16 +78,13 @@ pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> Persist nym_credential_storage::initialise_persistent_storage(db_path).await } -pub async fn block_until_coconut_is_available(client: &C) -> Result<()> +pub async fn block_until_ecash_is_available(client: &C) -> Result<()> where C: DkgQueryClient + Send + Sync, { loop { let epoch = client.get_current_epoch().await?; - let current_timestamp_secs = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("the system clock is set to 01/01/1970 (or earlier)") - .as_secs(); + let current_timestamp_secs = OffsetDateTime::now_utc().unix_timestamp() as u64; if epoch.state.is_final() { break; @@ -101,7 +99,7 @@ where } else { // this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation, // but let's guard ourselves against future changes - info!("it is unknown when coconut will be come available. Going to check again later"); + info!("it is unknown when ecash will be come available. Going to check again later"); tokio::time::sleep(Duration::from_secs(60 * 5)).await; } } @@ -109,42 +107,47 @@ where Ok(()) } -pub async fn recover_credentials( - client: &C, - recovery_storage: &RecoveryStorage, - shared_storage: &PersistentStorage, -) -> Result +pub async fn recover_deposits(client: &C, storage: &S) -> Result where C: DkgQueryClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let mut recovered_amount: u128 = 0; - for voucher in recovery_storage.unconsumed_vouchers()? { - let voucher_value = match voucher.typ() { - CredentialType::Voucher => voucher.get_bandwidth_attribute(), - CredentialType::FreePass => { - error!("unimplemented recovery of free pass credentials"); - continue; - } - }; - recovered_amount += voucher_value.parse::()?; + info!("checking for any incomplete previous issuance attempts..."); - let voucher_name = RecoveryStorage::voucher_filename(&voucher); - let state = State::new(voucher); + let incomplete = storage + .get_pending_ticketbooks() + .await + .map_err(Error::storage_error)?; + info!( + "we recovered {} incomplete ticketbook issuances", + incomplete.len() + ); - if let Err(e) = - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, shared_storage) - .await - { - error!("Could not recover deposit {voucher_name} due to {e}, try again later",) - } else { - info!( - "Converted deposit {voucher_name} to a credential, removing recovery data for it", - ); - if let Err(err) = recovery_storage.remove_voucher(voucher_name) { - warn!("Could not remove recovery data: {err}"); + let mut recovered_books = 0; + for issuance in incomplete { + let deposit = issuance.pending_ticketbook.deposit_id(); + if issuance.pending_ticketbook.expired() { + warn!("ticketbook data associated with deposit {deposit} has expired. if you haven't contacted more than 1/3 of signers. it could still be recoverable (but out of scope of this library)"); + continue; + } + + if issuance.pending_ticketbook.check_expiration_date() { + warn!("deposit {deposit} was made with a different expiration date, it's validity will be shorter than the max one"); + } + + match get_ticket_book(&issuance.pending_ticketbook, client, storage, None).await { + Err(err) => error!("could not recover deposit {deposit} due to: {err}"), + Ok(_) => { + info!("managed to recover deposit {deposit}! the ticketbook has been added to the storage"); + storage + .remove_pending_ticketbook(issuance.pending_id) + .await + .map_err(Error::storage_error)?; + recovered_books += 1; } } } - Ok(recovered_amount) + Ok(recovered_books) } diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index 5393ea37fd..7d2365683f 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -14,5 +14,8 @@ license.workspace = true bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +time = { workspace = true, features = ["serde"] } +rand = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } \ No newline at end of file diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 1337e633d5..765c479644 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -1,136 +1,218 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bls12_381::Scalar; +use rand::Rng; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use thiserror::Error; +use time::{Date, OffsetDateTime}; -pub use nym_coconut::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, aggregate_verification_keys, - blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential, - verify_credential, Attribute, Base58, BlindSignRequest, BlindedSerialNumber, BlindedSignature, - Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey, - Signature, SignatureShare, VerificationKey, VerifyCredentialRequest, +pub use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters, + error::CompactEcashError, + generate_keypair_user, generate_keypair_user_from_seed, issue_verify, + scheme::coin_indices_signatures::aggregate_indices_signatures, + scheme::coin_indices_signatures::{ + AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare, + PartialCoinIndexSignature, + }, + scheme::expiration_date_signatures::aggregate_expiration_signatures, + scheme::expiration_date_signatures::date_scalar, + scheme::expiration_date_signatures::{ + AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare, + PartialExpirationDateSignature, + }, + scheme::keygen::KeyPairUser, + scheme::withdrawal::RequestInfo, + scheme::Payment, + scheme::{Wallet, WalletSignatures}, + withdrawal_request, Base58, BlindedSignature, Bytable, PartialWallet, PayInfo, PublicKeyUser, + SecretKeyUser, VerificationKeyAuth, WithdrawalRequest, }; - -pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher"; -pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass"; - -// pub trait NymCredential { -// fn prove_credential(&self) -> Result<(), ()>; -// } - -#[derive(Debug, Error)] -#[error("{0} is not a valid credential type")] -pub struct UnknownCredentialType(String); - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum CredentialType { - Voucher, - FreePass, -} - -impl FromStr for CredentialType { - type Err = UnknownCredentialType; - - fn from_str(s: &str) -> Result { - if s == VOUCHER_INFO_TYPE { - Ok(CredentialType::Voucher) - } else if s == FREE_PASS_INFO_TYPE { - Ok(CredentialType::FreePass) - } else { - Err(UnknownCredentialType(s.to_string())) - } - } -} - -impl CredentialType { - pub fn validate(&self, type_plain: &str) -> bool { - match self { - CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE, - CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE, - } - } - - pub fn is_free_pass(&self) -> bool { - matches!(self, CredentialType::FreePass) - } - - pub fn is_voucher(&self) -> bool { - matches!(self, CredentialType::Voucher) - } -} - -impl Display for CredentialType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f), - CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f), - } - } -} +use nym_ecash_time::EcashTime; #[derive(Debug, Clone)] pub struct CredentialSigningData { - pub pedersen_commitments_openings: Vec, + pub withdrawal_request: WithdrawalRequest, - pub blind_sign_request: BlindSignRequest, + pub request_info: RequestInfo, - pub public_attributes_plain: Vec, + pub ecash_pub_key: PublicKeyUser, - pub typ: CredentialType, + pub expiration_date: Date, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CredentialSpendingData { - pub embedded_private_attributes: usize, + pub payment: Payment, - pub verify_credential_request: VerifyCredentialRequest, + pub pay_info: PayInfo, - pub public_attributes_plain: Vec, - - pub typ: CredentialType, + pub spend_date: Date, + // pub value: u64, /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation. pub epoch_id: u64, } impl CredentialSpendingData { - pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool { - let hashed_public_attributes = self - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - - // get references to the attributes - let public_attributes = hashed_public_attributes.iter().collect::>(); - - verify_credential( - params, + pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> { + self.payment.spend_verify( verification_key, - &self.verify_credential_request, - &public_attributes, + &self.pay_info, + date_scalar(self.spend_date.ecash_unix_timestamp()), ) } - pub fn validate_type_attribute(&self) -> bool { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - let Some(type_plain) = self.public_attributes_plain.get(1) else { - return false; + pub fn encoded_serial_number(&self) -> Vec { + self.payment.encoded_serial_number() + } + + pub fn serial_number_b58(&self) -> String { + self.payment.serial_number_bs58() + } + + pub fn to_bytes(&self) -> Vec { + // simple length prefixed serialization + // TODO: change it to a standard format instead + let mut bytes = Vec::new(); + let payment_bytes = self.payment.to_bytes(); + + bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes()); + bytes.extend_from_slice(&payment_bytes); + bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long + bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes()); + bytes.extend_from_slice(&self.epoch_id.to_be_bytes()); + + bytes + } + + pub fn try_from_bytes(raw: &[u8]) -> Result { + // minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix) + if raw.len() < 72 + 8 + 4 + 4 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let mut index = 0; + //SAFETY : casting a slice of length 4 into an array of size 4 + let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize; + index += 4; + + if raw[index..].len() != payment_len + 84 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let payment = Payment::try_from(&raw[index..index + payment_len])?; + index += payment_len; + + let pay_info = PayInfo { + //SAFETY : casting a slice of length 72 into an array of size 72 + pay_info_bytes: raw[index..index + 72].try_into().unwrap(), }; + index += 72; - self.typ.validate(type_plain) - } + //SAFETY : casting a slice of length 4 into an array of size 4 + let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap()); + let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| { + CompactEcashError::DeserializationFailure { + object: "CredentialSpendingData".into(), + } + })?; + index += 4; - pub fn get_bandwidth_attribute(&self) -> Option<&String> { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - self.public_attributes_plain.first() - } + if raw[index..].len() != 8 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } - pub fn blinded_serial_number(&self) -> BlindedSerialNumber { - self.verify_credential_request.blinded_serial_number() + //SAFETY : casting a slice of length 8 into an array of size 8 + let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap()); + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date, + epoch_id, + }) + } +} + +impl Bytable for CredentialSpendingData { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingData {} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct NymPayInfo { + randomness: [u8; 32], + timestamp: i64, + provider_public_key: [u8; 32], +} + +impl NymPayInfo { + /// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key. + /// + /// # Arguments + /// + /// * `provider_pk` - The public key of the payment provider. + /// + /// # Returns + /// + /// A new `NymPayInfo` instance. + /// + pub fn generate(provider_pk: [u8; 32]) -> Self { + let mut randomness = [0u8; 32]; + rand::thread_rng().fill(&mut randomness[..32]); + + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key: provider_pk, + } + } + + pub fn timestamp(&self) -> i64 { + self.timestamp + } + + pub fn pk(&self) -> [u8; 32] { + self.provider_public_key + } +} + +impl From for PayInfo { + fn from(value: NymPayInfo) -> Self { + let mut pay_info_bytes = [0u8; 72]; + + pay_info_bytes[..32].copy_from_slice(&value.randomness); + pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes()); + pay_info_bytes[40..].copy_from_slice(&value.provider_public_key); + + PayInfo { pay_info_bytes } + } +} + +impl From for NymPayInfo { + fn from(value: PayInfo) -> Self { + //SAFETY : slice to array of same length + let randomness = value.pay_info_bytes[..32].try_into().unwrap(); + let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap()); + let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key, + } } } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index a51604a142..48159258fc 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -16,11 +16,15 @@ time = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } zeroize = { workspace = true } +nym-ecash-time = { path = "../ecash-time", features = ["expiration"] } + # I guess temporarily until we get serde support in coconut up and running nym-credentials-interface = { path = "../credentials-interface" } -nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] } +nym-crypto = { path = "../crypto" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } +nym-network-defaults = { path = "../network-defaults" } [dev-dependencies] rand = "0.8.5" diff --git a/common/credentials/src/coconut/bandwidth/freepass.rs b/common/credentials/src/coconut/bandwidth/freepass.rs deleted file mode 100644 index 9329af2a6e..0000000000 --- a/common/credentials/src/coconut/bandwidth/freepass.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::FreePassRequest; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute, -}; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::{Duration, OffsetDateTime, Time}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week -pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct FreePassIssuedData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, -} - -impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData { - fn from(value: &'a FreePassIssuanceData) -> Self { - FreePassIssuedData { - expiry_date: value.expiry_date, - } - } -} - -impl FreePassIssuedData { - pub fn expired(&self) -> bool { - self.expiry_date <= OffsetDateTime::now_utc() - } - - pub fn expiry_date(&self) -> OffsetDateTime { - self.expiry_date - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } -} - -#[derive(Zeroize, Serialize, Deserialize)] -pub struct FreePassIssuanceData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, - - // the expiry date, as unix timestamp, hashed into a scalar - #[serde(with = "scalar_serde_helper")] - expiry_date_prehashed: PublicAttribute, -} - -impl FreePassIssuanceData { - pub fn new(expiry_date: Option) -> Self { - // ideally we should have implemented a proper error handling here, sure. - // but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments - let expiry_date = if let Some(provided) = expiry_date { - if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY { - panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}"); - } - - provided - } else { - Self::default_expiry_date() - }; - - let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string()); - - FreePassIssuanceData { - expiry_date, - expiry_date_prehashed, - } - } - - pub fn default_expiry_date() -> OffsetDateTime { - // set it to the furthest midnight in the future such as it's no more than a week away, - // i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024 - (OffsetDateTime::now_utc() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT) - } - - pub fn expiry_date_attribute(&self) -> &Attribute { - &self.expiry_date_prehashed - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } - - pub async fn obtain_free_pass_nonce( - &self, - client: &nym_validator_client::client::NymApiClient, - ) -> Result<[u8; 16], Error> { - let server_response = client.free_pass_nonce().await?; - Ok(server_response.current_nonce) - } - - pub fn create_free_pass_request( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - issuer_nonce: [u8; 16], - ) -> Result { - let nonce_signature = account_data - .private_key() - .sign(&issuer_nonce) - .map_err(|_| Error::Secp256k1SignFailure)?; - - Ok(FreePassRequest { - cosmos_pubkey: account_data.public_key(), - inner_sign_request: signing_request.blind_sign_request.clone(), - used_nonce: issuer_nonce, - nonce_signature, - public_attributes_plain: signing_request.public_attributes_plain.clone(), - }) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request: &FreePassRequest, - ) -> Result { - let server_response = client.issue_free_pass_credential(request).await?; - Ok(server_response.blinded_signature) - } - - pub async fn request_blinded_credential( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - client: &nym_validator_client::client::NymApiClient, - ) -> Result { - let signing_nonce = self.obtain_free_pass_nonce(client).await?; - let request = - self.create_free_pass_request(signing_request, account_data, signing_nonce)?; - self.obtain_blinded_credential(client, &request).await - } -} diff --git a/common/credentials/src/coconut/bandwidth/issuance.rs b/common/credentials/src/coconut/bandwidth/issuance.rs deleted file mode 100644 index a845b6ac7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issuance.rs +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::freepass::FreePassIssuanceData; -use crate::coconut::bandwidth::issued::IssuedBandwidthCredential; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use crate::coconut::bandwidth::{ - bandwidth_credential_params, CredentialSigningData, CredentialType, -}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar, - prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters, - PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub use nym_validator_client::nyxd::{Coin, Hash}; - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuanceDataVariant { - Voucher(BandwidthVoucherIssuanceData), - FreePass(FreePassIssuanceData), -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: FreePassIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: BandwidthVoucherIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuanceDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value(&self) -> &Attribute { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_attribute() - } - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } - - pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher), - _ => None, - } - } -} - -// all types of bandwidth credentials contain serial number and binding number -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuanceBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuanceDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, -} - -impl IssuanceBandwidthCredential { - pub const PUBLIC_ATTRIBUTES: u32 = 2; - pub const PRIVATE_ATTRIBUTES: u32 = 2; - pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES; - - pub fn default_parameters() -> Parameters { - // safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero - Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap() - } - - pub fn new>(variant_data: B) -> Self { - let variant_data = variant_data.into(); - let type_prehashed = hash_to_scalar(variant_data.info().to_string()); - - let params = bandwidth_credential_params(); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - IssuanceBandwidthCredential { - serial_number, - binding_number, - variant_data, - type_prehashed, - } - } - - pub fn new_voucher( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - Self::new(BandwidthVoucherIssuanceData::new( - value, - deposit_tx_hash, - signing_key, - unused_ed25519, - )) - } - - pub fn new_freepass(expiry_date: Option) -> Self { - Self::new(FreePassIssuanceData::new(expiry_date)) - } - - pub fn blind_serial_number(&self) -> BlindedSerialNumber { - (bandwidth_credential_params().gen2() * self.serial_number).into() - } - - pub fn blinded_serial_number_bs58(&self) -> String { - use nym_credentials_interface::Base58; - - self.blind_serial_number().to_bs58() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> { - vec![&self.serial_number, &self.binding_number] - } - - pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> { - vec![self.variant_data.public_value(), &self.type_prehashed] - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant { - &self.variant_data - } - - pub fn get_bandwidth_attribute(&self) -> String { - self.variant_data.public_value_plain() - } - - pub fn prepare_for_signing(&self) -> CredentialSigningData { - let params = bandwidth_credential_params(); - - // safety: the creation of the request can only fail if one provided invalid parameters - // and we created then specific to this type of the credential so the unwrap is fine - let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign( - params, - &[&self.serial_number, &self.binding_number], - &self.get_public_attributes(), - ) - .unwrap(); - - CredentialSigningData { - pedersen_commitments_openings, - blind_sign_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - } - } - - pub fn unblind_signature( - &self, - validator_vk: &VerificationKey, - signing_data: &CredentialSigningData, - blinded_signature: BlindedSignature, - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - let unblinded_signature = blinded_signature.unblind_and_verify( - params, - validator_vk, - &private_attributes, - &public_attributes, - &signing_data.blind_sign_request.get_commitment_hash(), - &signing_data.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature) - } - - pub async fn obtain_partial_freepass_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - account_data: &AccountData, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass - .request_blinded_credential(&signing_data, account_data, client) - .await? - } - _ => return Err(Error::NotAFreePass), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers - pub async fn obtain_partial_bandwidth_voucher_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - // TODO: the request can be re-used between different apis - let request = voucher.create_blind_sign_request_body(&signing_data); - voucher.obtain_blinded_credential(client, &request).await? - } - _ => return Err(Error::NotABandwdithVoucher), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - pub fn unchecked_aggregate_signature_shares( - &self, - shares: &[SignatureShare], - ) -> Result { - aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) - } - - pub fn aggregate_signature_shares( - &self, - verification_key: &VerificationKey, - shares: &[SignatureShare], - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(&public_attributes); - - aggregate_signature_shares_and_verify(params, verification_key, &attributes, shares) - .map_err(Error::SignatureAggregationError) - } - - // also drops self after the conversion - pub fn into_issued_credential( - self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - self.to_issued_credential(aggregate_signature, epoch_id) - } - - pub fn to_issued_credential( - &self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - aggregate_signature, - (&self.variant_data).into(), - self.type_prehashed, - epoch_id, - ) - } - - // TODO: is that actually needed? - pub fn to_recovery_bytes(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_recovery_bincode_serializer().serialize(self).unwrap() - } - - // TODO: is that actually needed? - // idea: make it consistent with the issued credential and its vX serde - pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result { - use bincode::Options; - make_recovery_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::RecoveryCredentialDeserializationFailure { source }) - } -} - -fn make_recovery_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/issued.rs b/common/credentials/src/coconut/bandwidth/issued.rs deleted file mode 100644 index 00e0ab341b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issued.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::bandwidth_credential_params; -use crate::coconut::bandwidth::freepass::FreePassIssuedData; -use crate::coconut::bandwidth::issuance::{ - BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential, -}; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::prove_bandwidth_credential; -use nym_credentials_interface::{ - Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey, -}; -use nym_validator_client::nym_api::EpochId; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; - -#[derive(Debug, Zeroize, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuedDataVariant { - Voucher(BandwidthVoucherIssuedData), - FreePass(FreePassIssuedData), -} - -impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant { - fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self { - match value { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - BandwidthCredentialIssuedDataVariant::Voucher(voucher.into()) - } - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - BandwidthCredentialIssuedDataVariant::FreePass(freepass.into()) - } - } - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: FreePassIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: BandwidthVoucherIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuedDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuedDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } -} - -// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuedBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// the underlying aggregated signature on the attributes - #[zeroize(skip)] - signature: Signature, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuedDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, - - /// Specifies the (DKG) epoch id when this credential has been issued - epoch_id: EpochId, -} - -impl IssuedBandwidthCredential { - pub fn new( - serial_number: PrivateAttribute, - binding_number: PrivateAttribute, - signature: Signature, - variant_data: BandwidthCredentialIssuedDataVariant, - type_prehashed: PublicAttribute, - epoch_id: EpochId, - ) -> Self { - IssuedBandwidthCredential { - serial_number, - binding_number, - signature, - variant_data, - type_prehashed, - epoch_id, - } - } - - pub fn try_unpack(bytes: &[u8], revision: impl Into>) -> Result { - let revision = revision.into().unwrap_or(CURRENT_SERIALIZATION_REVISION); - - match revision { - 1 => Self::unpack_v1(bytes), - _ => Err(Error::UnknownSerializationRevision { revision }), - } - } - - pub fn epoch_id(&self) -> EpochId { - self.epoch_id - } - - pub fn variant_data(&self) -> &BandwidthCredentialIssuedDataVariant { - &self.variant_data - } - - pub fn current_serialization_revision(&self) -> u8 { - CURRENT_SERIALIZATION_REVISION - } - - /// Pack (serialize) this credential data into a stream of bytes using v1 serializer. - pub fn pack_v1(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_storable_bincode_serializer().serialize(self).unwrap() - } - - /// Unpack (deserialize) the credential data from the given bytes using v1 serializer. - pub fn unpack_v1(bytes: &[u8]) -> Result { - use bincode::Options; - make_storable_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::SerializationFailure { - source, - revision: 1, - }) - } - - pub fn default_parameters() -> Parameters { - IssuanceBandwidthCredential::default_parameters() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn prepare_for_spending( - &self, - verification_key: &VerificationKey, - ) -> Result { - let params = bandwidth_credential_params(); - - let verify_credential_request = prove_bandwidth_credential( - params, - verification_key, - &self.signature, - &self.serial_number, - &self.binding_number, - )?; - - Ok(CredentialSpendingData { - embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize, - verify_credential_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - epoch_id: self.epoch_id, - }) - } -} - -fn make_storable_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/mod.rs b/common/credentials/src/coconut/bandwidth/mod.rs deleted file mode 100644 index de49c9d4b4..0000000000 --- a/common/credentials/src/coconut/bandwidth/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::OnceLock; - -pub use issuance::IssuanceBandwidthCredential; -pub use issued::IssuedBandwidthCredential; -pub use nym_credentials_interface::{ - CredentialSigningData, CredentialSpendingData, CredentialType, Parameters, - UnknownCredentialType, -}; - -pub mod freepass; -pub mod issuance; -pub mod issued; -pub mod voucher; - -// works under the assumption of having 4 attributes in the underlying credential(s) -pub fn bandwidth_credential_params() -> &'static Parameters { - static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) -} diff --git a/common/credentials/src/coconut/bandwidth/voucher.rs b/common/credentials/src/coconut/bandwidth/voucher.rs deleted file mode 100644 index 58c93edf7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/voucher.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::CredentialSigningData; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, CredentialType, PublicAttribute, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nyxd::{Coin, Hash}; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuedData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, -} - -impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData { - fn from(value: &'a BandwidthVoucherIssuanceData) -> Self { - BandwidthVoucherIssuedData { - value: value.value.clone(), - } - } -} - -impl BandwidthVoucherIssuedData { - pub fn new(value: Coin) -> Self { - BandwidthVoucherIssuedData { value } - } - - pub fn value(&self) -> &Coin { - &self.value - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } -} - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuanceData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, - - // note: as mentioned above, we're only hashing the value of the coin! - #[serde(with = "scalar_serde_helper")] - value_prehashed: PublicAttribute, - - /// the hash of the deposit transaction - #[zeroize(skip)] - deposit_tx_hash: Hash, - - /// base58 encoded private key ensuring the depositer requested these attributes - signing_key: identity::PrivateKey, - - /// base58 encoded private key ensuring only this client receives the signature share - unused_ed25519: encryption::PrivateKey, -} - -impl BandwidthVoucherIssuanceData { - pub fn new( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - let value = value.into(); - let value_prehashed = hash_to_scalar(value.amount.to_string()); - - BandwidthVoucherIssuanceData { - value, - value_prehashed, - deposit_tx_hash, - signing_key, - unused_ed25519, - } - } - - pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec { - let mut message = request.to_bytes(); - message.extend_from_slice(tx_hash.as_bytes()); - message - } - - fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { - let message = - Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash); - self.signing_key.sign(message) - } - - pub fn create_blind_sign_request_body( - &self, - signing_request: &CredentialSigningData, - ) -> BlindSignRequestBody { - let request_signature = self.request_signature(signing_request); - - BlindSignRequestBody::new( - signing_request.blind_sign_request.clone(), - self.deposit_tx_hash, - request_signature, - signing_request.public_attributes_plain.clone(), - ) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request_body: &BlindSignRequestBody, - ) -> Result { - let server_response = client.blind_sign(request_body).await?; - Ok(server_response.blinded_signature) - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } - - pub fn value_attribute(&self) -> &Attribute { - &self.value_prehashed - } - - pub fn typ() -> CredentialType { - CredentialType::Voucher - } - - pub fn tx_hash(&self) -> Hash { - self.deposit_tx_hash - } - - pub fn identity_key(&self) -> &identity::PrivateKey { - &self.signing_key - } - - pub fn encryption_key(&self) -> &encryption::PrivateKey { - &self.unused_ed25519 - } -} diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs deleted file mode 100644 index 3192804530..0000000000 --- a/common/credentials/src/coconut/utils.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::IssuanceBandwidthCredential; -use crate::error::Error; -use log::{debug, warn}; -use nym_credentials_interface::{ - aggregate_verification_keys, Signature, SignatureShare, VerificationKey, -}; -use nym_validator_client::client::CoconutApiClient; - -pub fn obtain_aggregate_verification_key( - api_clients: &[CoconutApiClient], -) -> Result { - if api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - - let indices: Vec<_> = api_clients - .iter() - .map(|api_client| api_client.node_id) - .collect(); - let shares: Vec<_> = api_clients - .iter() - .map(|api_client| api_client.verification_key.clone()) - .collect(); - - Ok(aggregate_verification_keys(&shares, Some(&indices))?) -} - -pub async fn obtain_aggregate_signature( - voucher: &IssuanceBandwidthCredential, - coconut_api_clients: &[CoconutApiClient], - threshold: u64, -) -> Result { - if coconut_api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - let mut shares = Vec::with_capacity(coconut_api_clients.len()); - let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?; - - let request = voucher.prepare_for_signing(); - - for coconut_api_client in coconut_api_clients.iter() { - debug!( - "attempting to obtain partial credential from {}", - coconut_api_client.api_client.api_url() - ); - - match voucher - .obtain_partial_bandwidth_voucher_credential( - &coconut_api_client.api_client, - &coconut_api_client.verification_key, - Some(request.clone()), - ) - .await - { - Ok(signature) => { - let share = SignatureShare::new(signature, coconut_api_client.node_id); - shares.push(share) - } - Err(err) => { - warn!( - "failed to obtain partial credential from {}: {err}", - coconut_api_client.api_client.api_url() - ); - } - }; - } - if shares.len() < threshold as usize { - return Err(Error::NotEnoughShares); - } - - voucher.aggregate_signature_shares(&verification_key, &shares) -} - -pub(crate) mod scalar_serde_helper { - use bls12_381::Scalar; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use zeroize::Zeroizing; - - pub fn serialize(scalar: &Scalar, serializer: S) -> Result { - scalar.to_bytes().serialize(serializer) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { - let b = <[u8; 32]>::deserialize(deserializer)?; - - // make sure the bytes get zeroed - let bytes = Zeroizing::new(b); - - let maybe_scalar: Option = Scalar::from_bytes(&bytes).into(); - maybe_scalar.ok_or(serde::de::Error::custom( - "did not construct a valid bls12-381 scalar out of the provided bytes", - )) - } -} diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs new file mode 100644 index 0000000000..5cbdea45bf --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -0,0 +1,239 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::IssuedTicketBook; +use crate::ecash::bandwidth::CredentialSigningData; +use crate::ecash::utils::cred_exp_date; +use crate::error::Error; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials_interface::{ + aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request, + BlindedSignature, KeyPairUser, PartialWallet, VerificationKeyAuth, WalletSignatures, + WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime}; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use time::Date; + +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +pub use nym_validator_client::nyxd::{Coin, Hash}; + +#[derive(Serialize, Deserialize)] +pub struct IssuanceTicketBook { + /// the id of the associated deposit + deposit_id: DepositId, + + /// base58 encoded private key ensuring the depositer requested these attributes + signing_key: identity::PrivateKey, + + /// ecash keypair related to the credential + ecash_keypair: KeyPairUser, + + /// expiration_date of that credential + expiration_date: Date, +} + +impl IssuanceTicketBook { + pub fn new>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + ) -> Self { + //this expiration date will get fed to the ecash library, force midnight to be set + Self::new_with_expiration( + deposit_id, + identifier, + signing_key, + ecash_default_expiration_date(), + ) + } + + pub fn new_with_expiration>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + expiration_date: Date, + ) -> Self { + let ecash_keypair = generate_keypair_user_from_seed(identifier); + IssuanceTicketBook { + deposit_id, + signing_key, + ecash_keypair, + expiration_date, + } + } + + pub fn ecash_pubkey_bs58(&self) -> String { + use nym_credentials_interface::Base58; + + self.ecash_keypair.public_key().to_bs58() + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn request_plaintext(request: &WithdrawalRequest, deposit_id: DepositId) -> Vec { + let mut message = request.to_bytes(); + message.extend_from_slice(&deposit_id.to_be_bytes()); + message + } + + fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { + let message = Self::request_plaintext(&signing_request.withdrawal_request, self.deposit_id); + self.signing_key.sign(message) + } + + pub fn create_blind_sign_request_body( + &self, + signing_request: &CredentialSigningData, + ) -> BlindSignRequestBody { + let request_signature = self.request_signature(signing_request); + + BlindSignRequestBody::new( + signing_request.withdrawal_request.clone(), + self.deposit_id, + request_signature, + signing_request.ecash_pub_key.clone(), + signing_request.expiration_date, + ) + } + + pub async fn obtain_blinded_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + request_body: &BlindSignRequestBody, + ) -> Result { + let server_response = client.blind_sign(request_body).await?; + Ok(server_response.blinded_signature) + } + + pub fn deposit_id(&self) -> DepositId { + self.deposit_id + } + + pub fn identity_key(&self) -> &identity::PrivateKey { + &self.signing_key + } + + pub fn check_expiration_date(&self) -> bool { + self.expiration_date != cred_exp_date().ecash_date() + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn prepare_for_signing(&self) -> CredentialSigningData { + // safety: the creation of the request can only fail if one provided invalid parameters + // and we created then specific to this type of the credential so the unwrap is fine + let (withdrawal_request, request_info) = withdrawal_request( + self.ecash_keypair.secret_key(), + self.expiration_date.ecash_unix_timestamp(), + ) + .unwrap(); + + CredentialSigningData { + withdrawal_request, + request_info, + ecash_pub_key: self.ecash_keypair.public_key(), + expiration_date: self.expiration_date, + } + } + + pub fn unblind_signature( + &self, + validator_vk: &VerificationKeyAuth, + signing_data: &CredentialSigningData, + blinded_signature: BlindedSignature, + signer_index: u64, + ) -> Result { + let unblinded_signature = issue_verify( + validator_vk, + self.ecash_keypair.secret_key(), + &blinded_signature, + &signing_data.request_info, + signer_index, + )?; + + Ok(unblinded_signature) + } + + // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers + pub async fn obtain_partial_bandwidth_voucher_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + signer_index: u64, + validator_vk: &VerificationKeyAuth, + signing_data: CredentialSigningData, + ) -> Result { + // We need signing data, because they will be used at the aggregation step + + let request = self.create_blind_sign_request_body(&signing_data); + let blinded_signature = self.obtain_blinded_credential(client, &request).await?; + self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index) + } + + // pub fn unchecked_aggregate_signature_shares( + // &self, + // shares: &[SignatureShare], + // ) -> Result { + // aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) + // } + + pub fn aggregate_signature_shares( + &self, + verification_key: &VerificationKeyAuth, + shares: &[PartialWallet], + signing_data: CredentialSigningData, + ) -> Result { + aggregate_wallets( + verification_key, + self.ecash_keypair.secret_key(), + shares, + &signing_data.request_info, + ) + .map_err(Error::SignatureAggregationError) + .map(|w| w.into_wallet_signatures()) + } + + // also drops self after the conversion + pub fn into_issued_ticketbook( + self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + self.to_issued_ticketbook(wallet, epoch_id) + } + + pub fn to_issued_ticketbook( + &self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + IssuedTicketBook::new( + wallet, + epoch_id, + self.ecash_keypair.secret_key().clone(), + self.expiration_date, + ) + } +} + +impl VersionedSerialise for IssuanceTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} diff --git a/common/credentials/src/ecash/bandwidth/issued.rs b/common/credentials/src/ecash/bandwidth/issued.rs new file mode 100644 index 0000000000..5d20af3ee8 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issued.rs @@ -0,0 +1,174 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +use crate::ecash::bandwidth::CredentialSpendingData; +use crate::ecash::utils::ecash_today; +use crate::error::Error; +use nym_credentials_interface::{ + CoinIndexSignature, ExpirationDateSignature, PayInfo, SecretKeyUser, VerificationKeyAuth, + Wallet, WalletSignatures, +}; +use nym_ecash_time::EcashTime; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use time::Date; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; + +// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned +#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct IssuedTicketBook { + /// the underlying wallet signatures + signatures_wallet: WalletSignatures, + + /// the counter indicating how many tickets have been spent so far + spent_tickets: u64, + + /// Specifies the (DKG) epoch id when this credential has been issued + epoch_id: EpochId, + + /// secret ecash key used to generate this wallet + ecash_secret_key: SecretKeyUser, + + /// expiration_date for easier discarding + #[zeroize(skip)] + expiration_date: Date, +} + +impl IssuedTicketBook { + pub fn new( + wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + expiration_date: Date, + ) -> Self { + IssuedTicketBook { + signatures_wallet: wallet, + spent_tickets: 0, + epoch_id, + ecash_secret_key, + expiration_date, + } + } + + pub fn from_parts( + signatures_wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + expiration_date: Date, + spent_tickets: u64, + ) -> Self { + IssuedTicketBook { + signatures_wallet, + spent_tickets, + epoch_id, + ecash_secret_key, + expiration_date, + } + } + + pub fn update_spent_tickets(&mut self, spent_tickets: u64) { + self.spent_tickets = spent_tickets + } + + pub fn epoch_id(&self) -> EpochId { + self.epoch_id + } + + pub fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn params_total_tickets(&self) -> u64 { + nym_credentials_interface::ecash_parameters().get_total_coins() + } + + pub fn spent_tickets(&self) -> u64 { + self.spent_tickets + } + + pub fn wallet(&self) -> &WalletSignatures { + &self.signatures_wallet + } + + pub fn prepare_for_spending( + &mut self, + verification_key: &VerificationKeyAuth, + pay_info: PayInfo, + coin_indices_signatures: &[BI], + expiration_date_signatures: &[BE], + tickets_to_spend: u64, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + let params = nym_credentials_interface::ecash_parameters(); + let spend_date = ecash_today(); + + // make sure we still have enough tickets to spend + Wallet::ensure_allowance(params, self.spent_tickets, tickets_to_spend)?; + + let payment = self.signatures_wallet.spend( + params, + verification_key, + &self.ecash_secret_key, + &pay_info, + self.spent_tickets, + tickets_to_spend, + expiration_date_signatures, + coin_indices_signatures, + spend_date.ecash_unix_timestamp(), + )?; + + self.spent_tickets += tickets_to_spend; + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date: spend_date.ecash_date(), + epoch_id: self.epoch_id, + }) + } +} + +impl VersionedSerialise for IssuedTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn credential_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/credentials/src/ecash/bandwidth/mod.rs b/common/credentials/src/ecash/bandwidth/mod.rs new file mode 100644 index 0000000000..ce75507df6 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use issuance::IssuanceTicketBook; +pub use issued::IssuedTicketBook; +pub use nym_credentials_interface::{CredentialSigningData, CredentialSpendingData}; + +pub mod issuance; +pub mod issued; +pub mod serialiser; diff --git a/common/credentials/src/ecash/bandwidth/serialiser.rs b/common/credentials/src/ecash/bandwidth/serialiser.rs new file mode 100644 index 0000000000..acb7629002 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/serialiser.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use crate::Error; +use bincode::Options; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::marker::PhantomData; + +pub struct VersionSerialised { + pub data: Vec, + pub revision: u8, + + // still wondering if there's any point in having the phantom in here + _phantom: PhantomData, +} + +pub trait VersionedSerialise { + const CURRENT_SERIALISATION_REVISION: u8; + + fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + // implicitly always uses current revision + fn pack(&self) -> VersionSerialised + where + Self: Serialize, + { + let data = make_current_storable_bincode_serializer() + .serialize(self) + .expect("serialisation failure"); + + VersionSerialised { + data, + revision: Self::CURRENT_SERIALISATION_REVISION, + _phantom: Default::default(), + } + } + + fn try_unpack_current(b: &[u8]) -> Result + where + Self: DeserializeOwned, + { + make_current_storable_bincode_serializer() + .deserialize(b) + .map_err(|source| Error::SerializationFailure { + source, + revision: Self::CURRENT_SERIALISATION_REVISION, + }) + } + + // this is up to whoever implements the trait to provide function implementation, + // as they might have to have different implementations per revision + fn try_unpack(b: &[u8], revision: impl Into>) -> Result + where + Self: DeserializeOwned; +} + +fn make_current_storable_bincode_serializer() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credentials/src/coconut/mod.rs b/common/credentials/src/ecash/mod.rs similarity index 100% rename from common/credentials/src/coconut/mod.rs rename to common/credentials/src/ecash/mod.rs diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs new file mode 100644 index 0000000000..38ee31f2c1 --- /dev/null +++ b/common/credentials/src/ecash/utils.rs @@ -0,0 +1,210 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::IssuanceTicketBook; +use crate::error::Error; +use log::{debug, warn}; +use nym_credentials_interface::{ + aggregate_expiration_signatures, aggregate_indices_signatures, Base58, CoinIndexSignature, + CoinIndexSignatureShare, ExpirationDateSignature, ExpirationDateSignatureShare, + VerificationKeyAuth, WalletSignatures, +}; +use nym_validator_client::client::EcashApiClient; + +// so we wouldn't break all the existing imports +pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime}; + +pub fn aggregate_verification_keys( + api_clients: &[EcashApiClient], +) -> Result { + if api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let indices: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); + let shares: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); + + Ok(nym_credentials_interface::aggregate_verification_keys( + &shares, + Some(&indices), + )?) +} + +pub fn obtain_aggregated_verification_key( + _api_clients: &[EcashApiClient], +) -> Result { + // TODO: + // let total = api_clients.len(); + // let mut rng = thread_rng(); + // let indices = sample(&mut rng, total, total); + // for index in indices { + // // randomly try apis until we succeed + // // if let Ok(res) = api_clients[index].api_client.get_aggregated_verification_key().await { + // // // + // // } + // } + todo!() +} + +pub async fn obtain_expiration_date_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + let expiration_date = cred_exp_date().unix_timestamp() as u64; + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_expiration_date_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(ExpirationDateSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this already takes care of partial signatures validation + aggregate_expiration_signatures(verification_key, expiration_date, &signatures_shares) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_coin_indices_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_coin_indices_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(CoinIndexSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this takes care of validating partial signatures + aggregate_indices_signatures( + nym_credentials_interface::ecash_parameters(), + verification_key, + &signatures_shares, + ) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_aggregate_wallet( + voucher: &IssuanceTicketBook, + ecash_api_clients: &[EcashApiClient], + threshold: u64, +) -> Result { + if ecash_api_clients.len() < threshold as usize { + return Err(Error::NoValidatorsAvailable); + } + let verification_key = aggregate_verification_keys(ecash_api_clients)?; + + let request = voucher.prepare_for_signing(); + + let mut wallets = Vec::with_capacity(ecash_api_clients.len()); + + // TODO: optimise and query just threshold + for ecash_api_client in ecash_api_clients.iter() { + debug!( + "attempting to obtain partial credential from {}", + ecash_api_client.api_client.api_url() + ); + + match voucher + .obtain_partial_bandwidth_voucher_credential( + &ecash_api_client.api_client, + ecash_api_client.node_id, + &ecash_api_client.verification_key, + request.clone(), + ) + .await + { + Ok(wallet) => wallets.push(wallet), + Err(err) => { + warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",); + } + }; + } + if wallets.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + voucher.aggregate_signature_shares(&verification_key, &wallets, request) +} + +pub fn signatures_to_string(sigs: &[B]) -> String +where + B: Base58, +{ + sigs.iter() + .map(|sig| sig.to_bs58()) + .collect::>() + .join(",") +} + +pub fn signatures_from_string(bs58_sigs: String) -> Result, Error> +where + B: Base58, +{ + bs58_sigs + .split(',') + .map(B::try_from_bs58) + .collect::, _>>() + .map_err(Error::CompactEcashError) +} diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index 6487d23870..c1f99608ee 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -1,11 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CoconutError; +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_validator_client::ValidatorClientError; - -use crate::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use thiserror::Error; #[derive(Debug, Error)] @@ -26,14 +25,11 @@ pub enum Error { #[error("unknown credential serializatio revision {revision}. the current (and max supported) version is {CURRENT_SERIALIZATION_REVISION}")] UnknownSerializationRevision { revision: u8 }, - #[error("The detailed description is yet to be determined")] - BandwidthCredentialError, - #[error("Could not contact any validator")] NoValidatorsAvailable, - #[error("Ran into a coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ran into a Compact ecash error - {0}")] + CompactEcashError(#[from] CompactEcashError), #[error("Ran into a validator client error - {0}")] ValidatorClientError(#[from] ValidatorClientError), @@ -51,7 +47,7 @@ pub enum Error { NotEnoughShares, #[error("Could not aggregate signature shares - {0}. Try again using the recovery command")] - SignatureAggregationError(CoconutError), + SignatureAggregationError(CompactEcashError), #[error("Could not deserialize bandwidth voucher - {0}")] BandwidthVoucherDeserializationError(String), @@ -59,9 +55,6 @@ pub enum Error { #[error("the provided issuance data wasn't prepared for a bandwidth voucher")] NotABandwdithVoucher, - #[error("the provided issuance data wasn't prepared for a free pass")] - NotAFreePass, - #[error("failed to create a secp256k1 signature")] Secp256k1SignFailure, } diff --git a/common/credentials/src/lib.rs b/common/credentials/src/lib.rs index c06eeb0a05..7f2c3ef2d4 100644 --- a/common/credentials/src/lib.rs +++ b/common/credentials/src/lib.rs @@ -1,12 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod coconut; +pub mod ecash; pub mod error; -pub use coconut::bandwidth::{ - CredentialSigningData, CredentialSpendingData, IssuanceBandwidthCredential, - IssuedBandwidthCredential, +pub use ecash::bandwidth::{ + CredentialSigningData, CredentialSpendingData, IssuanceTicketBook, IssuedTicketBook, }; -pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; +pub use ecash::utils::{aggregate_verification_keys, obtain_aggregate_wallet}; pub use error::Error; diff --git a/common/crypto/src/shared_key.rs b/common/crypto/src/shared_key.rs index bcc707489b..f90e99feb2 100644 --- a/common/crypto/src/shared_key.rs +++ b/common/crypto/src/shared_key.rs @@ -3,12 +3,13 @@ use crate::asymmetric::encryption; use crate::hkdf; -#[cfg(feature = "rand")] -use cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use cipher::{Key, KeyIvInit, StreamCipher}; use digest::crypto_common::BlockSizeUser; use digest::Digest; +#[cfg(feature = "rand")] +use rand::{CryptoRng, RngCore}; + /// Generate an ephemeral encryption keypair and perform diffie-hellman to establish /// shared key with the remote. #[cfg(feature = "rand")] diff --git a/common/ecash-double-spending/Cargo.toml b/common/ecash-double-spending/Cargo.toml new file mode 100644 index 0000000000..feb237f5d6 --- /dev/null +++ b/common/ecash-double-spending/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-ecash-double-spending" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bit-vec = { workspace = true } +bloomfilter = { workspace = true } +nym-network-defaults = { path = "../network-defaults" } diff --git a/common/ecash-double-spending/src/lib.rs b/common/ecash-double-spending/src/lib.rs new file mode 100644 index 0000000000..78f24dba38 --- /dev/null +++ b/common/ecash-double-spending/src/lib.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bit_vec::BitVec; +use bloomfilter::Bloom; +use nym_network_defaults::{BloomfilterParameters, ECASH_DS_BLOOMFILTER_PARAMS}; + +pub struct DoubleSpendingFilter { + params: BloomfilterParameters, + inner: Bloom>, +} + +impl Default for DoubleSpendingFilter { + fn default() -> Self { + DoubleSpendingFilter::new_empty_ecash() + } +} + +pub fn bloom_from_params(params: &BloomfilterParameters, bitvec: BitVec) -> Bloom> { + assert_eq!(params.bitmap_size, bitvec.len() as u64); + + Bloom::from_bit_vec( + bitvec, + params.bitmap_size, + params.num_hashes, + params.sip_keys, + ) +} + +impl DoubleSpendingFilter { + pub fn new_empty(params: BloomfilterParameters) -> Self { + let bitvec = BitVec::from_elem(params.bitmap_size as usize, false); + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, bitvec), + params, + } + } + + pub fn params(&self) -> BloomfilterParameters { + self.params + } + + pub fn rebuild(&self) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(self.params) + } + + pub fn reset(&mut self) { + self.inner.clear() + } + + pub fn new_empty_ecash() -> Self { + DoubleSpendingFilter::new_empty(ECASH_DS_BLOOMFILTER_PARAMS) + } + + pub fn builder(params: BloomfilterParameters) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(params) + } + + pub fn from_bytes(params: BloomfilterParameters, bitmap: &[u8]) -> Self { + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, BitVec::from_bytes(bitmap)), + params, + } + } + + pub fn replace_bitvec(&mut self, new: BitVec) { + self.inner = bloom_from_params(&self.params, new) + } + + pub fn dump_bitmap(&self) -> Vec { + self.inner.bitmap() + } + + pub fn set(&mut self, b: &Vec) { + self.inner.set(b); + } + + pub fn check(&self, b: &Vec) -> bool { + self.inner.check(b) + } +} + +pub struct DoubleSpendingFilterBuilder { + params: BloomfilterParameters, + bit_vec_builder: Option, +} + +impl DoubleSpendingFilterBuilder { + pub fn new(params: BloomfilterParameters) -> Self { + DoubleSpendingFilterBuilder { + params, + bit_vec_builder: None, + } + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + match &mut self.bit_vec_builder { + None => { + self.bit_vec_builder = Some(BitVecBuilder::new(b)); + true + } + Some(builder) => builder.add_bytes(b), + } + } + + pub fn build(self) -> DoubleSpendingFilter { + match self.bit_vec_builder { + None => DoubleSpendingFilter::new_empty(self.params), + Some(builder) => DoubleSpendingFilter { + inner: bloom_from_params(&self.params, builder.finish()), + params: self.params, + }, + } + } +} + +pub struct BitVecBuilder(BitVec); + +impl BitVecBuilder { + pub fn new(initial_bitmap: &[u8]) -> Self { + BitVecBuilder(BitVec::from_bytes(initial_bitmap)) + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + let add = BitVec::from_bytes(b); + if self.0.len() != add.len() { + return false; + } + self.0.or(&add); + true + } + + pub fn finish(self) -> BitVec { + self.0 + } +} diff --git a/common/ecash-time/Cargo.toml b/common/ecash-time/Cargo.toml new file mode 100644 index 0000000000..4eaee90438 --- /dev/null +++ b/common/ecash-time/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nym-ecash-time" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +time.workspace = true + +nym-compact-ecash = { path = "../nym_offline_compact_ecash", optional = true } + +[features] +expiration = ["nym-compact-ecash"] \ No newline at end of file diff --git a/common/ecash-time/src/lib.rs b/common/ecash-time/src/lib.rs new file mode 100644 index 0000000000..e6ac459508 --- /dev/null +++ b/common/ecash-time/src/lib.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::{Duration, PrimitiveDateTime, Time}; + +pub use time::{Date, OffsetDateTime}; + +pub trait EcashTime { + fn ecash_unix_timestamp(&self) -> u64 { + let ts = self.ecash_datetime().unix_timestamp(); + + // just panic on pre-1970 timestamps... + assert!(ts > 0); + + ts as u64 + } + + fn ecash_date(&self) -> Date { + self.ecash_datetime().date() + } + + fn ecash_datetime(&self) -> OffsetDateTime; +} + +impl EcashTime for OffsetDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.replace_time(Time::MIDNIGHT) + } +} + +impl EcashTime for PrimitiveDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.assume_utc().ecash_datetime() + } +} + +impl EcashTime for Date { + fn ecash_datetime(&self) -> OffsetDateTime { + OffsetDateTime::new_utc(*self, Time::MIDNIGHT) + } +} + +pub fn ecash_today() -> OffsetDateTime { + OffsetDateTime::now_utc().ecash_datetime() +} + +pub fn ecash_today_date() -> Date { + ecash_today().ecash_date() +} + +// no point in supporting more than i8 variance +pub fn ecash_date_offset(offset: i8) -> OffsetDateTime { + let today = ecash_today(); + + let day = today + Duration::days(offset as i64); + + // make sure to correct the time in case of DST + day.replace_time(Time::MIDNIGHT) +} + +#[cfg(feature = "expiration")] +pub fn cred_exp_date() -> OffsetDateTime { + //count today as well + ecash_date_offset(nym_compact_ecash::constants::CRED_VALIDITY_PERIOD_DAYS as i8 - 1) + // ecash_today() + Duration::days(constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1) +} + +#[cfg(feature = "expiration")] +pub fn ecash_default_expiration_date() -> Date { + cred_exp_date().ecash_date() +} diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 4de02982ec..562f12559c 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -14,6 +14,6 @@ hex-literal = { workspace = true } log = { workspace = true } once_cell = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } -serde = { workspace = true, features = ["derive"]} +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } url = { workspace = true } diff --git a/common/network-defaults/src/ecash.rs b/common/network-defaults/src/ecash.rs new file mode 100644 index 0000000000..944abdff27 --- /dev/null +++ b/common/network-defaults/src/ecash.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// How much bandwidth (in bytes) one ticket can buy +pub const TICKET_BANDWIDTH_VALUE: u64 = 100 * 1024 * 1024; // 100 MB + +///Tickets to spend per payment +pub const SPEND_TICKETS: u64 = 1; +/// Threshold for claiming more bandwidth: 1 MB +pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; + +// Constants for bloom filter for double spending detection +//Chosen for FP of +//Calculator at https://hur.st/bloomfilter/ +pub const ECASH_DS_BLOOMFILTER_PARAMS: BloomfilterParameters = BloomfilterParameters { + num_hashes: 13, + bitmap_size: 250_000, + sip_keys: [ + (12345678910111213141, 1415926535897932384), + (7182818284590452353, 3571113171923293137), + ], +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct BloomfilterParameters { + pub num_hashes: u32, + pub bitmap_size: u64, + pub sip_keys: [(u64, u64); 2], +} + +impl BloomfilterParameters { + pub const fn byte_size(&self) -> u64 { + self.bitmap_size / 8 + } + + pub const fn default_ecash() -> Self { + ECASH_DS_BLOOMFILTER_PARAMS + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 45e1a51285..79ddf5ac09 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -13,9 +13,12 @@ use std::{ }; use url::Url; +pub mod ecash; pub mod mainnet; pub mod var_names; +pub use ecash::*; + #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] pub struct ChainDetails { pub bech32_account_prefix: String, @@ -27,7 +30,7 @@ pub struct ChainDetails { pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, @@ -121,9 +124,7 @@ impl NymNetworkDetails { )) .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS)) .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS)) - .with_coconut_bandwidth_contract(get_optional_env( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - )) + .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS)) .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) @@ -148,9 +149,7 @@ impl NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str( @@ -239,8 +238,8 @@ impl NymNetworkDetails { } #[must_use] - pub fn with_coconut_bandwidth_contract>(mut self, contract: Option) -> Self { - self.contracts.coconut_bandwidth_contract_address = contract.map(Into::into); + pub fn with_ecash_contract>(mut self, contract: Option) -> Self { + self.contracts.ecash_contract_address = contract.map(Into::into); self } @@ -438,19 +437,6 @@ pub fn setup_env>(config_env_file: Option

) { } } -/// How much bandwidth (in bytes) one token can buy -pub const BYTES_PER_UTOKEN: u64 = 1024; -/// How much bandwidth (in bytes) one freepass provides -pub const BYTES_PER_FREEPASS: u64 = 1024 * 1024 * 1024; // 1GB -/// Threshold for claiming more bandwidth: 1 MB -pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; -/// How many tokens should be burned to buy bandwidth -pub const TOKENS_TO_BURN: u64 = 1; -/// How many ERC20 utokens should be burned to buy bandwidth -pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000; -/// Default bandwidth (in bytes) that we try to buy -pub const BANDWIDTH_VALUE: u64 = UTOKENS_TO_BURN * BYTES_PER_UTOKEN; - /// Defaults Cosmos Hub/ATOM path pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; // as set by validators in their configs diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index c7b1bec0be..451fa8c1d6 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -17,7 +17,7 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = ""; +pub const ECASH_CONTRACT_ADDRESS: &str = ""; pub const GROUP_CONTRACT_ADDRESS: &str = "n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr"; pub const MULTISIG_CONTRACT_ADDRESS: &str = @@ -93,10 +93,7 @@ pub fn export_to_env() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, @@ -135,10 +132,7 @@ pub fn export_to_env_if_not_set() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_conditionally_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_conditionally_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_conditionally_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_conditionally_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index f8a85ef45a..4831e82122 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -14,7 +14,7 @@ pub const STAKE_DENOM_DISPLAY: &str = "STAKE_DENOM_DISPLAY"; pub const DENOMS_EXPONENT: &str = "DENOMS_EXPONENT"; pub const MIXNET_CONTRACT_ADDRESS: &str = "MIXNET_CONTRACT_ADDRESS"; pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; +pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS"; pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; diff --git a/common/nym-id/src/error.rs b/common/nym-id/src/error.rs index 2edc03fd0e..780658944e 100644 --- a/common/nym-id/src/error.rs +++ b/common/nym-id/src/error.rs @@ -3,7 +3,7 @@ use std::error::Error; use thiserror::Error; -use time::OffsetDateTime; +use time::Date; #[derive(Debug, Error)] pub enum NymIdError { @@ -11,7 +11,7 @@ pub enum NymIdError { CredentialDeserializationFailure { source: nym_credentials::Error }, #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + ExpiredCredentialImport { expiration: Date }, #[error("failed to store credential in the provided store: {source}")] StorageError { diff --git a/common/nym-id/src/import_credential.rs b/common/nym-id/src/import_credential.rs index 0d3a9abce4..867db15749 100644 --- a/common/nym-id/src/import_credential.rs +++ b/common/nym-id/src/import_credential.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::NymIdError; -use nym_credential_storage::models::StorableIssuedCredential; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::IssuedTicketBook; use time::OffsetDateTime; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -14,7 +14,7 @@ pub async fn import_credential( credentials_store: S, raw_credential: Vec, credential_version: impl Into>, -) -> Result, NymIdError> +) -> Result where S: Storage, ::StorageError: Send + Sync + 'static, @@ -22,54 +22,28 @@ where let raw_credential = Zeroizing::new(raw_credential); // note: the type itself implements ZeroizeOnDrop - let credential = IssuedBandwidthCredential::try_unpack(&raw_credential, credential_version) + let ticketbook = IssuedTicketBook::try_unpack(&raw_credential, credential_version) .map_err(|source| NymIdError::CredentialDeserializationFailure { source })?; debug!( - "attempting to import credential of type {}", - credential.typ() + "attempting to import credential with expiration date at {}", + ticketbook.expiration_date() ); - let expiry_date = match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - debug!("with value of {}", voucher_info.value()); - None - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - warn!("the free pass has already expired!"); + if ticketbook.expired() { + warn!("the credential has already expired!"); - // technically we can import it, but the gateway will just reject it so what's the point - return Err(NymIdError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } else { - Some(freepass_info.expiry_date()) - } - } - }; - - // SAFETY: - // for the epoch to run over u32::MAX, we'd have to advance it for few centuries every block... - // the alternative is a very particularly malformed serialized data, but at that point blowing up is the right call - // because we can't rely on it anyway - #[allow(clippy::expect_used)] - let storable = StorableIssuedCredential { - serialization_revision: credential.current_serialization_revision(), - credential_data: &raw_credential, - credential_type: credential.typ().to_string(), - epoch_id: credential - .epoch_id() - .try_into() - .expect("our epoch is has run over u32::MAX!"), - }; + // technically we can import it, but the gateway will just reject it so what's the point + return Err(NymIdError::ExpiredCredentialImport { + expiration: ticketbook.expiration_date(), + }); + } credentials_store - .insert_issued_credential(storable) + .insert_issued_ticketbook(&ticketbook) .await .map_err(|source| NymIdError::StorageError { source: Box::new(source), })?; - Ok(expiry_date) + Ok(ticketbook.expiration_date().ecash_datetime()) } diff --git a/common/nym_offline_compact_ecash/Cargo.toml b/common/nym_offline_compact_ecash/Cargo.toml new file mode 100644 index 0000000000..d296effaca --- /dev/null +++ b/common/nym_offline_compact_ecash/Cargo.toml @@ -0,0 +1,64 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-compact-ecash" +version = "0.1.0" +authors = ["Ania Piotrowska "] +edition = "2021" +license = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental", "zeroize", "experimental_serde"] } +bincode.workspace = true +cfg-if.workspace = true +itertools = "0.12.1" +digest = "0.9" +rand = { workspace = true } +thiserror = { workspace = true } +sha2 = "0.9" +bs58 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +rayon = { version = "1.5.0", optional = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } +ff = { workspace = true } +group = { workspace = true } + +nym-pemstore = { path = "../pemstore" } + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + + +[[bench]] +name = "benchmarks_group_operations" +path = "benches/benchmarks_group_operations.rs" +harness = false + +[[bench]] +name = "benchmarks_expiration_date_signatures" +path = "benches/benchmarks_expiration_date_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_coin_indices_signatures" +path = "benches/benchmarks_coin_indices_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_ecash_e2e" +path = "benches/benchmarks_ecash_e2e.rs" +harness = false + +[features] +# for 1000 coin indices it goes from ~50ms to ~400ms, but we only have to issue them once per epoch +# so it's not really worth it +par_signing = ["rayon"] + +# for this one there's an argument for it since the verification of 1000 indices can take over 6s, +# but given it's not done very frequently, it shouldn't be too much of a problem +# furthermore, we can't and shouldn't dedicate the entire nym-api CPU just for verification, +# but this feature might potentially be desirable for clients. +par_verify = ["rayon"] \ No newline at end of file diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs new file mode 100644 index 0000000000..6b7628814d --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs @@ -0,0 +1,116 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, verify_coin_indices_signatures, + CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_coin_signing(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for coin indices + group.bench_function( + &format!( + "[IssuingAuthority] sign_coin_indices_L_{}", + params.get_total_coins() + ), + |b| b.iter(|| sign_coin_indices(¶ms, &verification_key, sk_i_auth)), + ); + + // CLIENT: verify the correctness of the (partial)) signatures for coin indices + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures).is_ok() + ); + group.bench_function( + &format!( + "[Client] verify_coin_indices_signatures_L_{}", + params.get_total_coins() + ), + |b| { + b.iter(|| { + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + }) + }, + ); +} + +fn bench_aggregate_coin_indices_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed coin indices + group.bench_function( + &format!( + "[Client] aggregate_coin_indices_signatures_from_{}_issuing_authorities_L_{}", + authorities_keypairs.len(), + params.get_total_coins(), + ), + |b| b.iter(|| aggregate_indices_signatures(¶ms, &verification_key, &combined_data)), + ); +} + +criterion_group!( + benches, + bench_coin_signing, + bench_aggregate_coin_indices_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs new file mode 100644 index 0000000000..8beefea27e --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs @@ -0,0 +1,283 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use itertools::izip; +use nym_compact_ecash::identify::{identify, IdentifyResult}; +use nym_compact_ecash::scheme::expiration_date_signatures::date_scalar; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, +}; +use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, + VerificationKeyAuth, +}; +use rand::seq::SliceRandom; + +struct BenchCase { + num_authorities: u64, + threshold_p: f32, + ll: u64, + spend_vv: u64, + case_nr_pub_keys: u64, +} + +fn bench_compact_ecash(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-compact-ecash"); + // group.sample_size(300); + // group.measurement_time(Duration::from_secs(1500)); + + let expiration_date = 1703721600; // Dec 28 2023 + let spend_date = 1701907200; // Dec 07 2023 + + let case = BenchCase { + num_authorities: 100, + threshold_p: 0.7, + ll: 1000, + spend_vv: 1, + case_nr_pub_keys: 99, + }; + + // SETUP PHASE and KEY GENERATION + let params = Parameters::new(case.ll); + + let grp = params.grp(); + let user_keypair = generate_keypair_user(); + let threshold = (case.threshold_p * case.num_authorities as f32).round() as u64; + let authorities_keypairs = ttp_keygen(threshold, case.num_authorities).unwrap(); + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let indices: Vec = (1..case.num_authorities + 1).collect(); + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // PRE-GENERATION OF THE EXPORATION DATE SIGNATURES AND THE COIN INDICES SIGNATURES + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // ISSUANCE PHASE + let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + + // CLIENT BENCHMARK: prepare a single withdrawal request + group.bench_function( + &format!( + "[Client] withdrawal_request_{}_authorities_{}_L_{}_threshold", + case.num_authorities, case.ll, case.threshold_p, + ), + |b| b.iter(|| withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap()), + ); + + // ISSUING AUTHRORITY BENCHMARK: Benchmark the issue function + // called by an authority to issue a blind signature on a partial wallet + let mut rng = rand::thread_rng(); + let keypair = authorities_keypairs.choose(&mut rng).unwrap(); + group.bench_function( + &format!( + "[Issuing Authority] issue_partial_wallet_with_L_{}", + case.ll, + ), + |b| { + b.iter(|| { + issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ) + }) + }, + ); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in &authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + // CLIENT BENCHMARK: verify the issued partial wallet + let w = wallet_blinded_signatures.first().unwrap(); + let vk = verification_keys_auth.first().unwrap(); + group.bench_function( + &format!("[Client] issue_verify_a_partial_wallet_with_L_{}", case.ll,), + |b| b.iter(|| issue_verify(vk, user_keypair.secret_key(), w, &req_info, 1).unwrap()), + ); + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // CLIENT BENCHMARK: aggregating all partial wallets + group.bench_function( + &format!( + "[Client] aggregate_wallets_with_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap() + }) + }, + ); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + let wallet_clone = aggr_wallet.clone(); + + // SPENDING PHASE + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + // CLIENT BENCHMARK: spend a single coin from the wallet + group.bench_function( + &format!( + "[Client] spend_a_single_coin_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap() + }) + }, + ); + + let payment = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // MERCHANT BENCHMARK: verify whether the submitted payment is legit + group.bench_function( + &format!( + "[Merchant] spend_verify_of_a_single_payment_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + payment + .spend_verify(&verification_key, &pay_info, date_scalar(spend_date)) + .unwrap() + }) + }, + ); + + // BENCHMARK IDENTIFICATION + // Let's generate a double spending payment + + // let's reverse the spending counter in the wallet to create a double spending payment + let mut aggr_wallet = wallet_clone; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..case.case_nr_pub_keys { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser::from_bytes(&sk.to_bytes()).unwrap(); + let pk_user = sk_user.public_key(); + public_keys.push(pk_user); + } + public_keys.push(user_keypair.public_key()); + + // MERCHANT BENCHMARK: identify double spending + group.bench_function( + &format!( + "[Merchant] identify_L_{}_threshold_{}_spend_vv_{}_pks_{}", + case.ll, + case.threshold_p, + case.spend_vv, + public_keys.len() + ), + |b| b.iter(|| identify(&payment, &payment2, pay_info, pay_info2)), + ); + let identify_result = identify(&payment, &payment2, pay_info, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); +} + +criterion_group!(benches, bench_compact_ecash); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs new file mode 100644 index 0000000000..8c049a96a5 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs @@ -0,0 +1,102 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::constants; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, verify_valid_dates_signatures, + ExpirationDateSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_partial_sign_expiration_date(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-expiration-date"); + let expiration_date = 1703183958; + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for a given expiration date + group.bench_function( + &format!( + "[IssuingAuthority] sign_expiration_date_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| sign_expiration_date(sk_i_auth, expiration_date)), + ); + + // CLIENT: verify the correctness of the set of (partial) signatures for a given expiration date + assert!(verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok()); + group.bench_function( + &format!( + "[Client] verify_valid_dates_signatures_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date)), + ); +} + +fn bench_aggregate_expiration_date_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-verify-expiration-date-signatures"); + let expiration_date = 1703183958; + + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk, expiration_date).unwrap(); + partial_signatures.push(sign); + } + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed valid dates + group.bench_function( + &format!( + "[Client] aggregate_expiration_signatures_from_{}_issuing_authorities_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, authorities_keypairs.len(), + ), + |b| { + b.iter(|| { + aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + }) + }, + ); +} + +criterion_group!( + benches, + bench_partial_sign_expiration_date, + bench_aggregate_expiration_date_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs new file mode 100644 index 0000000000..73c74472e7 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs @@ -0,0 +1,189 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Neg; +use std::time::Duration; + +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, Scalar, +}; +use criterion::{criterion_group, criterion_main, Criterion}; +use ff::Field; +use group::{Curve, Group}; +use nym_compact_ecash::utils::check_bilinear_pairing; + +#[allow(unused)] +fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); + let gt2 = bls12_381::pairing(g12, g22); + assert_eq!(gt1, gt2) +} + +#[allow(unused)] +fn single_pairing(g11: &G1Affine, g21: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); +} + +#[allow(unused)] +fn exponent_in_g1(g1: G1Projective, r: Scalar) { + let g11 = (g1 * r); +} + +#[allow(unused)] +fn exponent_in_g2(g2: G2Projective, r: Scalar) { + let g22 = (g2 * r); +} + +#[allow(unused)] +fn exponent_in_gt(gt: Gt, r: Scalar) { + let gtt = (gt * r); +} + +#[allow(unused)] +fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let miller_loop_result = multi_miller_loop(&[ + (g11, &G2Prepared::from(*g21)), + (&g12.neg(), &G2Prepared::from(*g22)), + ]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn multi_miller_pairing_with_prepared( + g11: &G1Affine, + g21: &G2Prepared, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +// the case of being able to prepare G2 generator +#[allow(unused)] +fn multi_miller_pairing_with_semi_prepared( + g11: &G1Affine, + g21: &G2Affine, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = + multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn bench_group_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("bench_group_operations"); + group.measurement_time(Duration::from_secs(200)); + + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + let gt = bls12_381::pairing(&g11, &g21); + let gen1 = G1Projective::generator(); + let gen2 = G2Projective::generator(); + + group.bench_function("exponent operation in G1", |b| { + b.iter(|| exponent_in_g1(gen1, r)) + }); + + group.bench_function("exponent operation in G2", |b| { + b.iter(|| exponent_in_g2(gen2, r)) + }); + + group.bench_function("exponent operation in Gt", |b| { + b.iter(|| exponent_in_gt(gt, r)) + }); + + group.bench_function("single pairing", |b| b.iter(|| single_pairing(&g11, &g21))); + + group.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + group.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); + + // bench_checking_vk_pairing + // assume key of size 5 + let scalars = [ + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + ]; + let gen1 = G1Affine::generator(); + let gen2_prep = G2Prepared::from(G2Affine::generator()); + + let g1 = scalars + .iter() + .map(|s| G1Affine::generator() * s) + .collect::>(); + let g2 = scalars + .iter() + .map(|s| G2Affine::generator() * s) + .collect::>(); + + group.bench_function("individual pairings", |b| { + b.iter(|| { + for (g1, g2) in g1.iter().zip(g2.iter()) { + let _ = check_bilinear_pairing( + &gen1, + &G2Prepared::from(g2.to_affine()), + &g1.to_affine(), + &gen2_prep, + ); + } + }) + }); + + group.bench_function("miller loop with duplicate elements", |b| { + b.iter(|| { + let mut terms = vec![]; + let neg_g1 = gen1.neg(); + for (g1, g2) in g1.iter().zip(g2.iter()) { + // TODO: optimise refs + terms.push((neg_g1, G2Prepared::from(g2.to_affine()))); + terms.push((g1.to_affine(), gen2_prep.clone())); + } + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + let _: bool = multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into(); + }) + }); +} + +criterion_group!(benches, bench_group_operations); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/src/common_types.rs b/common/nym_offline_compact_ecash/src/common_types.rs new file mode 100644 index 0000000000..6aaf81f673 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/common_types.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::error::Result; +use crate::helpers::{g1_tuple_to_bytes, recover_g1_tuple}; +use bls12_381::{G1Projective, Scalar}; +use serde::{Deserialize, Serialize}; + +pub type SignerIndex = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Signature { + pub(crate) h: G1Projective, + pub(crate) s: G1Projective, +} + +pub type PartialSignature = Signature; + +impl Signature { + pub(crate) fn sig1(&self) -> &G1Projective { + &self.h + } + + pub(crate) fn sig2(&self) -> &G1Projective { + &self.s + } + + /// Function randomises the signature. + /// + /// # Returns + /// + /// A tuple containing the randomised signature and the blinding scalar. + pub fn blind_and_randomise(&self) -> (Signature, Scalar) { + let params = ecash_group_parameters(); + + // Generate random blinding scalars + let r = params.random_scalar(); + let r_prime = params.random_scalar(); + + // Calculate h_prime and s_prime using the random scalars + let h_prime = self.h * r_prime; + let s_prime = (self.s * r_prime) + (h_prime * r); + ( + Signature { + h: h_prime, + s: s_prime, + }, + r, + ) + } + + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.s)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, s) = recover_g1_tuple::(bytes)?; + Ok(Signature { h, s }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct BlindedSignature { + pub(crate) h: G1Projective, + pub(crate) c: G1Projective, +} + +impl BlindedSignature { + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.c)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, c) = recover_g1_tuple::(bytes)?; + Ok(BlindedSignature { h, c }) + } +} + +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 + } +} diff --git a/common/nym_offline_compact_ecash/src/constants.rs b/common/nym_offline_compact_ecash/src/constants.rs new file mode 100644 index 0000000000..fd053bc9d9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/constants.rs @@ -0,0 +1,28 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bls12_381::Scalar; + +pub const PUBLIC_ATTRIBUTES_LEN: usize = 1; //expiration date +pub const PRIVATE_ATTRIBUTES_LEN: usize = 2; //user and wallet secret +pub const ATTRIBUTES_LEN: usize = PUBLIC_ATTRIBUTES_LEN + PRIVATE_ATTRIBUTES_LEN; // number of attributes encoded in a single zk-nym credential + +pub const CRED_VALIDITY_PERIOD_DAYS: u64 = 30; + +pub(crate) const SECONDS_PER_DAY: u64 = 86400; + +/// Total number of tickets in each issued ticket book. +pub const NB_TICKETS: u64 = 1000; + +pub const TYPE_EXP: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMEXP"), + u64::from_le_bytes(*b"IRATIOND"), + u64::from_le_bytes(*b"ATE4llCB"), + u64::from_le_bytes(*b"MEypAxr3"), +]); +pub const TYPE_IDX: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMSIN"), + u64::from_le_bytes(*b"DICESh^7"), + u64::from_le_bytes(*b"gTYbhnap"), + u64::from_le_bytes(*b"*12n5GG6"), +]); diff --git a/common/nym_offline_compact_ecash/src/error.rs b/common/nym_offline_compact_ecash/src/error.rs new file mode 100644 index 0000000000..0d49581ec6 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/error.rs @@ -0,0 +1,125 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum CompactEcashError { + #[error("failed to verify expiration date signatures")] + ExpirationDateSignatureVerification, + + #[error("failed to validate expiration date signatures")] + ExpirationDateSignatureValidity, + + #[error("empty set for aggregation")] + AggregationEmptySet, + + #[error("duplicate indices for aggregation")] + AggregationDuplicateIndices, + + #[error("aggregation verification error")] + AggregationVerification, + + #[error("different element size for aggregation")] + AggregationSizeMismatch, + + #[error("withdrawal request failed to verify")] + WithdrawalRequestVerification, + + #[error("invalid key generation parameters")] + KeygenParameters, + + #[error("signing authority's key is too short")] + KeyTooShort, + + #[error("empty/incomplete set of coordinates for interpolation")] + InterpolationSetSize, + + #[error("issuance verification failed")] + IssuanceVerification, + + #[error("trying to spend more than what's available. Spending : {spending}, available : {remaining}")] + SpendExceedsAllowance { spending: u64, remaining: u64 }, + + #[error("signature failed validity check")] + SpendSignaturesValidity, + + #[error("signature failed verification check")] + SpendSignaturesVerification, + + #[error("duplicate serial number in the payment")] + SpendDuplicateSerialNumber, + + #[error("given spend date is too late")] + SpendDateTooLate, + + #[error("given spend date is too early")] + SpendDateTooEarly, + + #[error("ZK proof failed to verify")] + SpendZKProofVerification, + + #[error("could not decode base 58 string - {0}")] + MalformedString(#[from] bs58::decode::Error), + + #[error("failed to verify coin indices signatures")] + CoinIndicesSignatureVerification, + + #[error("failed to deserialize a {object}")] + DeserializationFailure { object: String }, + + #[error("failed to deserialise {type_name}: {source}")] + BinaryDeserialisationFailure { + type_name: String, + source: bincode::Error, + }, + + #[error( + "deserialization error, expected at least {} bytes, got {}", + min, + actual + )] + DeserializationMinLength { min: usize, actual: usize }, + + #[error("{type_name} deserialization error, expected {expected} bytes, got {actual}")] + DeserializationLengthMismatch { + type_name: String, + expected: usize, + actual: usize, + }, + + #[error("tried to deserialize {object} with bytes of invalid length. Expected {actual} < {target} or {modulus_target} % {modulus} == 0")] + DeserializationInvalidLength { + actual: usize, + target: usize, + modulus_target: usize, + modulus: usize, + object: String, + }, + + #[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")] + ScalarDeserializationFailure, + + #[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")] + G1ProjectiveDeserializationFailure, + + #[error("failed to deserialize G2Projective point from the received bytes - it might not have been canonically encoded")] + G2ProjectiveDeserializationFailure, + + #[error("verification key is invalid for this operation")] + VerificationKeyTooShort, + + #[error("did not provide the sufficient number of coin index signatures")] + InsufficientNumberOfIndexSignatures, + + #[error("did not provide the sufficient number of expiration date signatures")] + InsufficientNumberOfExpirationSignatures, + + //context : This can happen only if the wallet secret `v` was picked such that `v + coin_index + 1 == 0`. + //The chance of this happening is of the order 2^-381 and not failing is waay too much work. + //TLDR: this event can happen, but with probability 0 + #[error("you're one of the most unluck person on your planet and your wallet cannot complete this payment")] + UnluckiestError, +} diff --git a/common/nym_offline_compact_ecash/src/helpers.rs b/common/nym_offline_compact_ecash/src/helpers.rs new file mode 100644 index 0000000000..1fa0a15245 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::try_deserialize_g1_projective; +use crate::CompactEcashError; +use bls12_381::G1Projective; +use group::Curve; +use std::any::{type_name, Any}; + +pub(crate) fn g1_tuple_to_bytes(el: (G1Projective, G1Projective)) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..48].copy_from_slice(&el.0.to_affine().to_compressed()); + bytes[48..].copy_from_slice(&el.1.to_affine().to_compressed()); + bytes +} + +pub(crate) fn recover_g1_tuple( + bytes: &[u8], +) -> crate::error::Result<(G1Projective, G1Projective)> { + if bytes.len() != 96 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: type_name::().into(), + expected: 96, + actual: bytes.len(), + }); + } + //SAFETY : [0..48] into 48 sized array and [48..96] into 48 sized array + #[allow(clippy::unwrap_used)] + let first_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let second_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap(); + + let first = try_deserialize_g1_projective(first_bytes)?; + let second = try_deserialize_g1_projective(second_bytes)?; + + Ok((first, second)) +} diff --git a/common/nym_offline_compact_ecash/src/lib.rs b/common/nym_offline_compact_ecash/src/lib.rs new file mode 100644 index 0000000000..b47ee4b56e --- /dev/null +++ b/common/nym_offline_compact_ecash/src/lib.rs @@ -0,0 +1,62 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +use bls12_381::Scalar; +use std::sync::OnceLock; + +pub use crate::error::CompactEcashError; +pub use crate::traits::Bytable; +pub use bls12_381::G1Projective; +pub use common_types::{BlindedSignature, Signature}; +pub use scheme::aggregation::aggregate_verification_keys; +pub use scheme::aggregation::aggregate_wallets; +pub use scheme::identify; +pub use scheme::keygen::ttp_keygen; +pub use scheme::keygen::{generate_keypair_user, generate_keypair_user_from_seed}; +pub use scheme::keygen::{ + KeyPairAuth, PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth, +}; +pub use scheme::setup; +pub use scheme::withdrawal::issue; +pub use scheme::withdrawal::issue_verify; +pub use scheme::withdrawal::withdrawal_request; +pub use scheme::withdrawal::WithdrawalRequest; +pub use scheme::PartialWallet; +pub use scheme::PayInfo; +pub use setup::GroupParameters; +pub use traits::Base58; + +pub mod common_types; +pub mod constants; +pub mod error; +mod helpers; +mod proofs; +pub mod scheme; +pub mod tests; +mod traits; +pub mod utils; + +pub type Attribute = Scalar; + +pub fn ecash_parameters() -> &'static setup::Parameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::Parameters::new(constants::NB_TICKETS)) +} + +pub fn ecash_group_parameters() -> &'static setup::GroupParameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::GroupParameters::new(constants::ATTRIBUTES_LEN)) +} + +// if anything changes here you MUST correctly increase semver of this library +pub(crate) fn binary_serialiser() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/mod.rs b/common/nym_offline_compact_ecash/src/proofs/mod.rs new file mode 100644 index 0000000000..5133c2bedd --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/mod.rs @@ -0,0 +1,59 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Borrow; + +use bls12_381::Scalar; +use digest::generic_array::typenum::Unsigned; +use digest::Digest; +use sha2::Sha256; + +pub mod proof_spend; +pub mod proof_withdrawal; + +type ChallengeDigest = Sha256; + +/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points. +fn compute_challenge(iter: I) -> Scalar +where + D: Digest, + I: Iterator, + 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_replacement: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar { + witness_replacement - challenge * secret +} + +// note: it's caller's responsibility to ensure witnesses.len() = secrets.len() +fn produce_responses(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec +where + S: Borrow, +{ + debug_assert_eq!(witnesses.len(), secrets.len()); + + witnesses + .iter() + .zip(secrets.iter()) + .map(|(w, x)| produce_response(w, challenge, x.borrow())) + .collect() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs new file mode 100644 index 0000000000..966dd2211c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs @@ -0,0 +1,397 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::VerificationKeyAuth; +use crate::scheme::PayInfo; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use group::GroupEncoding; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SpendInstance { + pub kappa: G2Projective, + pub cc: G1Projective, + pub aa: Vec, + pub ss: Vec, + pub tt: Vec, + pub kappa_k: Vec, + pub kappa_e: G2Projective, +} + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct SpendWitness<'a> { + // includes skUser, v, t + #[zeroize(skip)] + pub(crate) attributes: &'a [&'a Scalar], + + // signature randomizing element + pub(crate) r: Scalar, + pub(crate) o_c: Scalar, + pub(crate) lk: Vec, + pub(crate) o_a: Vec, + pub(crate) mu: Vec, + pub(crate) o_mu: Vec, + pub(crate) r_k: Vec, + pub(crate) r_e: Scalar, +} + +pub struct WitnessReplacement { + pub r_attributes: Vec, + pub r_r: Scalar, + pub r_r_e: Scalar, + pub r_o_c: Scalar, + pub r_r_k: Vec, + pub r_lk: Vec, + pub r_o_a: Vec, + pub r_mu: Vec, + pub r_o_mu: Vec, +} + +pub struct InstanceCommitments { + pub tt_kappa: G2Projective, + pub tt_kappa_e: G2Projective, + pub tt_cc: G1Projective, + pub tt_aa: Vec, + pub tt_ss: Vec, + pub tt_tt: Vec, + pub tt_gamma1: Vec, + pub tt_kappa_k: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SpendProof { + challenge: Scalar, + response_r: Scalar, + response_r_e: Scalar, + responses_r_k: Vec, + responses_l: Vec, + responses_o_a: Vec, + response_o_c: Scalar, + responses_mu: Vec, + responses_o_mu: Vec, + responses_attributes: Vec, +} + +pub fn generate_witness_replacement(witness: &SpendWitness) -> WitnessReplacement { + let grp_params = ecash_group_parameters(); + let r_attributes = grp_params.n_random_scalars(witness.attributes.len()); + let r_r = grp_params.random_scalar(); + let r_r_e = grp_params.random_scalar(); + let r_o_c = grp_params.random_scalar(); + + let r_r_k = grp_params.n_random_scalars(witness.r_k.len()); + let r_lk = grp_params.n_random_scalars(witness.lk.len()); + let r_o_a = grp_params.n_random_scalars(witness.o_a.len()); + let r_mu = grp_params.n_random_scalars(witness.mu.len()); + let r_o_mu = grp_params.n_random_scalars(witness.o_mu.len()); + WitnessReplacement { + r_attributes, + r_r, + r_r_e, + r_o_c, + r_r_k, + r_lk, + r_o_a, + r_mu, + r_o_mu, + } +} + +pub fn compute_instance_commitments( + witness_replacement: &WitnessReplacement, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], +) -> InstanceCommitments { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + let tt_kappa = grp_params.gen2() * witness_replacement.r_r + + verification_key.alpha + + witness_replacement + .r_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * witness_replacement.r_o_c + gamma1 * witness_replacement.r_attributes[1]; + + let tt_kappa_e = grp_params.gen2() * witness_replacement.r_r_e + + verification_key.alpha + + verification_key.beta_g2[0] * witness_replacement.r_attributes[2]; + + let tt_aa: Vec = witness_replacement + .r_o_a + .iter() + .zip(witness_replacement.r_lk.iter()) + .map(|(r_o_a_k, r_l_k)| g1 * r_o_a_k + gamma1 * r_l_k) + .collect::>(); + + let tt_kappa_k = witness_replacement + .r_lk + .iter() + .zip(witness_replacement.r_r_k.iter()) + .map(|(r_l_k, r_r_k)| { + verification_key.alpha + verification_key.beta_g2[0] * r_l_k + grp_params.gen2() * r_r_k + }) + .collect::>(); + + let tt_ss = witness_replacement + .r_mu + .iter() + .map(|r_mu_k| grp_params.delta() * r_mu_k) + .collect::>(); + + let tt_tt = rr + .iter() + .zip(witness_replacement.r_mu.iter()) + .map(|(rr_k, r_mu_k)| g1 * witness_replacement.r_attributes[0] + (g1 * rr_k) * r_mu_k) + .collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(witness_replacement.r_mu.iter()) + .zip(witness_replacement.r_o_mu.iter()) + .map(|((aa_k, r_mu_k), r_o_mu_k)| (aa_k + instance.cc + gamma1) * r_mu_k + g1 * r_o_mu_k) + .collect::>(); + + InstanceCommitments { + tt_kappa, + tt_kappa_e, + tt_cc, + tt_aa, + tt_ss, + tt_tt, + tt_gamma1, + tt_kappa_k, + } +} + +impl SpendProof { + pub fn construct( + instance: &SpendInstance, + witness: &SpendWitness, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> Self { + let grp_params = ecash_group_parameters(); + // generate random values to replace each witness + let witness_replacement = generate_witness_replacement(witness); + + // compute zkp commitment for each instance + let instance_commitments = + compute_instance_commitments(&witness_replacement, instance, verification_key, rr); + + let tt_aa_bytes = instance_commitments + .tt_aa + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_ss_bytes = instance_commitments + .tt_ss + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_tt_bytes = instance_commitments + .tt_tt + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_gamma1_bytes = instance_commitments + .tt_gamma1 + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_kappa_k_bytes = instance_commitments + .tt_kappa_k + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + + // compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once( + instance_commitments.tt_kappa.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_kappa_e.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_cc.to_bytes().as_ref(), + )) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + // compute response for each witness + let responses_attributes = produce_responses( + &witness_replacement.r_attributes, + &challenge, + witness.attributes, + ); + let response_r = produce_response(&witness_replacement.r_r, &challenge, &witness.r); + let response_r_e = produce_response(&witness_replacement.r_r_e, &challenge, &witness.r_e); + let response_o_c = produce_response(&witness_replacement.r_o_c, &challenge, &witness.o_c); + + let responses_r_k = produce_responses(&witness_replacement.r_r_k, &challenge, &witness.r_k); + let responses_l = produce_responses(&witness_replacement.r_lk, &challenge, &witness.lk); + let responses_o_a = produce_responses(&witness_replacement.r_o_a, &challenge, &witness.o_a); + let responses_mu = produce_responses(&witness_replacement.r_mu, &challenge, &witness.mu); + let responses_o_mu = + produce_responses(&witness_replacement.r_o_mu, &challenge, &witness.o_mu); + + SpendProof { + challenge, + response_r, + response_r_e, + responses_r_k, + responses_l, + responses_o_a, + response_o_c, + responses_mu, + responses_o_mu, + responses_attributes, + } + } + + pub fn verify( + &self, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> bool { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + // re-compute each zkp commitment + let tt_kappa = instance.kappa * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + grp_params.gen2() * self.response_r + + self + .responses_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * self.response_o_c + + gamma1 * self.responses_attributes[1] + + instance.cc * self.challenge; + + let tt_kappa_e = instance.kappa_e * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + verification_key.beta_g2[0] * self.responses_attributes[2] + + grp_params.gen2() * self.response_r_e; + + let tt_aa = self + .responses_o_a + .iter() + .zip(self.responses_l.iter()) + .zip(instance.aa.iter()) + .map(|((resp_o_a_k, resp_l_k), aa_k)| { + g1 * resp_o_a_k + gamma1 * resp_l_k + aa_k * self.challenge + }) + .collect::>(); + + let tt_aa_bytes = tt_aa.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_ss = self + .responses_mu + .iter() + .zip(instance.ss.iter()) + .map(|(resp_mu_k, ss_k)| grp_params.delta() * resp_mu_k + ss_k * self.challenge) + .collect::>(); + + let tt_ss_bytes = tt_ss.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_tt = self + .responses_mu + .iter() + .zip(rr.iter()) + .zip(instance.tt.iter()) + .map(|((resp_mu_k, rr_k), tt_k)| { + g1 * self.responses_attributes[0] + (g1 * rr_k) * resp_mu_k + tt_k * self.challenge + }) + .collect::>(); + + let tt_tt_bytes = tt_tt.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(self.responses_mu.iter()) + .zip(self.responses_o_mu.iter()) + .map(|((aa_k, resp_mu_k), resp_o_mu_k)| { + (aa_k + instance.cc + gamma1) * resp_mu_k + + g1 * resp_o_mu_k + + gamma1 * self.challenge + }) + .collect::>(); + + let tt_gamma1_bytes = tt_gamma1.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_kappa_k = instance + .kappa_k + .iter() + .zip(self.responses_r_k.iter()) + .zip(self.responses_l.iter()) + .map(|((kappa_k, resp_r_k), resp_r_l_k)| { + kappa_k * self.challenge + + grp_params.gen2() * resp_r_k + + verification_key.alpha * (Scalar::one() - self.challenge) + + verification_key.beta_g2[0] * resp_r_l_k + }) + .collect::>(); + + let tt_kappa_k_bytes = tt_kappa_k.iter().map(|x| x.to_bytes()).collect::>(); + + // re-compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa_e.to_bytes().as_ref())) + .chain(std::iter::once(tt_cc.to_bytes().as_ref())) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + challenge == self.challenge + } +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs new file mode 100644 index 0000000000..cba7161713 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs @@ -0,0 +1,248 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::PublicKeyUser; +use bls12_381::{G1Projective, Scalar}; +use group::GroupEncoding; +use itertools::izip; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +// instance: g, gamma1, gamma2, gamma3, com, h, com1, com2, com3, pkUser +pub struct WithdrawalReqInstance { + // Joined commitment to all attributes + pub(crate) joined_commitment: G1Projective, + // Hash of the joined commitment com + pub(crate) joined_commitment_hash: G1Projective, + // Pedersen commitments to each attribute + pub(crate) private_attributes_commitments: Vec, + // Public key of a user + pub(crate) pk_user: PublicKeyUser, +} + +// witness: m1, m2, m3, o, o1, o2, o3, +pub struct WithdrawalReqWitness { + pub private_attributes: Vec, + // Opening for the joined commitment com + pub joined_commitment_opening: Scalar, + // Openings for the pedersen commitments of private attributes + pub private_attributes_openings: Vec, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalReqProof { + challenge: Scalar, + response_opening: Scalar, + response_openings: Vec, + response_attributes: Vec, +} + +impl WithdrawalReqProof { + pub(crate) fn construct( + instance: &WithdrawalReqInstance, + witness: &WithdrawalReqWitness, + ) -> Self { + let params = ecash_group_parameters(); + // generate random values to replace the witnesses + let r_com_opening = params.random_scalar(); + let r_pedcom_openings = params.n_random_scalars(witness.private_attributes_openings.len()); + let r_attributes = params.n_random_scalars(witness.private_attributes.len()); + + // compute zkp commitments for each instance + let zkcm_com = params.gen1() * r_com_opening + + r_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(rm_i, gamma_i)| gamma_i * rm_i) + .sum::(); + + let zkcm_pedcom = r_pedcom_openings + .iter() + .zip(r_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + instance.joined_commitment_hash * m_j) + .collect::>(); + + let zkcm_user_sk = params.gen1() * r_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // compute zkp challenge using g1, gammas, c, h, c1, c2, c3, zk commitments + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|gamma| gamma.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zkcm_user_sk.to_bytes().as_ref())), + ); + + // compute response + let response_opening = produce_response( + &r_com_opening, + &challenge, + &witness.joined_commitment_opening, + ); + let response_openings = produce_responses( + &r_pedcom_openings, + &challenge, + &witness + .private_attributes_openings + .iter() + .collect::>(), + ); + let response_attributes = produce_responses( + &r_attributes, + &challenge, + &witness.private_attributes.iter().collect::>(), + ); + + WithdrawalReqProof { + challenge, + response_opening, + response_openings, + response_attributes, + } + } + + pub(crate) fn verify(&self, instance: &WithdrawalReqInstance) -> bool { + let params = ecash_group_parameters(); + // recompute zk commitments for each instance + let zkcm_com = instance.joined_commitment * self.challenge + + params.gen1() * self.response_opening + + self + .response_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(m_i, gamma_i)| gamma_i * m_i) + .sum::(); + + let zkcm_pedcom = izip!( + instance.private_attributes_commitments.iter(), + self.response_openings.iter(), + self.response_attributes.iter() + ) + .map(|(cm_j, resp_o_j, resp_m_j)| { + cm_j * self.challenge + + params.gen1() * resp_o_j + + instance.joined_commitment_hash * resp_m_j + }) + .collect::>(); + + let zk_commitment_user_sk = + instance.pk_user.pk * self.challenge + params.gen1() * self.response_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // recompute zkp challenge + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|hs| hs.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zk_commitment_user_sk.to_bytes().as_ref())), + ); + + challenge == self.challenge + } +} + +#[cfg(test)] +mod tests { + use group::Group; + use rand::thread_rng; + + use crate::GroupParameters; + use crate::{constants, utils::hash_g1}; + + use super::*; + + #[test] + fn withdrawal_request_instance_roundtrip() { + let mut rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let instance = WithdrawalReqInstance { + joined_commitment: G1Projective::random(&mut rng), + joined_commitment_hash: G1Projective::random(&mut rng), + private_attributes_commitments: vec![ + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + ], + pk_user: PublicKeyUser { + pk: params.gen1() * params.random_scalar(), + }, + }; + + let instance_bytes = instance.to_bytes(); + let instance_p = WithdrawalReqInstance::from_bytes(&instance_bytes).unwrap(); + assert_eq!(instance, instance_p) + } + + #[test] + fn withdrawal_proof_construct_and_verify() { + let _rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let sk = params.random_scalar(); + let pk_user = PublicKeyUser { + pk: params.gen1() * sk, + }; + let v = params.random_scalar(); + let t = params.random_scalar(); + let private_attributes = vec![sk, v, t]; + + let joined_commitment_opening = params.random_scalar(); + let joined_commitment = params.gen1() * joined_commitment_opening + + private_attributes + .iter() + .zip(params.gammas()) + .map(|(&m, gamma)| gamma * m) + .sum::(); + let joined_commitment_hash = hash_g1(joined_commitment.to_bytes()); + + let private_attributes_openings = params.n_random_scalars(private_attributes.len()); + let private_attributes_commitments = private_attributes_openings + .iter() + .zip(private_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + joined_commitment_hash * m_j) + .collect::>(); + + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments, + pk_user, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings, + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + assert!(zk_proof.verify(&instance)) + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs new file mode 100644 index 0000000000..d4441179bb --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -0,0 +1,161 @@ +// Copyright 2024 - Nym Technologies SA +// 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::common_types::{PartialSignature, Signature, SignatureShare, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::expiration_date_signatures::scalar_date; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{PartialWallet, Wallet, WalletSignatures}; +use crate::utils::{check_bilinear_pairing, perform_lagrangian_interpolation_at_origin}; +use crate::{ecash_group_parameters, Attribute}; + +pub(crate) trait Aggregatable: Sized { + fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result; + + 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() + } +} + +impl Aggregatable for T +where + T: Sum, + for<'a> T: Sum<&'a T>, + for<'a> &'a T: Mul, +{ + fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result { + if aggregatable.is_empty() { + return Err(CompactEcashError::AggregationEmptySet); + } + + if let Some(indices) = indices { + if !Self::check_unique_indices(indices) { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + 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 { + let h = sigs + .first() + .ok_or(CompactEcashError::AggregationEmptySet)? + .sig1(); + + // TODO: is it possible to avoid this allocation? + let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::>(); + let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?; + + Ok(Signature { + h: *h, + s: aggr_sigma, + }) + } +} + +/// Ensures all provided verification keys were generated to verify the same number of attributes. +fn check_same_key_size(keys: &[VerificationKeyAuth]) -> bool { + keys.iter().map(|vk| vk.beta_g1.len()).all_equal() + && keys.iter().map(|vk| vk.beta_g2.len()).all_equal() +} + +pub fn aggregate_verification_keys( + keys: &[VerificationKeyAuth], + indices: Option<&[SignerIndex]>, +) -> Result { + if !check_same_key_size(keys) { + return Err(CompactEcashError::AggregationSizeMismatch); + } + Aggregatable::aggregate(keys, indices) +} + +pub fn aggregate_signature_shares( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + shares: &[SignatureShare], +) -> Result { + let (signatures, indices): (Vec<_>, Vec<_>) = shares + .iter() + .map(|share| (*share.signature(), share.index())) + .unzip(); + + aggregate_signatures(verification_key, attributes, &signatures, Some(&indices)) +} + +pub fn aggregate_signatures( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + signatures: &[PartialSignature], + indices: Option<&[SignerIndex]>, +) -> Result { + let params = ecash_group_parameters(); + // aggregate the signature + + let signature = match Aggregatable::aggregate(signatures, indices) { + Ok(res) => res, + Err(err) => return Err(err), + }; + + // Verify the signature + let tmp = attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + if !check_bilinear_pairing( + &signature.h.to_affine(), + &G2Prepared::from((verification_key.alpha + tmp).to_affine()), + &signature.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::AggregationVerification); + } + Ok(signature) +} + +pub fn aggregate_wallets( + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + wallets: &[PartialWallet], + req_info: &RequestInfo, +) -> Result { + // Aggregate partial wallets + let signature_shares: Vec = wallets + .iter() + .map(|wallet| SignatureShare::new(*wallet.signature(), wallet.index())) + .collect(); + + let attributes = vec![ + sk_user.sk, + *req_info.get_v(), + *req_info.get_expiration_date(), + ]; + let aggregated_signature = + aggregate_signature_shares(verification_key, &attributes, &signature_shares)?; + + let expiration_date_timestamp = req_info.get_expiration_date(); + + Ok(Wallet { + signatures: WalletSignatures { + sig: aggregated_signature, + v: *req_info.get_v(), + expiration_date_timestamp: scalar_date(expiration_date_timestamp), + }, + tickets_spent: 0, + }) +} diff --git a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs new file mode 100644 index 0000000000..b08ab368dc --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs @@ -0,0 +1,439 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::constants; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::scheme::setup::Parameters; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +pub type CoinIndexSignature = Signature; +pub type PartialCoinIndexSignature = CoinIndexSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedCoinIndexSignature { + pub signature: CoinIndexSignature, + pub index: u64, +} + +impl Borrow for AnnotatedCoinIndexSignature { + fn borrow(&self) -> &CoinIndexSignature { + &self.signature + } +} + +impl From for CoinIndexSignature { + fn from(value: AnnotatedCoinIndexSignature) -> Self { + value.signature + } +} + +pub struct CoinIndexSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs coin indices. +/// +/// This function takes cryptographic parameters, a global verification key, and a secret key of the signing authority, +/// and generates partial coin index signatures for a specified number of indices using a parallel fold operation. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `vk` - The global verification key. +/// * `sk_auth` - The secret key associated with the individual signing authority. +/// +/// # Returns +/// +/// A vector containing partial coin index signatures. +pub fn sign_coin_indices( + params: &Parameters, + vk: &VerificationKeyAuth, + sk_auth: &SecretKeyAuth, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + + let vk_bytes = vk.to_bytes(); + let partial_s_exponent = sk_auth.x + sk_auth.ys[1] * m1 + sk_auth.ys[2] * m2; + + let sign_index = |index: u64| { + let m0: Scalar = Scalar::from(index); + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + index.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&index.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Sign the attributes + let s_exponent = partial_s_exponent + sk_auth.ys[0] * m0; + + // Create the signature struct + let signature = PartialCoinIndexSignature { + h, + s: h * s_exponent, + }; + AnnotatedCoinIndexSignature { signature, index } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..params.get_total_coins()) + .into_par_iter() + .map(sign_index) + .collect()) + } else { + Ok((0..params.get_total_coins()).map(sign_index).collect()) + } + } +} + +/// Verifies coin index signatures using parallel iterators. +/// +/// This function takes cryptographic parameters, verification keys, and a list of coin index +/// signatures. It verifies each signature's commitment hash and performs a bilinear pairing check. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the verification process. +/// * `vk` - The global verification key. +/// * `vk_auth` - The verification key associated with the authority which issued the partial signatures. +/// * `signatures` - A slice containing coin index signatures to be verified. +/// +/// # Returns +/// +/// Returns `Ok(())` if all signatures are valid, otherwise returns an error with a description +/// of the verification failure. +pub fn verify_coin_indices_signatures( + vk: &VerificationKeyAuth, + vk_auth: &VerificationKeyAuth, + signatures: &[B], +) -> Result<()> +where + B: Borrow, +{ + if vk_auth.beta_g2.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + let partially_signed = vk_auth.alpha + vk_auth.beta_g2[1] * m1 + vk_auth.beta_g2[2] * m2; + let vk_bytes = vk.to_bytes(); + + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u64; + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + + // Compute the hash h + let h = hash_g1(concatenated_bytes.clone()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + let m0 = Scalar::from(l); + // push elements for computing + // e(h1, X1) * e(s1, g2^-1) * ... * e(hi, Xi) * e(si, g2^-1) + // where + // h: H(vk, l) + // si: h^{xi + yi[0] * mi0 + yi[1] * m1 + yi[2] * m2} + // X: g2^{x + y[0] * mi0 + yi[1] * m1 + yi[2] * m2} + pairing_terms.push((sig, vk_auth.beta_g2[0] * m0 + partially_signed)); + } + + // computing all pairings in parallel using rayon makes it go from ~45ms to ~30ms, + // but given this function is called very infrequently, the possible interference up the stack is not worth it + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + Ok(()) +} + +fn _aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_coin_indices_signatures(vk, &share.key, &share.signatures) + })?; + } else { + + signatures_shares.iter().try_for_each(|share| verify_coin_indices_signatures(vk, &share.key, &share.signatures))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_coin_signatures: Vec = + Vec::with_capacity(params.get_total_coins() as usize); + + let vk_bytes = vk.to_bytes(); + for l in 0..params.get_total_coins() { + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Collect the partial signatures for the same coin index + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each coin index + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = CoinIndexSignature { h, s: aggr_s }; + aggregated_coin_signatures.push(aggr_sig); + } + verify_coin_indices_signatures(vk, vk, &aggregated_coin_signatures)?; + Ok(aggregated_coin_signatures) +} + +/// Aggregates and verifies partial coin index signatures. +/// +/// This function takes cryptographic parameters, a master verification key, and a list of tuples +/// containing indices, verification keys, and partial coin index signatures from different authorities. +/// It aggregates these partial signatures into a final set of coin index signatures, and verifying the +/// final aggregated signatures. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the aggregation process. +/// * `vk` - The master verification key against which the partial signatures are verified. +/// * `signatures` - A slice of tuples, where each tuple contains an index, a verification key, and +/// a vector of partial coin index signatures from a specific authority. +/// +/// # Returns +/// +/// Returns a vector of aggregated coin index signatures if the aggregation is successful. +/// Otherwise, returns an error describing the nature of the failure. +pub fn aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_indices_signatures(params, vk, signatures_shares, true) +} + +/// Perform aggregation and verification of partial coin index signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with index information +pub fn aggregate_annotated_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signature_shares: &[CoinIndexSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signature_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + for (idx, sig) in share.signatures.iter().enumerate() { + if idx != sig.index as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + } + + let aggregated = aggregate_indices_signatures(params, vk, signature_shares)?; + Ok(aggregated + .into_iter() + .enumerate() + .map(|(index, signature)| AnnotatedCoinIndexSignature { + signature, + index: index as u64, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_indices_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_coin_indices_signatures` +pub fn unchecked_aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> { + _aggregate_indices_signatures(params, vk, signatures_shares, false) +} + +/// Generates parameters for the scheme setup. +/// +/// # Arguments +/// +/// * `total_coins` - it is the number of coins in a freshly generated wallet. It is the public parameter of the scheme. +/// +/// # Returns +/// +/// A `Parameters` struct containing group parameters, public key, the number of signatures (`total_coins`), +/// and a map of signatures for each index `l`. +/// + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_sign_coins() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + .is_ok() + ); + } + + #[test] + fn test_sign_coins_fail() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_0_auth = authorities_keypairs[0].secret_key(); + let vk_1_auth = authorities_keypairs[1].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_0_auth).unwrap(); + // Since we used a non matching verification key to verify the signature, the verification should fail + assert!( + verify_coin_indices_signatures(&verification_key, &vk_1_auth, &partial_signatures) + .is_err() + ); + } + + #[test] + fn test_aggregate_coin_indices_signatures() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_indices_signatures(¶ms, &verification_key, &combined_data).is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs new file mode 100644 index 0000000000..b419d0ef73 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs @@ -0,0 +1,500 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::constants; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +/// A structure representing an expiration date signature. +pub type ExpirationDateSignature = Signature; +pub type PartialExpirationDateSignature = ExpirationDateSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedExpirationDateSignature { + pub signature: ExpirationDateSignature, + pub expiration_timestamp: u64, + pub spending_timestamp: u64, +} + +impl Borrow for AnnotatedExpirationDateSignature { + fn borrow(&self) -> &ExpirationDateSignature { + &self.signature + } +} + +impl From for ExpirationDateSignature { + fn from(value: AnnotatedExpirationDateSignature) -> Self { + value.signature + } +} + +pub struct ExpirationDateSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs given expiration date for a specified validity period using the given secret key of a single authority. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `sk_auth` - The secret key of the signing authority. +/// * `expiration_unix_timestamp` - The expiration date for which signatures will be generated (as unix timestamp). +/// +/// # Returns +/// +/// A vector containing partial signatures for each date within the validity period (i.e., +/// from expiration_date - CRED_VALIDITY_PERIOD till expiration_date. +/// +/// # Note +/// +/// This function is executed by a single singing authority and generates partial expiration date +/// signatures for a specified validity period. Each signature is created by combining cryptographic +/// attributes derived from the expiration date, and the resulting vector contains signatures for +/// each date within the defined validity period till expiration date. +/// The validity period is determined by the constant `CRED_VALIDITY_PERIOD` in the `constants` module. +pub fn sign_expiration_date( + sk_auth: &SecretKeyAuth, + expiration_unix_timestamp: u64, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m0: Scalar = Scalar::from(expiration_unix_timestamp); + let m2: Scalar = constants::TYPE_EXP; + + let partial_s_exponent = sk_auth.x + sk_auth.ys[0] * m0 + sk_auth.ys[2] * m2; + + let sign_expiration = |offset: u64| { + // we produce tuples of (assuming CRED_VALIDITY_PERIOD_DAYS = 30): + // (expiration, expiration - 29) + // (expiration, expiration - 28) + // ... + // (expiration, expiration) + let spending_unix_timestamp = expiration_unix_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - offset - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(spending_unix_timestamp); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + // Sign the attributes by performing scalar-point multiplications and accumulating the result + let s_exponent = partial_s_exponent + sk_auth.ys[1] * m1; + + // Create the signature struct on the expiration date + let signature = PartialExpirationDateSignature { + h, + s: h * s_exponent, + }; + + AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: expiration_unix_timestamp, + spending_timestamp: spending_unix_timestamp, + } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS) + .into_par_iter() + .map(sign_expiration) + .collect()) + } else { + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS).map(sign_expiration).collect()) + } + } +} + +/// Verifies the expiration date signatures against the given verification key. +/// +/// This function iterates over the provided valid date signatures and verifies each one +/// against the provided verification key. It computes the hash and checks the correctness of the +/// signature using bilinear pairings. +/// +/// # Arguments +/// +/// * `vkey` - The verification key of the signing authority. +/// * `signatures` - The list of date signatures to be verified. +/// * `expiration_date` - The expiration date for which signatures are being issued (as unix timestamp). +/// +/// # Returns +/// +/// Returns `Ok(true)` if all signatures are verified successfully, otherwise returns an error +/// +pub fn verify_valid_dates_signatures( + vk: &VerificationKeyAuth, + signatures: &[B], + expiration_date: u64, +) -> Result<()> +where + B: Borrow, +{ + let m0: Scalar = Scalar::from(expiration_date); + let m2: Scalar = constants::TYPE_EXP; + + let partially_signed = vk.alpha + vk.beta_g2[0] * m0 + vk.beta_g2[2] * m2; + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u64; + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(valid_date); + + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + // let partially_signed_attributes = partially_signed + vk.beta_g2[1] * m1; + pairing_terms.push((sig, partially_signed + vk.beta_g2[1] * m1)); + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + Ok(()) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +fn _aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date) + })?; + } else { + signatures_shares.iter().try_for_each(|share| verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_date_signatures: Vec = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + + let m0: Scalar = Scalar::from(expiration_date); + + for l in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(valid_date); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + // Collect the partial signatures for the same valid date + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each validity date + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = ExpirationDateSignature { h, s: aggr_s }; + aggregated_date_signatures.push(aggr_sig); + } + verify_valid_dates_signatures(vk, &aggregated_date_signatures, expiration_date)?; + Ok(aggregated_date_signatures) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +pub fn aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, true) +} + +/// Perform aggregation and verification of partial expiration date signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with timestamp information +pub fn aggregate_annotated_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signatures_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + for (i, sig) in share.signatures.iter().enumerate() { + if sig.expiration_timestamp != expiration_date { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + let l = i as u64; + let expected_spending = sig.expiration_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + + if sig.spending_timestamp != expected_spending { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + } + + let aggregated = aggregate_expiration_signatures(vk, expiration_date, signatures_shares)?; + assert_eq!(aggregated.len(), share.signatures.len()); + + Ok(aggregated + .into_iter() + .zip(share.signatures.iter()) + .map(|(signature, sh)| AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: sh.expiration_timestamp, + spending_timestamp: sh.spending_timestamp, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_expiration_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_valid_dates_signatures` +pub fn unchecked_aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, false) +} + +/// Finds the index corresponding to the given spend date based on the expiration date. +/// +/// This function calculates the index such that the following equality holds: +/// `spend_date = expiration_date - 30 + index` +/// This index is used to retrieve a corresponding signature. +/// +/// # Arguments +/// +/// * `spend_date` - The spend date for which to find the index. +/// * `expiration_date` - The expiration date used in the calculation. +/// +/// # Returns +/// +/// If a valid index is found, returns `Ok(index)`. If no valid index is found +/// (i.e., `spend_date` is earlier than `expiration_date - 30`), returns `Err(InvalidDateError)`. +/// +pub fn find_index(spend_date: u64, expiration_date: u64) -> Result { + let start_date = + expiration_date - ((constants::CRED_VALIDITY_PERIOD_DAYS - 1) * constants::SECONDS_PER_DAY); + + if spend_date >= start_date { + let index_a = ((spend_date - start_date) / constants::SECONDS_PER_DAY) as usize; + if index_a as u64 >= constants::CRED_VALIDITY_PERIOD_DAYS { + Err(CompactEcashError::SpendDateTooLate) + } else { + Ok(index_a) + } + } else { + Err(CompactEcashError::SpendDateTooEarly) + } +} + +pub fn date_scalar(date: u64) -> Scalar { + Scalar::from(date) +} + +// TODO: this will not work for **all** scalars, +// but timestamps have extremely (relatively speaking) limited range, +// so this should be fine +pub fn scalar_date(scalar: &Scalar) -> u64 { + let b = scalar.to_bytes(); + u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_find_index() { + let expiration_date = 1701993600; // Dec 8 2023 + for i in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let current_spend_date = expiration_date - i * 86400; + assert_eq!( + find_index(current_spend_date, expiration_date).unwrap(), + (constants::CRED_VALIDITY_PERIOD_DAYS - 1 - i) as usize + ) + } + + let late_spend_date = expiration_date + 86400; + assert!(find_index(late_spend_date, expiration_date).is_err()); + + let early_spend_date = expiration_date - (constants::CRED_VALIDITY_PERIOD_DAYS) * 86400; + assert!(find_index(early_spend_date, expiration_date).is_err()); + } + + #[test] + fn test_sign_expiration_date() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + assert!( + verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok() + ); + } + + #[test] + fn test_aggregate_expiration_signatures() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + + let combined_data = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + .is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs new file mode 100644 index 0000000000..1dd2779137 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -0,0 +1,574 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::scheme::keygen::PublicKeyUser; +use crate::scheme::{compute_pay_info_hash, Payment}; + +use crate::PayInfo; + +#[derive(Debug, Eq, PartialEq)] +pub enum IdentifyResult { + NotADuplicatePayment, + DuplicatePayInfo(PayInfo), + DoubleSpendingPublicKeys(PublicKeyUser), +} + +pub fn identify( + payment1: &Payment, + payment2: &Payment, + pay_info1: PayInfo, + pay_info2: PayInfo, +) -> IdentifyResult { + let mut k = 0; + let mut j = 0; + for (id1, pay1_ss) in payment1.ss.iter().enumerate() { + for (id2, pay2_ss) in payment2.ss.iter().enumerate() { + if pay1_ss == pay2_ss { + k = id1; + j = id2; + break; + } + } + } + if payment1 + .ss + .iter() + .any(|pay1_ss| payment2.ss.contains(pay1_ss)) + { + if pay_info1 == pay_info2 { + IdentifyResult::DuplicatePayInfo(pay_info1) + } else { + let rr_k_payment1 = compute_pay_info_hash(&pay_info1, k as u64); + let rr_j_payment2 = compute_pay_info_hash(&pay_info2, j as u64); + let rr_diff = rr_k_payment1 - rr_j_payment2; + //SAFETY: `pay_info1` and `pay_info2` are different here, so rr_diff will not be zero, invert is then fine + let pk = (payment2.tt[j] * rr_k_payment1 - payment1.tt[k] * rr_j_payment2) + * rr_diff.invert().unwrap(); + let pk_user = PublicKeyUser { pk }; + IdentifyResult::DoubleSpendingPublicKeys(pk_user) + } + } else { + IdentifyResult::NotADuplicatePayment + } +} + +#[cfg(test)] +mod tests { + use crate::scheme::expiration_date_signatures::date_scalar; + use crate::scheme::identify::{identify, IdentifyResult}; + use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use crate::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth, + }; + use itertools::izip; + + #[test] + fn duplicate_payments_with_the_same_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let payment2 = payment1.clone(); + assert!(payment2 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info1); + assert_eq!(identify_result, IdentifyResult::DuplicatePayInfo(pay_info1)); + } + + #[test] + fn ok_if_two_different_payments() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!(identify_result, IdentifyResult::NotADuplicatePayment); + } + + #[test] + fn two_payments_with_one_repeating_serial_number_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _i in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 1; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } + + #[test] + fn two_payments_with_multiple_repeating_serial_numbers_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 10; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 10; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs new file mode 100644 index 0000000000..375d974e9c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -0,0 +1,657 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{CompactEcashError, Result}; +use crate::scheme::aggregation::aggregate_verification_keys; +use crate::scheme::SignerIndex; +use crate::traits::Bytable; +use crate::utils::{hash_to_scalar, Polynomial}; +use crate::utils::{ + try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar, + try_deserialize_scalar_vec, +}; +use crate::{ecash_group_parameters, Base58}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use core::borrow::Borrow; +use core::iter::Sum; +use core::ops::{Add, Mul}; +use group::{Curve, GroupEncoding}; +use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyAuth { + pub(crate) x: Scalar, + pub(crate) ys: Vec, +} + +impl PemStorableKey for SecretKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH SECRET KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom<&[u8]> for SecretKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be x and at least one y + if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8, + target: 32 * 2 + 8, + modulus: 32, + object: "secret key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + 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(CompactEcashError::DeserializationLengthMismatch { + type_name: "Secret_key ys".into(), + expected: ys_len as usize, + actual: actual_ys_len, + }); + } + + let x = try_deserialize_scalar(&x_bytes)?; + let ys = try_deserialize_scalar_vec(ys_len, &bytes[40..])?; + + Ok(SecretKeyAuth { x, ys }) + } +} + +impl SecretKeyAuth { + /// Following a (distributed) key generation process, scalar values can be obtained + /// outside of the normal key generation process. + pub fn create_from_raw(x: Scalar, ys: Vec) -> Self { + Self { x, ys } + } + + /// Extract the Scalar copy of the underlying secrets. + /// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized + pub fn hazmat_to_raw(&self) -> (Scalar, Vec) { + (self.x, self.ys.clone()) + } + + pub fn size(&self) -> usize { + self.ys.len() + } + + pub(crate) fn get_y_by_idx(&self, i: usize) -> Option<&Scalar> { + self.ys.get(i) + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + let params = ecash_group_parameters(); + let g1 = params.gen1(); + let g2 = params.gen2(); + VerificationKeyAuth { + alpha: g2 * self.x, + beta_g1: self.ys.iter().map(|y| g1 * y).collect(), + beta_g2: self.ys.iter().map(|y| g2 * y).collect(), + } + } + + pub fn to_bytes(&self) -> Vec { + let ys_len = self.ys.len(); + let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 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 { + SecretKeyAuth::try_from(bytes) + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct VerificationKeyAuth { + pub(crate) alpha: G2Projective, + pub(crate) beta_g1: Vec, + pub(crate) beta_g2: Vec, +} + +impl PemStorableKey for VerificationKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH VERIFICATION KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom<&[u8]> for VerificationKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be at least alpha, one betaG1 and one betaG2 and their length + if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8 - 96, + target: 96 * 2 + 48 + 8, + modulus: 96 + 48, + object: "verification key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap()); + + let actual_betas_len = (bytes.len() - 104) / (96 + 48); + + if betas_len as usize != actual_betas_len { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Verification_key betas".into(), + expected: betas_len as usize, + actual: actual_betas_len, + }); + } + + let alpha = try_deserialize_g2_projective(&alpha_bytes)?; + + let mut beta_g1 = Vec::with_capacity(betas_len as usize); + let mut beta_g1_end: u64 = 0; + for i in 0..betas_len { + let start = (104 + i * 48) as usize; + let end = start + 48; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g1_projective(&beta_i_bytes)?; + + beta_g1_end = end as u64; + beta_g1.push(beta_i) + } + + let mut beta_g2 = Vec::with_capacity(betas_len as usize); + for i in 0..betas_len { + let start = (beta_g1_end + i * 96) as usize; + let end = start + 96; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g2_projective(&beta_i_bytes)?; + + beta_g2.push(beta_i) + } + + Ok(VerificationKeyAuth { + alpha, + beta_g1, + beta_g2, + }) + } +} + +impl<'b> Add<&'b VerificationKeyAuth> for VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn add(self, rhs: &'b VerificationKeyAuth) -> VerificationKeyAuth { + // 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_g1.len(), + rhs.beta_g1.len(), + "trying to add verification keys generated for different number of attributes [G1]" + ); + + assert_eq!( + self.beta_g2.len(), + rhs.beta_g2.len(), + "trying to add verification keys generated for different number of attributes [G2]" + ); + + assert_eq!( + self.beta_g1.len(), + self.beta_g2.len(), + "this key is incorrect - the number of elements G1 and G2 does not match" + ); + + assert_eq!( + rhs.beta_g1.len(), + rhs.beta_g2.len(), + "they key you want to add is incorrect - the number of elements G1 and G2 does not match" + ); + + VerificationKeyAuth { + alpha: self.alpha + rhs.alpha, + beta_g1: self + .beta_g1 + .iter() + .zip(rhs.beta_g1.iter()) + .map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1) + .collect(), + beta_g2: self + .beta_g2 + .iter() + .zip(rhs.beta_g2.iter()) + .map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2) + .collect(), + } + } +} + +impl<'a> Mul for &'a VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn mul(self, rhs: Scalar) -> Self::Output { + VerificationKeyAuth { + alpha: self.alpha * rhs, + beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(), + beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(), + } + } +} + +impl Sum for VerificationKeyAuth +where + T: Borrow, +{ + #[inline] + fn sum(iter: I) -> Self + where + I: Iterator, + { + let mut peekable = iter.peekable(); + let head_attributes = match peekable.peek() { + Some(head) => head.borrow().beta_g2.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 VerificationKeyAuth::identity(0); + } + }; + + peekable.fold( + VerificationKeyAuth::identity(head_attributes), + |acc, item| acc + item.borrow(), + ) + } +} + +impl VerificationKeyAuth { + /// Create a (kinda) identity verification key using specified + /// number of 'beta' elements + pub(crate) fn identity(beta_size: usize) -> Self { + VerificationKeyAuth { + alpha: G2Projective::identity(), + beta_g1: vec![G1Projective::identity(); beta_size], + beta_g2: vec![G2Projective::identity(); beta_size], + } + } + + pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result { + aggregate_verification_keys(sigs, indices) + } + + pub fn alpha(&self) -> &G2Projective { + &self.alpha + } + + pub fn beta_g1(&self) -> &Vec { + &self.beta_g1 + } + + pub fn beta_g2(&self) -> &Vec { + &self.beta_g2 + } + + pub fn to_bytes(&self) -> Vec { + let beta_g1_len = self.beta_g1.len(); + let beta_g2_len = self.beta_g2.len(); + let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96); + + bytes.extend_from_slice(&self.alpha.to_affine().to_compressed()); + + bytes.extend_from_slice(&beta_g1_len.to_le_bytes()); + + for beta_g1 in self.beta_g1.iter() { + bytes.extend_from_slice(&beta_g1.to_affine().to_compressed()) + } + + for beta_g2 in self.beta_g2.iter() { + bytes.extend_from_slice(&beta_g2.to_affine().to_compressed()) + } + + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + VerificationKeyAuth::try_from(bytes) + } +} + +impl Bytable for VerificationKeyAuth { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for VerificationKeyAuth {} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyUser { + pub(crate) sk: Scalar, +} + +impl SecretKeyUser { + pub fn public_key(&self) -> PublicKeyUser { + PublicKeyUser { + pk: ecash_group_parameters().gen1() * self.sk, + } + } + + pub fn to_bytes(&self) -> Vec { + self.sk.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let sk = Scalar::try_from_byte_slice(bytes)?; + Ok(SecretKeyUser { sk }) + } +} + +impl Bytable for SecretKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for SecretKeyUser {} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct PublicKeyUser { + pub(crate) pk: G1Projective, +} + +impl PublicKeyUser { + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.pk.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.pk.to_affine().to_compressed().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PublicKeyUser".into(), + expected: 48, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let pk_bytes: &[u8; 48] = bytes[..48].try_into().unwrap(); + let pk = try_deserialize_g1_projective(pk_bytes)?; + Ok(PublicKeyUser { pk }) + } +} + +impl Bytable for PublicKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::from_bytes(slice) + } +} + +impl Base58 for PublicKeyUser {} + +#[derive(Debug, Zeroize, ZeroizeOnDrop)] +pub struct KeyPairAuth { + secret_key: SecretKeyAuth, + #[zeroize(skip)] + verification_key: VerificationKeyAuth, + /// Optional index value specifying polynomial point used during threshold key generation. + pub index: Option, +} + +impl From for KeyPairAuth { + fn from(secret_key: SecretKeyAuth) -> Self { + KeyPairAuth { + verification_key: secret_key.verification_key(), + secret_key, + index: None, + } + } +} + +impl PemStorableKeyPair for KeyPairAuth { + type PrivatePemKey = SecretKeyAuth; + type PublicPemKey = VerificationKeyAuth; + + fn private_key(&self) -> &Self::PrivatePemKey { + &self.secret_key + } + + fn public_key(&self) -> &Self::PublicPemKey { + &self.verification_key + } + + fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self { + Self::from_keys(secret_key, verification_key) + } +} + +impl KeyPairAuth { + pub fn new( + sk: SecretKeyAuth, + vk: VerificationKeyAuth, + index: Option, + ) -> KeyPairAuth { + KeyPairAuth { + secret_key: sk, + verification_key: vk, + index, + } + } + + pub fn from_keys(secret_key: SecretKeyAuth, verification_key: VerificationKeyAuth) -> Self { + Self { + secret_key, + verification_key, + index: None, + } + } + + pub fn secret_key(&self) -> &SecretKeyAuth { + &self.secret_key + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + self.verification_key.clone() + } + + pub fn verification_key_ref(&self) -> &VerificationKeyAuth { + &self.verification_key + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct KeyPairUser { + secret_key: SecretKeyUser, + public_key: PublicKeyUser, +} + +impl KeyPairUser { + pub fn secret_key(&self) -> &SecretKeyUser { + &self.secret_key + } + + pub fn public_key(&self) -> PublicKeyUser { + self.public_key.clone() + } + + pub fn to_bytes(&self) -> Vec { + [self.secret_key.to_bytes(), self.public_key.to_bytes()].concat() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 32 + 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "KeyPairUser".into(), + expected: 80, + actual: bytes.len(), + }); + } + let sk = SecretKeyUser::from_bytes(&bytes[..32])?; + let pk = PublicKeyUser::from_bytes(&bytes[32..32 + 48])?; + Ok(KeyPairUser { + secret_key: sk, + public_key: pk, + }) + } +} + +pub fn generate_keypair_user() -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: params.random_scalar(), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn generate_keypair_user_from_seed>(seed: M) -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: hash_to_scalar(seed), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn ttp_keygen(threshold: u64, num_authorities: u64) -> Result> { + let params = ecash_group_parameters(); + if threshold == 0 { + return Err(CompactEcashError::KeygenParameters); + } + + if threshold > num_authorities { + return Err(CompactEcashError::KeygenParameters); + } + + let attributes = params.gammas().len(); + + // generate polynomials + let v = Polynomial::new_random(params, threshold - 1); + let ws = (0..attributes + 1) + .map(|_| Polynomial::new_random(params, threshold - 1)) + .collect::>(); + + // 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::>(); + + // 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::>() + }); + + // finally set the keys + let secret_keys = x.zip(ys).map(|(x, ys)| SecretKeyAuth { x, ys }); + + let keypairs = secret_keys + .zip(polynomial_indices.iter()) + .map(|(secret_key, index)| { + let verification_key = secret_key.verification_key(); + KeyPairAuth { + secret_key, + verification_key, + index: Some(*index), + } + }) + .collect(); + + Ok(keypairs) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn secret_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/mod.rs b/common/nym_offline_compact_ecash/src/scheme/mod.rs new file mode 100644 index 0000000000..7440c3641c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/mod.rs @@ -0,0 +1,979 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness}; +use crate::scheme::coin_indices_signatures::CoinIndexSignature; +use crate::scheme::expiration_date_signatures::{date_scalar, find_index, ExpirationDateSignature}; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::{GroupParameters, Parameters}; +use crate::traits::Bytable; +use crate::utils::{ + batch_verify_signatures, check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar, +}; +use crate::Base58; +use crate::{constants, ecash_group_parameters}; +use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar}; +use group::Curve; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::borrow::Borrow; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub mod aggregation; +pub mod coin_indices_signatures; +pub mod expiration_date_signatures; +pub mod identify; +pub mod keygen; +pub mod setup; +pub mod withdrawal; + +/// The struct represents a partial wallet with essential components for a payment transaction. +/// +/// A `PartialWallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// `SignerIndex` (`idx`) indicating the signer's index, and an expiration date (`expiration_date`). +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct PartialWallet { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + idx: SignerIndex, + expiration_date: Scalar, +} + +impl PartialWallet { + pub fn signature(&self) -> &Signature { + &self.sig + } + + pub fn index(&self) -> SignerIndex { + self.idx + } + pub fn expiration_date(&self) -> Scalar { + self.expiration_date + } + + /// Converts the `PartialWallet` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 168 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), + /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 168]`) representing the serialized form of the `PartialWallet`. + /// + pub fn to_bytes(&self) -> [u8; 168] { + let mut bytes = [0u8; 168]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes()); + bytes[160..168].copy_from_slice(&self.idx.to_le_bytes()); + bytes + } + + /// Convert a byte slice into a `PartialWallet` instance. + /// + /// This function performs deserialization on the provided byte slice, which + /// represent a serialized `PartialWallet`. + /// + /// # Arguments + /// + /// * `bytes` - A reference to the byte slice to be deserialized. + /// + /// # Returns + /// + /// A `Result` containing the deserialized `PartialWallet` if successful, or a + /// `CompactEcashError` indicating the reason for failure. + pub fn from_bytes(bytes: &[u8]) -> Result { + const SIGNATURE_BYTES: usize = 96; + const V_BYTES: usize = 32; + const EXPIRATION_DATE_BYTES: usize = 32; + const IDX_BYTES: usize = 8; + const EXPECTED_LENGTH: usize = + SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + IDX_BYTES; + + if bytes.len() != EXPECTED_LENGTH { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PartialWallet".into(), + expected: EXPECTED_LENGTH, + actual: bytes.len(), + }); + } + + let mut j = 0; + + let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?; + j += SIGNATURE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap(); + let v = try_deserialize_scalar(v_bytes)?; + j += V_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap(); + let expiration_date = try_deserialize_scalar(expiration_date_bytes)?; + j += EXPIRATION_DATE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let idx_bytes = bytes[j..].try_into().unwrap(); + let idx = u64::from_le_bytes(idx_bytes); + + Ok(PartialWallet { + sig, + v, + idx, + expiration_date, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)] +pub struct Wallet { + /// The cryptographic materials required for producing spending proofs and payments. + signatures: WalletSignatures, + + /// Also known as `l` parameter in the paper + tickets_spent: u64, +} + +impl Wallet { + pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self { + Wallet { + signatures, + tickets_spent, + } + } + + pub fn into_wallet_signatures(self) -> WalletSignatures { + self.into() + } + + pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] { + let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8]; + bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes()); + bytes[WalletSignatures::SERIALISED_SIZE..] + .copy_from_slice(&self.tickets_spent.to_be_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Wallet".into(), + expected: WalletSignatures::SERIALISED_SIZE + 8, + actual: bytes.len(), + }); + } + + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..] + .try_into() + .unwrap(); + + let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?; + let tickets_spent = u64::from_be_bytes(tickets_bytes); + + Ok(Wallet { + signatures, + tickets_spent, + }) + } + + pub fn ensure_allowance( + params: &Parameters, + tickets_spent: u64, + spend_value: u64, + ) -> Result<()> { + if tickets_spent + spend_value > params.get_total_coins() { + Err(CompactEcashError::SpendExceedsAllowance { + spending: spend_value, + remaining: params.get_total_coins() - tickets_spent, + }) + } else { + Ok(()) + } + } + + pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> { + Self::ensure_allowance(params, self.tickets_spent, spend_value) + } + + #[allow(clippy::too_many_arguments)] + pub fn spend( + &mut self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + spend_value: u64, + valid_dates_signatures: &[ExpirationDateSignature], + coin_indices_signatures: &[CoinIndexSignature], + spend_date_timestamp: u64, + ) -> Result { + self.check_remaining_allowance(params, spend_value)?; + + // produce payment + let payment = self.signatures.spend( + params, + verification_key, + sk_user, + pay_info, + self.tickets_spent, + spend_value, + valid_dates_signatures, + coin_indices_signatures, + spend_date_timestamp, + )?; + + // update the ticket counter + self.tickets_spent += spend_value; + Ok(payment) + } +} + +impl From for WalletSignatures { + fn from(value: Wallet) -> Self { + value.signatures + } +} + +/// The struct represents a wallet with essential components for a payment transaction. +/// +/// A `Wallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// an expiration date (`expiration_date`) +/// and an u64 ('l') indicating the total number of spent coins. +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct WalletSignatures { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + expiration_date_timestamp: u64, +} + +impl WalletSignatures { + pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet { + Wallet { + signatures: self, + tickets_spent, + } + } + + pub fn new_wallet(self) -> Wallet { + self.with_tickets_spent(0) + } + + pub fn encoded_expiration_date(&self) -> Scalar { + date_scalar(self.expiration_date_timestamp) + } +} + +/// Computes the hash of payment information concatenated with a numeric value. +/// +/// This function takes a `PayInfo` structure and a numeric value `k`, and +/// concatenates the serialized `payinfo` field of `PayInfo` with the little-endian +/// byte representation of `k`. The resulting byte sequence is then hashed to produce +/// a scalar value using the `hash_to_scalar` function. +/// +/// # Arguments +/// +/// * `pay_info` - A reference to the `PayInfo` structure containing payment information. +/// * `k` - A numeric value used in the hash computation. +/// +/// # Returns +/// +/// A `Scalar` value representing the hash of the concatenated byte sequence. +/// +pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&pay_info.pay_info_bytes); + bytes.extend_from_slice(&k.to_le_bytes()); + hash_to_scalar(bytes) +} + +impl WalletSignatures { + // signature size (96) + secret size (32) + expiration size (8) + pub const SERIALISED_SIZE: usize = 136; + + pub fn signature(&self) -> &Signature { + &self.sig + } + + /// Converts the `WalletSignatures` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 168 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), and + /// expiration date (`expiration_date`) fields of the `WalletSignatures` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 136]`) representing the serialized form of the `Wallet`. + /// + pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] { + let mut bytes = [0u8; Self::SERIALISED_SIZE]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..136].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::SERIALISED_SIZE { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "WalletSignatures".into(), + expected: Self::SERIALISED_SIZE, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[128..].try_into().unwrap(); + + let sig = Signature::try_from(sig_bytes.as_slice())?; + let v = Scalar::from_bytes(v_bytes).unwrap(); + let expiration_date_timestamp = u64::from_be_bytes(expiration_date_bytes); + + Ok(WalletSignatures { + sig, + v, + expiration_date_timestamp, + }) + } + + /// Performs a spending operation with the given parameters, updating the wallet and generating a payment. + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key. + /// * `sk_user` - The secret key of the user who wants to spend from their wallet. + /// * `pay_info` - Unique information related to the payment. + /// * `current_tickets_spent` - The total number of tickets already spent in the associated wallet. + /// * `spend_value` - The amount to spend from the wallet. + /// * `valid_dates_signatures` - A list of **SORTED** signatures on valid dates during which we can spend from the wallet. + /// * `coin_indices_signatures` - A list of **SORTED** signatures on coin indices. + /// * `spend_date` - The date on which the spending occurs, expressed as unix timestamp. + /// + /// # Returns + /// + /// A tuple containing the generated payment and a reference to the updated wallet, or an error. + #[allow(clippy::too_many_arguments)] + pub fn spend( + &self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + current_tickets_spent: u64, + spend_value: u64, + valid_dates_signatures: &[BE], + coin_indices_signatures: &[BI], + spend_date_timestamp: u64, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + // Extract group parameters + let grp_params = params.grp(); + + if verification_key.beta_g2.is_empty() { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures); + } + + if coin_indices_signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::InsufficientNumberOfIndexSignatures); + } + + Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?; + + // Wallet attributes needed for spending + let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()]; + + // Randomize wallet signature + let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise(); + + // compute kappa (i.e., blinded attributes for show) to prove possession of the wallet signature + let kappa = compute_kappa( + grp_params, + verification_key, + &attributes, + sign_blinding_factor, + ); + + // Randomise the expiration date signature for the date when we want to perform the spending, and compute kappa_e to prove possession of + // the expiration signature + let date_signature_index = + find_index(spend_date_timestamp, self.expiration_date_timestamp)?; + + //SAFETY : find_index eiter returns a valid index or an error. The unwrap is therefore fine + #[allow(clippy::unwrap_used)] + let date_signature = valid_dates_signatures + .get(date_signature_index) + .unwrap() + .borrow(); + let (date_signature_prime, date_sign_blinding_factor) = + date_signature.blind_and_randomise(); + // compute kappa_e to prove possession of the expiration signature + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date(); + + // pick random openings o_c and compute commitments C to v (wallet secret) + let o_c = grp_params.random_scalar(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v; + + let mut aa: Vec = Default::default(); + let mut ss: Vec = Default::default(); + let mut tt: Vec = Default::default(); + let mut rr: Vec = Default::default(); + let mut o_a: Vec = Default::default(); + let mut o_mu: Vec = Default::default(); + let mut mu: Vec = Default::default(); + let r_k_vec: Vec = Default::default(); + let mut kappa_k_vec: Vec = Default::default(); + let mut lk_vec: Vec = Default::default(); + + let mut coin_indices_signatures_prime: Vec = Default::default(); + for k in 0..spend_value { + let lk = current_tickets_spent + k; + lk_vec.push(Scalar::from(lk)); + + // compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + + let o_a_k = grp_params.random_scalar(); + o_a.push(o_a_k); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let aa_k = + grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk); + aa.push(aa_k); + + // compute the serial numbers + let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?; + ss.push(ss_k); + // compute the identification tags + let tt_k = grp_params.gen1() * sk_user.sk + + pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k; + tt.push(tt_k); + + // compute values mu, o_mu, lambda, o_lambda + let maybe_mu_k: Option = (self.v + Scalar::from(lk) + Scalar::from(1)) + .invert() + .into(); + let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?; + mu.push(mu_k); + + let o_mu_k = ((o_a_k + o_c) * mu_k).neg(); + o_mu.push(o_mu_k); + + // Randomize the coin index signatures and compute kappa_k to prove possession of each coin's signature + // This involves iterating over the signatures corresponding to the coins we want to spend in this payment. + //SAFETY : Earlier `ensure_allowance` ensures we don't do out of of bound here + #[allow(clippy::unwrap_used)] + let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow(); + let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise(); + coin_indices_signatures_prime.push(coin_sign_prime); + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_k: G2Projective = grp_params.gen2() * coin_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * Scalar::from(lk); + kappa_k_vec.push(kappa_k); + } + + // construct the zkp proof + let spend_instance = SpendInstance { + kappa, + cc, + aa: aa.clone(), + ss: ss.clone(), + tt: tt.clone(), + kappa_k: kappa_k_vec.clone(), + kappa_e, + }; + let spend_witness = SpendWitness { + attributes: &attributes, + r: sign_blinding_factor, + o_c, + lk: lk_vec, + o_a, + mu, + o_mu, + r_k: r_k_vec, + r_e: date_sign_blinding_factor, + }; + + let zk_proof = SpendProof::construct( + &spend_instance, + &spend_witness, + verification_key, + &rr, + pay_info, + spend_value, + ); + + // output pay + let pay = Payment { + kappa, + kappa_e, + sig: signature_prime, + sig_exp: date_signature_prime, + kappa_k: kappa_k_vec.clone(), + omega: coin_indices_signatures_prime, + ss: ss.clone(), + tt: tt.clone(), + aa: aa.clone(), + spend_value, + cc, + zk_proof, + }; + + Ok(pay) + } +} + +fn pseudorandom_f_delta_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.delta() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +fn pseudorandom_f_g_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.gen1() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +/// Computes the value of kappa (blinded private attributes for show) for proving possession of the wallet signature. +/// +/// This function calculates the value of kappa, which is used to prove possession of the wallet signature in the zero-knowledge proof. +/// +/// # Arguments +/// +/// * `params` - A reference to the group parameters required for the computation. +/// * `verification_key` - The global verification key of the signing authorities. +/// * `attributes` - A slice of private attributes associated with the wallet. +/// * `blinding_factor` - The blinding factor used used to randomise the wallet's signature. +/// +/// # Returns +/// +/// A `G2Projective` element representing the computed value of kappa. +/// +fn compute_kappa( + params: &GroupParameters, + verification_key: &VerificationKeyAuth, + attributes: &[&Scalar], + blinding_factor: Scalar, +) -> G2Projective { + params.gen2() * blinding_factor + + verification_key.alpha + + attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(&priv_attr, beta_i)| beta_i * priv_attr) + .sum::() +} + +/// Represents the unique payment information associated with the payment. +/// The bytes representing the payment information encode the public key of the +/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes. +/// +/// # Fields +/// +/// * `payinfo_bytes` - An array of bytes representing the payment information. +/// +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct PayInfo { + pub pay_info_bytes: [u8; 72], +} + +impl Serialize for PayInfo { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + self.pay_info_bytes.to_vec().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PayInfo { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let pay_info_bytes = >::deserialize(deserializer)?; + Ok(PayInfo { + pay_info_bytes: pay_info_bytes + .try_into() + .map_err(|_| serde::de::Error::custom("invalid pay info bytes"))?, + }) + } +} + +impl Bytable for PayInfo { + fn to_byte_vec(&self) -> Vec { + self.pay_info_bytes.to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + if slice.len() != 72 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PayInfo".into(), + expected: 72, + actual: slice.len(), + }); + } + //safety : we checked that slices length is exactly 72, hence this unwrap won't fail + #[allow(clippy::unwrap_used)] + Ok(Self { + pay_info_bytes: slice.try_into().unwrap(), + }) + } +} + +impl Base58 for PayInfo {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Payment { + pub kappa: G2Projective, + pub kappa_e: G2Projective, + pub sig: Signature, + pub sig_exp: ExpirationDateSignature, + pub kappa_k: Vec, + pub omega: Vec, + pub ss: Vec, + pub tt: Vec, + pub aa: Vec, + pub spend_value: u64, + pub cc: G1Projective, + pub zk_proof: SpendProof, +} + +impl Payment { + /// Checks the validity of the payment signature. + /// + /// This function performs two checks to ensure the payment signature is valid: + /// - Verifies that the element `h` of the payment signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the signature and the payment (`h`, `kappa`, and `s`). + /// + /// # Arguments + /// + /// * `params` - A reference to the system parameters required for the checks. + /// + /// # Returns + /// + /// A `Result` indicating success if the signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment signature equals the identity. + /// - The bilinear pairing check for `kappa` fails. + /// + pub fn check_signature_validity(&self) -> Result<()> { + let params = ecash_group_parameters(); + if bool::from(self.sig.h.is_identity()) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + + if !check_bilinear_pairing( + &self.sig.h.to_affine(), + &G2Prepared::from(self.kappa.to_affine()), + &self.sig.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + Ok(()) + } + + /// Checks the validity of the expiration signature encoded in the payment given a spending date. + /// If the spending date is within the allowed range before the expiration date, the check is successful. + /// + /// This function performs two checks to ensure the payment expiration signature is valid: + /// - Verifies that the element `h` of the expiration signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the expiration signature and the payment (`h`, `kappa_e`, and `s`). + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key of the signing authorities. + /// * `spend_date` - The date associated with the payment. + /// + /// # Returns + /// + /// A `Result` indicating success if the expiration signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment expiration signature equals the identity. + /// - The bilinear pairing check for `kappa_e` fails. + /// + pub fn check_exp_signature_validity( + &self, + verification_key: &VerificationKeyAuth, + spend_date: Scalar, + ) -> Result<()> { + let grp_params = ecash_group_parameters(); + // Check if the element h of the payment expiration signature equals the identity. + if bool::from(self.sig_exp.h.is_identity()) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + // Calculate m1 and m2 values. + let m1: Scalar = spend_date; + let m2: Scalar = constants::TYPE_EXP; + + // Perform a bilinear pairing check for kappa_e + //SAFETY: we checked the size of beta_G2 earlier + let combined_kappa_e = + self.kappa_e + verification_key.beta_g2[1] * m1 + verification_key.beta_g2[2] * m2; + + if !check_bilinear_pairing( + &self.sig_exp.h.to_affine(), + &G2Prepared::from(combined_kappa_e.to_affine()), + &self.sig_exp.s.to_affine(), + grp_params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + Ok(()) + } + + /// Checks that all serial numbers in the payment are unique. + /// + /// This function verifies that each serial number in the payment's serial number array (`ss`) is unique. + /// + /// # Returns + /// + /// A `Result` indicating success if all serial numbers are unique or an error if any serial number is duplicated. + /// + /// # Errors + /// + /// An error is returned if not all serial numbers in the payment are unique. + /// + pub fn no_duplicate_serial_numbers(&self) -> Result<()> { + let mut seen_serial_numbers = Vec::new(); + + for serial_number in &self.ss { + if seen_serial_numbers.contains(serial_number) { + return Err(CompactEcashError::SpendDuplicateSerialNumber); + } + seen_serial_numbers.push(*serial_number); + } + + Ok(()) + } + + // /// Checks the validity of the coin index signature at a specific index. + // /// + // /// This function performs two checks to ensure the coin index signature at a given index (`k`) is valid: + // /// - Verifies that the element `h` of the coin index signature does not equal the identity. + // /// - Calculates a combined element for the bilinear pairing check involving `kappa_k`, and verifies the pairing with the coin index signature elements (`h`, `kappa_k`, and `s`). + // /// + // /// # Arguments + // /// + // /// * `verification_key` - The global verification key of the signing authorities. + // /// * `k` - The index at which to check the coin index signature. + // /// + // /// # Returns + // /// + // /// A `Result` indicating success if the coin index signature is valid or an error if any check fails. + // /// + // /// # Errors + // /// + // /// An error is returned if: + // /// - The element `h` of the coin index signature at the specified index equals the identity. + // /// - The bilinear pairing check for `kappa_k` at the specified index fails. + // /// - The specified index is out of bounds for the coin index signatures array (`omega`). + // /// + // pub fn check_coin_index_signature( + // &self, + // verification_key: &VerificationKeyAuth, + // k: u64, + // ) -> Result<()> { + // if let Some(coin_idx_sign) = self.omega.get(k as usize) { + // if bool::from(coin_idx_sign.h.is_identity()) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // if verification_key.beta_g2.len() < 3 { + // return Err(CompactEcashError::VerificationKeyTooShort); + // } + // //SAFETY: we checked the size of beta_G2 earlier + // #[allow(clippy::unwrap_used)] + // let combined_kappa_k = self.kappa_k[k as usize].to_affine() + // + verification_key.beta_g2.get(1).unwrap() * constants::TYPE_IDX + // + verification_key.beta_g2.get(2).unwrap() * constants::TYPE_IDX; + // + // if !check_bilinear_pairing( + // &coin_idx_sign.h.to_affine(), + // &G2Prepared::from(combined_kappa_k.to_affine()), + // &coin_idx_sign.s.to_affine(), + // ecash_group_parameters().prepared_miller_g2(), + // ) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // } else { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // Ok(()) + // } + + /// Checks the validity of all coin index signatures available. + pub fn batch_check_coin_index_signatures( + &self, + verification_key: &VerificationKeyAuth, + ) -> Result<()> { + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if self.omega.len() != self.kappa_k.len() { + return Err(CompactEcashError::SpendSignaturesVerification); + } + + let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX + + verification_key.beta_g2[2] * constants::TYPE_IDX; + + let mut pairing_terms = Vec::with_capacity(self.omega.len()); + for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) { + pairing_terms.push((sig, partially_signed + kappa_k)) + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::SpendSignaturesVerification); + } + Ok(()) + } + + /// Checks the validity of the attached zk proof of spending. + pub fn verify_spend_proof( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + ) -> Result<()> { + // Compute pay_info hash for each coin + let mut rr = Vec::with_capacity(self.spend_value as usize); + for k in 0..self.spend_value { + // Compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + } + + // verify the zk proof + let instance = SpendInstance { + kappa: self.kappa, + cc: self.cc, + aa: self.aa.clone(), + ss: self.ss.clone(), + tt: self.tt.clone(), + kappa_k: self.kappa_k.clone(), + kappa_e: self.kappa_e, + }; + + // verify the zk-proof + if !self + .zk_proof + .verify(&instance, verification_key, &rr, pay_info, self.spend_value) + { + return Err(CompactEcashError::SpendZKProofVerification); + } + + Ok(()) + } + + /// Verifies the validity of a spend transaction, including signature checks, + /// expiration date signature checks, serial number uniqueness, coin index signature checks, + /// and zero-knowledge proof verification. + /// + /// # Arguments + /// + /// * `params` - The cryptographic parameters. + /// * `verification_key` - The verification key used for validation. + /// * `pay_info` - The pay information associated with the transaction. + /// * `spend_date` - The date at which the spending transaction occurs. + /// + /// # Returns + /// + /// Returns `Ok(true)` if the spend transaction is valid; otherwise, returns an error. + pub fn spend_verify( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + spend_date: Scalar, + ) -> Result<()> { + // check if all serial numbers are different + self.no_duplicate_serial_numbers()?; + // verify the zk proof + self.verify_spend_proof(verification_key, pay_info)?; + // Verify whether the payment signature and kappa are correct + self.check_signature_validity()?; + // Verify whether the expiration date signature and kappa_e are correct + self.check_exp_signature_validity(verification_key, spend_date)?; + // Verify whether the coin indices signatures and kappa_k are correct + self.batch_check_coin_index_signatures(verification_key)?; + + Ok(()) + } + + pub fn encoded_serial_number(&self) -> Vec { + SerialNumberRef { inner: &self.ss }.to_bytes() + } + + pub fn serial_number_bs58(&self) -> String { + SerialNumberRef { inner: &self.ss }.to_bs58() + } + + // pub fn has_serial_number(&self, serial_number_bs58: &str) -> Result { + // let serial_number = SerialNumberRef::try_from_bs58(serial_number_bs58)?; + // let ret = self.ss.eq(&serial_number.inner); + // Ok(ret) + // } +} + +pub struct SerialNumberRef<'a> { + pub(crate) inner: &'a [G1Projective], +} + +impl<'a> SerialNumberRef<'a> { + pub fn to_bytes(&self) -> Vec { + let ss_len = self.inner.len(); + let mut bytes: Vec = Vec::with_capacity(ss_len * 48); + for s in self.inner { + bytes.extend_from_slice(&s.to_affine().to_compressed()); + } + bytes + } + + pub fn to_bs58(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/setup.rs b/common/nym_offline_compact_ecash/src/scheme/setup.rs new file mode 100644 index 0000000000..dd469fb009 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/setup.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::utils::hash_g1; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand::thread_rng; + +#[derive(Debug)] +pub struct GroupParameters { + /// Generator of the G1 group + g1: G1Affine, + /// Generator of the G2 group + g2: G2Affine, + /// Additional generators of the G1 group + gammas: Vec, + // Additional generator of the G1 group + delta: G1Projective, + /// Precomputed G2 generator used for the miller loop + _g2_prepared_miller: G2Prepared, +} + +impl GroupParameters { + pub fn new(attributes: usize) -> GroupParameters { + assert!(attributes > 0); + let gammas = (1..=attributes) + .map(|i| hash_g1(format!("gamma{}", i))) + .collect(); + + let delta = hash_g1("delta"); + + GroupParameters { + g1: G1Affine::generator(), + g2: G2Affine::generator(), + gammas, + delta, + _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 gammas(&self) -> &Vec { + &self.gammas + } + + pub(crate) fn gammas_to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.gammas.len() * 48); + for g in &self.gammas { + bytes.extend_from_slice(g.to_bytes().as_ref()); + } + bytes + } + + pub(crate) fn gamma_idx(&self, i: usize) -> Option<&G1Projective> { + self.gammas.get(i) + } + + pub(crate) fn delta(&self) -> &G1Projective { + &self.delta + } + + 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 { + (0..n).map(|_| self.random_scalar()).collect() + } + + pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared { + &self._g2_prepared_miller + } +} + +#[derive(Debug)] +pub struct Parameters { + /// Number of coins of fixed denomination in the credential wallet; L in construction + total_coins: u64, +} + +impl Parameters { + pub fn new(total_coins: u64) -> Parameters { + assert!(total_coins > 0); + Parameters { total_coins } + } + pub fn grp(&self) -> &GroupParameters { + ecash_group_parameters() + } + + pub fn get_total_coins(&self) -> u64 { + self.total_coins + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs new file mode 100644 index 0000000000..1cd116e611 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -0,0 +1,480 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::proofs::proof_withdrawal::{ + WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness, +}; +use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::GroupParameters; +use crate::scheme::PartialWallet; +use crate::utils::{check_bilinear_pairing, hash_g1}; +use crate::{constants, ecash_group_parameters, Attribute}; +use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar}; +use group::{Curve, Group, GroupEncoding}; +use serde::{Deserialize, Serialize}; +use std::ops::Neg; + +/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential. +/// +/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment, +/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request. +/// +/// # Fields +/// +/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element. +/// * `joined_commitment` - The joined commitment represented as a G1Projective element. +/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements. +/// * `zk_proof` - The zero-knowledge proof for the withdrawal request. +/// +/// # Derives +/// +/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality. +/// +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalRequest { + joined_commitment_hash: G1Projective, + joined_commitment: G1Projective, + private_attributes_commitments: Vec, + zk_proof: WithdrawalReqProof, +} + +impl WithdrawalRequest { + pub fn get_private_attributes_commitments(&self) -> &[G1Projective] { + &self.private_attributes_commitments + } +} + +/// Represents information associated with a withdrawal request. +/// +/// This structure holds the commitment hash, commitment opening, private attributes openings, +/// the wallet secret (scalar), and the expiration date related to a withdrawal request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestInfo { + joined_commitment_hash: G1Projective, + joined_commitment_opening: Scalar, + private_attributes_openings: Vec, + wallet_secret: Scalar, + expiration_date: Scalar, +} + +impl RequestInfo { + pub fn get_joined_commitment_hash(&self) -> &G1Projective { + &self.joined_commitment_hash + } + pub fn get_joined_commitment_opening(&self) -> &Scalar { + &self.joined_commitment_opening + } + pub fn get_private_attributes_openings(&self) -> &[Scalar] { + &self.private_attributes_openings + } + pub fn get_v(&self) -> &Scalar { + &self.wallet_secret + } + pub fn get_expiration_date(&self) -> &Scalar { + &self.expiration_date + } +} + +/// Computes Pedersen commitments for private attributes. +/// +/// Given a set of private attributes and the commitment hash for all attributes, +/// this function generates random blinding factors (`openings`) and computes corresponding +/// Pedersen commitments for each private attribute. +/// Pedersen commitments have the hiding and binding properties, providing a secure way +/// to represent private values in a commitment scheme. +/// +/// # Arguments +/// +/// * `params` - Group parameters for the cryptographic group. +/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments. +/// * `private_attributes` - A slice of private attributes to be committed. +/// +/// # Returns +/// +/// A tuple containing vectors of blinding factors (`openings`) and corresponding +/// Pedersen commitments for each private attribute. +fn compute_private_attribute_commitments( + params: &GroupParameters, + joined_commitment_hash: &G1Projective, + private_attributes: &[Scalar], +) -> (Vec, Vec) { + let (openings, commitments): (Vec, Vec) = private_attributes + .iter() + .map(|m_j| { + let o_j = params.random_scalar(); + (o_j, params.gen1() * o_j + joined_commitment_hash * m_j) + }) + .unzip(); + + (openings, commitments) +} + +/// Generates a withdrawal request for the given user to request a zk-nym credential wallet. +/// +/// # Arguments +/// +/// * `sk_user` - A reference to the user's secret key. +/// * `expiration_date` - The expiration date for the withdrawal request. +/// +/// # Returns +/// +/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails. +/// +/// # Details +/// +/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes, +/// including public (expiration date) and private ones (user secret key and wallet secret). +/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes. +/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes. +/// +/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private +/// attributes, and the constructed zero-knowledge proof. +/// +/// The associated `RequestInfo` includes information such as commitment hash, commitment opening, +/// openings for private attributes, `v`, and the expiration date. +pub fn withdrawal_request( + sk_user: &SecretKeyUser, + expiration_date: u64, +) -> Result<(WithdrawalRequest, RequestInfo)> { + let params = ecash_group_parameters(); + // Generate random and unique wallet secret + let v = params.random_scalar(); + let joined_commitment_opening = params.random_scalar(); + // Compute joined commitment for all attributes (public and private) + //SAFETY: params is static with length 3 + #[allow(clippy::unwrap_used)] + let joined_commitment: G1Projective = params.gen1() * joined_commitment_opening + + params.gamma_idx(0).unwrap() * sk_user.sk + + params.gamma_idx(1).unwrap() * v; + + // Compute commitment hash h + #[allow(clippy::unwrap_used)] + let joined_commitment_hash = hash_g1( + (joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) + .to_bytes(), + ); + + // Compute Pedersen commitments for private attributes (wallet secret and user's secret) + let private_attributes = vec![sk_user.sk, v]; + let (private_attributes_openings, private_attributes_commitments) = + compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes); + + // construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2 + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments: private_attributes_commitments.clone(), + pk_user: PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + + // Create and return WithdrawalRequest and RequestInfo + Ok(( + WithdrawalRequest { + joined_commitment_hash, + joined_commitment, + private_attributes_commitments, + zk_proof, + }, + RequestInfo { + joined_commitment_hash, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + wallet_secret: v, + expiration_date: Scalar::from(expiration_date), + }, + )) +} + +/// Verifies the integrity of a withdrawal request, including the joined commitment hash +/// and the zero-knowledge proof of knowledge. +/// +/// # Arguments +/// +/// * `req` - The withdrawal request to be verified. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `expiration_date` - Expiration date for the withdrawal request. +/// +/// # Returns +/// +/// Returns `Ok(true)` if the verification is successful, otherwise returns an error +/// with a specific message indicating the verification failure. +pub fn request_verify( + req: &WithdrawalRequest, + pk_user: PublicKeyUser, + expiration_date: u64, +) -> Result<()> { + let params = ecash_group_parameters(); + // Verify the joined commitment hash + //SAFETY: params is static with length 3 + #[allow(clippy::unwrap_used)] + let expected_commitment_hash = hash_g1( + (req.joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) + .to_bytes(), + ); + if req.joined_commitment_hash != expected_commitment_hash { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + // Verify zk proof + let instance = WithdrawalReqInstance { + joined_commitment: req.joined_commitment, + joined_commitment_hash: req.joined_commitment_hash, + private_attributes_commitments: req.private_attributes_commitments.clone(), + pk_user, + }; + if !req.zk_proof.verify(&instance) { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + Ok(()) +} + +/// Signs an expiration date using a joined commitment hash and a secret key. +/// +/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`), +/// and a secret key for authentication (`sk_auth`), this function computes the signature of the +/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key +/// and the expiration date. +/// +/// # Arguments +/// +/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash. +/// * `expiration_date` - The expiration date timestamp to be signed. +/// * `sk_auth` - The secret key of the signing authority. Assumes key is long enough. +/// +/// # Returns +/// +/// A `Result` containing the resulting G1Projective point if successful, or an error if the +/// authentication secret key index is out of bounds. +fn sign_expiration_date( + joined_commitment_hash: &G1Projective, + expiration_date: u64, + sk_auth: &SecretKeyAuth, +) -> G1Projective { + //SAFETY : this fn assumes a long enough key + #[allow(clippy::unwrap_used)] + let yi = sk_auth.get_y_by_idx(2).unwrap(); + joined_commitment_hash * (yi * Scalar::from(expiration_date)) +} + +/// Issues a blinded signature for a withdrawal request, after verifying its integrity. +/// +/// This function first verifies the withdrawal request using the provided group parameters, +/// user's public key, and expiration date. If the verification is successful, +/// the function proceeds to blind sign the private attributes and sign the expiration date, +/// combining both signatures into a final signature. +/// +/// # Arguments +/// +/// * `sk_auth` - Secret key of the signing authority. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `withdrawal_req` - The withdrawal request to be signed. +/// * `expiration_date` - Expiration date for the withdrawal request. +/// +/// # Returns +/// +/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue( + sk_auth: &SecretKeyAuth, + pk_user: PublicKeyUser, + withdrawal_req: &WithdrawalRequest, + expiration_date: u64, +) -> Result { + // Verify the withdrawal request + request_verify(withdrawal_req, pk_user, expiration_date)?; + // Verify `sk_auth` is long enough + if sk_auth.ys.len() < constants::ATTRIBUTES_LEN { + return Err(CompactEcashError::KeyTooShort); + } + // Blind sign the private attributes + let blind_signatures: G1Projective = withdrawal_req + .private_attributes_commitments + .iter() + .zip(sk_auth.ys.iter().take(2)) + .map(|(pc, yi)| pc * yi) + .sum(); + // Sign the expiration date + //SAFETY: key length was verified before + let expiration_date_sign = sign_expiration_date( + &withdrawal_req.joined_commitment_hash, + expiration_date, + sk_auth, + ); + // Combine both signatures + let signature = + blind_signatures + withdrawal_req.joined_commitment_hash * sk_auth.x + expiration_date_sign; + + Ok(BlindedSignature { + h: withdrawal_req.joined_commitment_hash, + c: signature, + }) +} + +/// Verifies the integrity and correctness of a blinded signature +/// and returns an unblinded partial zk-nym wallet. +/// +/// This function first verifies the integrity of the received blinded signature by checking +/// if the joined commitment hash matches the one provided in the `req_info`. If the verification +/// is successful, it proceeds to unblind the blinded signature and verify its correctness. +/// +/// # Arguments +/// +/// * `vk_auth` - Verification key of the signing authority. +/// * `sk_user` - Secret key of the user. +/// * `blind_signature` - Blinded signature received from the authority. +/// * `req_info` - Information associated with the request, including the joined commitment hash, +/// private attributes openings, v, and expiration date. +/// +/// # Returns +/// +/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue_verify( + vk_auth: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + blind_signature: &BlindedSignature, + req_info: &RequestInfo, + signer_index: SignerIndex, +) -> Result { + let params = ecash_group_parameters(); + // Verify the integrity of the response from the authority + if req_info.joined_commitment_hash != blind_signature.h { + return Err(CompactEcashError::IssuanceVerification); + } + + // Unblind the blinded signature on the partial signature + let blinding_removers = vk_auth + .beta_g1 + .iter() + .zip(&req_info.private_attributes_openings) + .map(|(beta, opening)| beta * opening) + .sum::(); + let unblinded_c = blind_signature.c - blinding_removers; + + let attr = [sk_user.sk, req_info.wallet_secret, req_info.expiration_date]; + + let signed_attributes = attr + .iter() + .zip(vk_auth.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + // Verify the signature correctness on the wallet share + if !check_bilinear_pairing( + &blind_signature.h.to_affine(), + &G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()), + &unblinded_c.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::IssuanceVerification); + } + + Ok(PartialWallet { + sig: Signature { + h: blind_signature.h, + s: unblinded_c, + }, + v: req_info.wallet_secret, + idx: signer_index, + expiration_date: req_info.expiration_date, + }) +} + +/// Verifies a partial blind signature using the provided parameters and validator's verification key. +/// +/// # Arguments +/// +/// * `blind_sign_request` - A reference to the blind signature request signed by the client. +/// * `public_attributes` - A reference to the public attributes included in the client's request. +/// * `blind_sig` - A reference to the issued partial blinded signature to be verified. +/// * `partial_verification_key` - A reference to the validator's partial verification key. +/// +/// # Returns +/// +/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`). +/// +/// # Remarks +/// +/// This function verifies the correctness and validity of a partial blind signature using +/// the provided cryptographic parameters, blind signature request, blinded signature, +/// and partial verification key. +/// It calculates pairings based on the provided values and checks whether the partial blind signature +/// is consistent with the verification key and commitments in the blind signature request. +/// The function returns `true` if the partial blind signature is valid, and `false` otherwise. +pub fn verify_partial_blind_signature( + private_attribute_commitments: &[G1Projective], + public_attributes: &[&Attribute], + blind_sig: &BlindedSignature, + partial_verification_key: &VerificationKeyAuth, +) -> bool { + let params = ecash_group_parameters(); + let num_private_attributes = private_attribute_commitments.len(); + if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { + return false; + } + + // TODO: we're losing some memory here due to extra allocation, + // but worst-case scenario (given SANE amount of attributes), it's just few kb at most + let c_neg = blind_sig.c.to_affine().neg(); + let g2_prep = params.prepared_miller_g2(); + + let mut terms = vec![ + // (c^{-1}, g2) + (c_neg, g2_prep.clone()), + // (s, alpha) + ( + blind_sig.h.to_affine(), + G2Prepared::from(partial_verification_key.alpha.to_affine()), + ), + ]; + + // for each private attribute, add (cm_i, beta_i) to the miller terms + for (private_attr_commit, beta_g2) in private_attribute_commitments + .iter() + .zip(&partial_verification_key.beta_g2) + { + // (cm_i, beta_i) + terms.push(( + private_attr_commit.to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms + for (&pub_attr, beta_g2) in public_attributes.iter().zip( + partial_verification_key + .beta_g2 + .iter() + .skip(num_private_attributes), + ) { + // (s^pub_j, beta_j) + terms.push(( + (blind_sig.h * pub_attr).to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // get the references to all the terms to get the arguments the miller loop expects + #[allow(clippy::map_identity)] + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + // since checking whether e(a, b) == e(c, d) + // is equivalent to checking e(a, b) • e(c, d)^{-1} == id + // and thus to e(a, b) • e(c^{-1}, d) == id + // + // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) + multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into() +} diff --git a/common/nym_offline_compact_ecash/src/tests/e2e.rs b/common/nym_offline_compact_ecash/src/tests/e2e.rs new file mode 100644 index 0000000000..1ce1f84b08 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/e2e.rs @@ -0,0 +1,135 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use crate::error::Result; + use crate::scheme::aggregation::{aggregate_verification_keys, aggregate_wallets}; + use crate::scheme::expiration_date_signatures::date_scalar; + use crate::scheme::keygen::{ + generate_keypair_user, ttp_keygen, SecretKeyAuth, VerificationKeyAuth, + }; + use crate::scheme::withdrawal::{issue, issue_verify, withdrawal_request, WithdrawalRequest}; + use crate::scheme::{PartialWallet, PayInfo, Payment, Wallet}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use itertools::izip; + + #[test] + fn main() -> Result<()> { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // generate authorities keys + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3]))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // request a wallet + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let req_bytes = req.to_bytes(); + let req2 = WithdrawalRequest::try_from(req_bytes.as_slice()).unwrap(); + assert_eq!(req, req2); + + // issue partial wallets + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + let partial_wallet = unblinded_wallet_shares.first().unwrap().clone(); + let partial_wallet_bytes = partial_wallet.to_bytes(); + let partial_wallet2 = PartialWallet::try_from(&partial_wallet_bytes[..]).unwrap(); + assert_eq!(partial_wallet, partial_wallet2); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + let wallet_bytes = aggr_wallet.to_bytes(); + let wallet = Wallet::from_bytes(&wallet_bytes).unwrap(); + assert_eq!(aggr_wallet, wallet); + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + assert!(payment + .spend_verify(&verification_key, &pay_info, date_scalar(spend_date)) + .is_ok()); + + let payment_bytes = payment.to_bytes(); + let payment2 = Payment::try_from(&payment_bytes[..]).unwrap(); + assert_eq!(payment, payment2); + + Ok(()) + } +} diff --git a/common/nym_offline_compact_ecash/src/tests/helpers.rs b/common/nym_offline_compact_ecash/src/tests/helpers.rs new file mode 100644 index 0000000000..4e98e7b060 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/helpers.rs @@ -0,0 +1,178 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::Result; +use crate::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, CoinIndexSignature, CoinIndexSignatureShare, +}; +use crate::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature, + ExpirationDateSignatureShare, +}; +use crate::scheme::keygen::{KeyPairAuth, SecretKeyAuth}; +use crate::scheme::Payment; +use crate::setup::Parameters; +use crate::{ + aggregate_verification_keys, aggregate_wallets, constants, generate_keypair_user, issue, + issue_verify, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth, +}; +use itertools::izip; + +pub fn generate_expiration_date_signatures( + expiration_date: u64, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + //Test helpers + #[allow(clippy::unwrap_used)] + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + let combined_data: Vec<_> = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_expiration_signatures(verification_key, expiration_date, &combined_data) +} + +pub fn generate_coin_indices_signatures( + params: &Parameters, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + // create the partial signatures from each authority + //Test helpers + #[allow(clippy::unwrap_used)] + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_indices_signatures(params, verification_key, &combined_data) +} + +pub fn payment_from_keys_and_expiration_date( + ecash_keypairs: &Vec, + indices: &[SignerIndex], + expiration_date: u64, +) -> Result<(Payment, PayInfo)> { + let total_coins = 32; + let params = Parameters::new(total_coins); + let spend_date = expiration_date - 29 * constants::SECONDS_PER_DAY; + let user_keypair = generate_keypair_user(); + + let secret_keys_authorities: Vec<&SecretKeyAuth> = ecash_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = ecash_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let verification_key = aggregate_verification_keys(&verification_keys_auth, Some(indices))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + // request a wallet + let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + + // generate blinded signatures + let mut wallet_blinded_signatures = Vec::new(); + + for keypair in ecash_keypairs { + let blinded_signature = issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + )?; + wallet_blinded_signatures.push(blinded_signature) + } + + // Unblind + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + Ok((payment, pay_info)) +} diff --git a/common/nym_offline_compact_ecash/src/tests/mod.rs b/common/nym_offline_compact_ecash/src/tests/mod.rs new file mode 100644 index 0000000000..6c3d2a3b0d --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod e2e; +pub mod helpers; diff --git a/common/nym_offline_compact_ecash/src/traits.rs b/common/nym_offline_compact_ecash/src/traits.rs new file mode 100644 index 0000000000..a9a22144b9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/traits.rs @@ -0,0 +1,140 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof}; +use crate::proofs::proof_withdrawal::{WithdrawalReqInstance, WithdrawalReqProof}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{Payment, WalletSignatures}; +use crate::{Attribute, CompactEcashError, PartialWallet, WithdrawalRequest}; +use bls12_381::{G1Affine, G1Projective}; +use group::GroupEncoding; + +#[macro_export] +macro_rules! impl_byteable_bs58 { + ($typ:ident) => { + impl $crate::traits::Bytable for $typ { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> $crate::error::Result { + Self::from_bytes(slice) + } + } + + impl $crate::traits::Base58 for $typ {} + + impl TryFrom<&[u8]> for $typ { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> $crate::error::Result { + Self::from_bytes(bytes) + } + } + }; +} + +macro_rules! impl_complex_binary_bytable { + ($typ:ident) => { + impl $typ { + pub fn to_bytes(&self) -> Vec { + use bincode::Options; + + // all of our manually derived types correctly serialise into bincode + #[allow(clippy::unwrap_used)] + crate::binary_serialiser().serialize(self).unwrap() + } + + pub fn from_bytes(bytes: &[u8]) -> crate::error::Result { + use bincode::Options; + crate::binary_serialiser() + .deserialize(bytes) + .map_err(|source| CompactEcashError::BinaryDeserialisationFailure { + type_name: std::any::type_name::<$typ>().to_string(), + source, + }) + } + } + + impl_byteable_bs58!($typ); + }; +} + +pub trait Bytable +where + Self: Sized, +{ + fn to_byte_vec(&self) -> Vec; + + fn try_from_byte_slice(slice: &[u8]) -> Result; +} + +pub trait Base58 +where + Self: Bytable, +{ + fn try_from_bs58>(x: S) -> Result { + Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec()?) + } + fn to_bs58(&self) -> String { + bs58::encode(self.to_byte_vec()).into_string() + } +} + +impl Bytable for G1Projective { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().as_ref().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let bytes = slice + .try_into() + .map_err(|_| CompactEcashError::G1ProjectiveDeserializationFailure)?; + + let maybe_g1 = G1Affine::from_compressed(&bytes); + if maybe_g1.is_none().into() { + Err(CompactEcashError::G1ProjectiveDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_g1.unwrap().into()) + } + } +} + +impl Base58 for G1Projective {} + +impl Bytable for Attribute { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let maybe_attribute = Attribute::from_bytes( + slice + .try_into() + .map_err(|_| CompactEcashError::ScalarDeserializationFailure)?, + ); + if maybe_attribute.is_none().into() { + Err(CompactEcashError::ScalarDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_attribute.unwrap()) + } + } +} + +impl_byteable_bs58!(Signature); +impl_byteable_bs58!(BlindedSignature); +impl_byteable_bs58!(WalletSignatures); +impl_byteable_bs58!(PartialWallet); + +impl_complex_binary_bytable!(SpendProof); +impl_complex_binary_bytable!(SpendInstance); +impl_complex_binary_bytable!(WithdrawalReqProof); +impl_complex_binary_bytable!(WithdrawalReqInstance); +impl_complex_binary_bytable!(Payment); +impl_complex_binary_bytable!(WithdrawalRequest); +impl_complex_binary_bytable!(RequestInfo); diff --git a/common/nym_offline_compact_ecash/src/utils.rs b/common/nym_offline_compact_ecash/src/utils.rs new file mode 100644 index 0000000000..57a648fff2 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/utils.rs @@ -0,0 +1,404 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::setup::GroupParameters; +use crate::{ecash_group_parameters, Signature, VerificationKeyAuth}; +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField}; +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar, +}; +use core::iter::Sum; +use core::ops::Mul; +use ff::Field; +use group::{Curve, Group}; +use itertools::Itertools; +use std::borrow::Borrow; +use std::ops::Neg; + +pub struct Polynomial { + coefficients: Vec, +} + +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: &GroupParameters, 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().unwrap_u8() == 1 { + // we checked that coefficients are not empty so unwrap here is fine + #[allow(clippy::unwrap_used)] + *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] +pub fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec { + 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 + //SAFETY: denominator start as one, and (xi-xj) is guaranteed to be non zero, as we force i != j + 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( + points: &[SignerIndex], + values: &[T], +) -> Result +where + T: Sum, + for<'a> &'a T: Mul, +{ + if points.is_empty() || values.is_empty() { + return Err(CompactEcashError::InterpolationSetSize); + } + + if points.len() != values.len() { + return Err(CompactEcashError::InterpolationSetSize); + } + + let coefficients = generate_lagrangian_coefficients_at_origin(points); + + Ok(coefficients + .into_iter() + .zip(values.iter()) + .map(|(coeff, val)| val * coeff) + .sum()) +} + +//domain name following https://www.rfc-editor.org/rfc/rfc9380.html#name-domain-separation-requireme recommendation +const G1_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; +const SCALAR_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-expander-SHA256"; + +pub fn hash_g1>(msg: M) -> G1Projective { + >>::hash_to_curve(msg, G1_HASH_DOMAIN) +} + +pub fn hash_to_scalar>(msg: M) -> Scalar { + let mut output = vec![Scalar::zero()]; + + Scalar::hash_to_field::>( + msg.as_ref(), + SCALAR_HASH_DOMAIN, + &mut output, + ); + output[0] +} + +pub fn try_deserialize_scalar_vec(expected_len: u64, bytes: &[u8]) -> Result> { + if bytes.len() != expected_len as usize * 32 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Scalar vector".into(), + expected: expected_len as usize * 32, + actual: bytes.len(), + }); + } + + let mut out = Vec::with_capacity(expected_len as usize); + for i in 0..expected_len as usize { + //SAFETY : casting 32 len slice into 32 len array + #[allow(clippy::unwrap_used)] + let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap(); + let s = match Scalar::from_bytes(&s_bytes).into() { + None => return Err(CompactEcashError::ScalarDeserializationFailure), + Some(scalar) => scalar, + }; + out.push(s) + } + + Ok(out) +} + +pub fn try_deserialize_scalar(bytes: &[u8; 32]) -> Result { + Into::>::into(Scalar::from_bytes(bytes)) + .ok_or(CompactEcashError::ScalarDeserializationFailure) +} + +pub fn try_deserialize_g1_projective(bytes: &[u8; 48]) -> Result { + Into::>::into(G1Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G1ProjectiveDeserializationFailure) + .map(G1Projective::from) +} + +pub fn try_deserialize_g2_projective(bytes: &[u8; 96]) -> Result { + Into::>::into(G2Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G2ProjectiveDeserializationFailure) + .map(G2Projective::from) +} + +/// 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() +} + +// compute e(h1, X1) * e(s1, g2^-1) * ... == id +// pub fn batch_verify_signatures(iter: Vec<(&Signature, G2Projective)>) -> bool { +pub fn batch_verify_signatures(iter: impl Iterator) -> bool +where + T: Borrow<(S, G2)>, + S: Borrow, + G2: Borrow, +{ + let mut miller_terms_owned = Vec::new(); + for t in iter { + let (sig, q) = t.borrow(); + let sig = sig.borrow(); + miller_terms_owned.push(( + sig.h.to_affine(), + G2Prepared::from(q.borrow().to_affine()), + sig.s.to_affine(), + )); + } + + let params = ecash_group_parameters(); + let g2_prep_neg = G2Prepared::from(params.gen2().neg()); + + let mut miller_terms = Vec::with_capacity(miller_terms_owned.len() * 2); + for (h, q, s) in &miller_terms_owned { + miller_terms.push((h, q)); + miller_terms.push((s, &g2_prep_neg)); + } + + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn check_vk_pairing( + params: &GroupParameters, + dkg_values: &[G2Projective], + vk: &VerificationKeyAuth, +) -> bool { + let values_len = dkg_values.len(); + if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { + return false; + } + + // safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine + #[allow(clippy::unwrap_used)] + if &vk.alpha != *dkg_values.first().as_ref().unwrap() { + return false; + } + let dkg_betas = &dkg_values[1..]; + if dkg_betas + .iter() + .zip(vk.beta_g2.iter()) + .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) + { + return false; + } + + let mut owned_miller_terms = vec![]; + for (i, (g1, g2)) in vk.beta_g1.iter().zip(vk.beta_g2.iter()).enumerate() { + if i % 2 == 0 { + owned_miller_terms.push((g1.to_affine(), G2Prepared::from(g2.to_affine()))); + } else { + // negate every other g1 element + owned_miller_terms.push((g1.neg().to_affine(), G2Prepared::from(g2.to_affine()))); + } + } + + // if our key has odd length, make sure to include the generators to correctly compute the final exponentiation + if owned_miller_terms.len() % 2 == 1 { + let neg_g1 = params.gen1().neg(); + let g2_prep = params.prepared_miller_g2(); + owned_miller_terms.push((neg_g1, g2_prep.to_owned())) + } + + let mut miller_terms = Vec::new(); + for ((p, s), (r, q)) in owned_miller_terms.iter().tuples::<(_, _)>() { + miller_terms.push((p, q)); + miller_terms.push((r, s)); + } + + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... == id + // in case of odd-length key check: + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... * e(g1^-z, g2) * e(g1^1, g2^z) == id + // (this is more than 2x as fast as checking each pairing individually for key of size 5) + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +#[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)); + } +} diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs index e550791969..debda49677 100644 --- a/common/nymcoconut/benches/benchmarks.rs +++ b/common/nymcoconut/benches/benchmarks.rs @@ -33,6 +33,40 @@ fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g )) } +#[allow(unused)] +fn bench_pairings(c: &mut Criterion) { + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + c.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + c.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); +} + #[allow(unused)] fn multi_miller_pairing_with_prepared( g11: &G1Affine, @@ -125,43 +159,9 @@ impl BenchCase { } } -#[allow(unused)] -fn bench_pairings(c: &mut Criterion) { - let mut rng = rand::thread_rng(); - - let g1 = G1Affine::generator(); - let g2 = G2Affine::generator(); - let r = Scalar::random(&mut rng); - let s = Scalar::random(&mut rng); - - let g11 = (g1 * r).to_affine(); - let g21 = (g2 * s).to_affine(); - let g21_prep = G2Prepared::from(g21); - - let g12 = (g1 * s).to_affine(); - let g22 = (g2 * r).to_affine(); - let g22_prep = G2Prepared::from(g22); - - c.bench_function("double pairing", |b| { - b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller in affine", |b| { - b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller with prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) - }); - - c.bench_function("multi miller with semi-prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) - }); -} - fn bench_coconut(c: &mut Criterion) { let mut group = c.benchmark_group("benchmark-coconut"); - group.measurement_time(Duration::from_secs(100)); + group.measurement_time(Duration::from_secs(1000)); let case = BenchCase { num_authorities: 100, threshold_p: 0.7, diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 3eed2bf88c..dbcbda5b39 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -47,7 +47,7 @@ mod proofs; mod scheme; pub mod tests; mod traits; -mod utils; +pub mod utils; pub type Attribute = bls12_381::Scalar; pub type PrivateAttribute = Attribute; diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 819156cb23..d12707408c 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -122,7 +122,7 @@ const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_R // 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>(msg: M) -> G1Projective { +pub fn hash_g1>(msg: M) -> G1Projective { >>::hash_to_curve(msg, G1_HASH_DOMAIN) } @@ -137,7 +137,7 @@ pub fn hash_to_scalar>(msg: M) -> Scalar { output[0] } -pub(crate) fn try_deserialize_scalar_vec( +pub fn try_deserialize_scalar_vec( expected_len: u64, bytes: &[u8], err: CoconutError, @@ -161,23 +161,17 @@ pub(crate) fn try_deserialize_scalar_vec( Ok(out) } -pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { +pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { Into::>::into(Scalar::from_bytes(bytes)).ok_or(err) } -pub(crate) fn try_deserialize_g1_projective( - bytes: &[u8; 48], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result { Into::>::into(G1Affine::from_compressed(bytes)) .ok_or(err) .map(G1Projective::from) } -pub(crate) fn try_deserialize_g2_projective( - bytes: &[u8; 96], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result { Into::>::into(G2Affine::from_compressed(bytes)) .ok_or(err) .map(G2Projective::from) diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index a89bc628c5..bd36158229 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; pub mod traits; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct KeyPairPath { pub private_key_path: PathBuf, pub public_key_path: PathBuf, diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml new file mode 100644 index 0000000000..f6907190b3 --- /dev/null +++ b/common/serde-helpers/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "serde-helpers" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +serde = { workspace = true, default-features = false } +bs58 = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } + + +[features] +bs58 = ["dep:bs58"] +base64 = ["dep:base64"] diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs new file mode 100644 index 0000000000..f90a65a2f4 --- /dev/null +++ b/common/serde-helpers/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "base64")] +pub mod base64 { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + STANDARD.decode(s).map_err(serde::de::Error::custom) + } +} + +#[cfg(feature = "bs58")] +pub mod bs58 { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::bs58::encode(bytes).into_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom) + } +} diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index a7e4176d7a..9179a0170b 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -65,7 +65,7 @@ impl TransactionDetails { #[derive(Deserialize, Serialize, Debug)] pub struct TransactionExecuteResult { pub logs_json: String, - pub data_json: String, + pub msg_responses_json: String, pub transaction_hash: String, pub gas_info: GasInfo, pub fee: Option, @@ -79,7 +79,7 @@ impl TransactionExecuteResult { Ok(TransactionExecuteResult { gas_info: value.gas_info.into(), transaction_hash: value.transaction_hash.to_string(), - data_json: ::serde_json::to_string_pretty(&value.data)?, + msg_responses_json: ::serde_json::to_string_pretty(&value.msg_responses)?, logs_json: ::serde_json::to_string_pretty(&value.logs)?, fee, }) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c1440b1c6f..e51183c7a7 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -242,6 +242,21 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_panic" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cosmwasm-crypto" version = "1.4.3" @@ -438,9 +453,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" dependencies = [ "anyhow", "cosmwasm-std", @@ -744,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -797,6 +812,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "ff" version = "0.12.1" @@ -891,6 +912,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hex" version = "0.4.3" @@ -931,6 +958,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + [[package]] name = "inout" version = "0.1.3" @@ -987,6 +1024,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" + [[package]] name = "libc" version = "0.2.153" @@ -1151,6 +1215,45 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash" +version = "0.1.0" +dependencies = [ + "bs58 0.4.0", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "cw4", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-multisig-contract-common", + "rand_chacha", + "schemars", + "semver", + "serde", + "sylvia", + "thiserror", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" @@ -1333,6 +1436,40 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1561,6 +1698,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -1724,6 +1870,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sylvia" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33388920659b494dab887f3bb40ebb071c602750597575034bea7c63ab12800" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "derivative", + "konst", + "schemars", + "serde", + "serde-cw-value", + "serde-json-wasm", + "sylvia-derive", +] + +[[package]] +name = "sylvia-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8031f53dbfda341acd7bd321e10d0d684b673324145026e23705da4b6d5c4919" +dependencies = [ + "convert_case", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1814,18 +1993,56 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "vergen" version = "8.3.1" @@ -1853,6 +2070,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 175bbcc767..a7c49c9099 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,8 @@ resolver = "2" members = [ "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", + "ecash", "mixnet", "mixnet-vesting-integration-tests", "multisig/cw3-flex-multisig", @@ -38,7 +39,7 @@ cosmwasm-schema = "=1.4.3" cosmwasm-std = "=1.4.3" cosmwasm-storage = "=1.4.3" cw-controllers = "=1.1.0" -cw-multi-test = "=0.16.4" +cw-multi-test = "=0.16.5" cw-storage-plus = "=1.2.0" cw-utils = "=1.0.1" cw2 = "=1.1.2" @@ -48,5 +49,7 @@ cw4 = "=1.1.2" cw20 = "=1.1.2" semver = "1.0.21" serde = "1.0.196" +sylvia = "0.8.0" +schemars = "0.8.16" thiserror = "1.0.48" diff --git a/contracts/coconut-dkg/schema/nym-coconut-dkg.json b/contracts/coconut-dkg/schema/nym-coconut-dkg.json index 43f3a69975..b33212190b 100644 --- a/contracts/coconut-dkg/schema/nym-coconut-dkg.json +++ b/contracts/coconut-dkg/schema/nym-coconut-dkg.json @@ -374,6 +374,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1843,6 +1866,13 @@ } } }, + "get_epoch_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "get_registered_dealer": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "RegisteredDealerDetails", diff --git a/contracts/coconut-dkg/schema/raw/query.json b/contracts/coconut-dkg/schema/raw/query.json index 72e1e095cd..386632565c 100644 --- a/contracts/coconut-dkg/schema/raw/query.json +++ b/contracts/coconut-dkg/schema/raw/query.json @@ -41,6 +41,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json new file mode 100644 index 0000000000..7b729a7b96 --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 +} diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index a1d84337af..acc1917b8d 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -13,8 +13,9 @@ use crate::dealings::queries::{ use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata}; use crate::epoch_state::queries::{ query_can_advance_state, query_current_epoch, query_current_epoch_threshold, + query_epoch_threshold, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::{ try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing, }; @@ -134,6 +135,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&query_current_epoch_threshold(deps.storage)?)? } + QueryMsg::GetEpochThreshold { epoch_id } => { + to_binary(&query_epoch_threshold(deps.storage, epoch_id)?)? + } QueryMsg::GetRegisteredDealer { dealer_address, epoch_id, @@ -217,6 +221,13 @@ pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; use cosmwasm_std::{Env, Storage}; -use nym_coconut_dkg_common::types::{Epoch, EpochState, StateAdvanceResponse}; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, StateAdvanceResponse}; pub(crate) fn query_can_advance_state( storage: &dyn Storage, @@ -45,6 +45,13 @@ pub(crate) fn query_current_epoch_threshold( Ok(THRESHOLD.may_load(storage)?) } +pub(crate) fn query_epoch_threshold( + storage: &dyn Storage, + epoch_id: EpochId, +) -> Result, ContractError> { + Ok(EPOCH_THRESHOLDS.may_load(storage, epoch_id)?) +} + #[cfg(test)] pub(crate) mod test { use super::*; diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index 56c80fac2d..0ab1d9ab9c 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,8 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::Item; -use nym_coconut_dkg_common::types::Epoch; +use cw_storage_plus::{Item, Map}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index 396a7d1408..7ca350120a 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::reset_dkg_state; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; @@ -61,7 +61,10 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use crate::helpers::{create_batch_redemption_proposal, create_blacklist_proposal, ProposalId}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, Coin, Deps, SubMsg}; +use cw3::ProposalResponse; +use nym_ecash_contract_common::EcashContractError; +use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg; +use sylvia::types::ExecCtx; + +#[cw_serde] +pub(crate) struct Invariants { + pub(crate) ticket_book_value: Coin, + pub(crate) ticket_value: Coin, +} + +impl NymEcashContract<'_> { + fn must_get_multisig_addr(&self, deps: Deps) -> Result { + // SAFETY: multisig admin MUST always be set on initialisation, + // if the call fails, we're in some weird UB land + #[allow(clippy::expect_used)] + Ok(self + .multisig + .get(deps)? + .expect("multisig admin must always be set on initialisation")) + } + + pub(crate) fn create_redemption_proposal( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_batch_redemption_proposal( + commitment_bs58, + ctx.info.sender.into_string(), + number_of_tickets, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + pub(crate) fn create_blacklist_proposal( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_blacklist_proposal( + public_key, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + pub(crate) fn query_multisig_proposal( + &self, + deps: Deps, + proposal_id: ProposalId, + ) -> Result { + let msg = MultisigQueryMsg::Proposal { proposal_id }; + let multisig_addr = self.must_get_multisig_addr(deps)?; + + let proposal_response: ProposalResponse = deps.querier.query( + &cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart { + contract_addr: multisig_addr.to_string(), + msg: to_binary(&msg)?, + }), + )?; + Ok(proposal_response) + } +} diff --git a/contracts/ecash/src/contract/mod.rs b/contracts/ecash/src/contract/mod.rs new file mode 100644 index 0000000000..66c5a60873 --- /dev/null +++ b/contracts/ecash/src/contract/mod.rs @@ -0,0 +1,411 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::helpers::Invariants; +use crate::deposit::DepositStorage; +use crate::helpers::{ + BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT, + CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT, +}; +use cosmwasm_std::{BankMsg, Coin, Decimal, Event, Order, Reply, Response, StdResult, Uint128}; +use cw4::Cw4Contract; +use cw_controllers::Admin; +use cw_storage_plus::{Bound, Item, Map}; +use nym_contracts_common::set_build_information; +use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, Blacklisting, PagedBlacklistedAccountResponse, +}; +use nym_ecash_contract_common::deposit::{DepositData, DepositResponse, PagedDepositsResponse}; +use nym_ecash_contract_common::events::{ + BLACKLIST_PROPOSAL_REPLY_ID, DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, + PROPOSAL_ID_ATTRIBUTE_NAME, REDEMPTION_PROPOSAL_REPLY_ID, TICKET_BOOK_VALUE, TICKET_VALUE, +}; +use nym_ecash_contract_common::EcashContractError; +use sylvia::types::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx, ReplyCtx}; +use sylvia::{contract, entry_points}; + +mod helpers; + +#[cfg(test)] +mod test; + +pub struct NymEcashContract<'a> { + pub(crate) multisig: Admin<'a>, + pub(crate) config: Item<'a, Config>, + pub(crate) expected_invariants: Item<'a, Invariants>, + + pub(crate) blacklist: Map<'a, BlacklistKey, Blacklisting>, + + pub(crate) deposits: DepositStorage<'a>, +} + +#[entry_points] +#[contract] +#[error(EcashContractError)] +impl NymEcashContract<'_> { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + multisig: Admin::new("multisig"), + config: Item::new("config"), + expected_invariants: Item::new("expected_invariants"), + blacklist: Map::new("blacklist"), + deposits: DepositStorage::new(), + } + } + + #[msg(instantiate)] + pub fn instantiate( + &self, + mut ctx: InstantiateCtx, + holding_account: String, + multisig_addr: String, + group_addr: String, + mix_denom: String, + ) -> Result { + let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?; + let holding_account = ctx.deps.api.addr_validate(&holding_account)?; + let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| { + EcashContractError::InvalidGroup { + addr: group_addr.clone(), + } + })?); + + self.multisig + .set(ctx.deps.branch(), Some(multisig_addr.clone()))?; + + self.expected_invariants.save( + ctx.deps.storage, + &Invariants { + ticket_book_value: Coin::new(TICKET_BOOK_VALUE, &mix_denom), + ticket_value: Coin::new(TICKET_VALUE, &mix_denom), + }, + )?; + + let cfg = Config { + group_addr, + mix_denom, + holding_account, + + redemption_gateway_share: Decimal::percent(5), + }; + self.config.save(ctx.deps.storage, &cfg)?; + + cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(ctx.deps.storage)?; + + Ok(Response::default()) + } + + /*================== + ======QUERIES======= + ==================*/ + #[msg(query)] + pub fn get_blacklist_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(BLACKLIST_PAGE_DEFAULT_LIMIT) + .min(BLACKLIST_PAGE_MAX_LIMIT) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let nodes = self + .blacklist + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = nodes + .last() + .map(|account: &BlacklistedAccount| account.public_key().to_string()); + + Ok(PagedBlacklistedAccountResponse::new( + nodes, + limit, + start_next_after, + )) + } + + #[msg(query)] + pub fn get_blacklisted_account( + &self, + ctx: QueryCtx, + public_key: String, + ) -> StdResult { + let account = self.blacklist.may_load(ctx.deps.storage, public_key)?; + Ok(BlacklistedAccountResponse::new(account)) + } + + #[msg(query)] + pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> Result { + let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom; + let expected_deposit = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_book_value; + let current = Coin::new(TICKET_BOOK_VALUE, mix_denom); + if expected_deposit != current { + return Err(EcashContractError::DepositAmountChanged { + at_init: expected_deposit, + current, + }); + } + + Ok(current) + } + + #[msg(query)] + pub fn get_deposit( + &self, + ctx: QueryCtx, + deposit_id: u32, + ) -> Result { + Ok(DepositResponse { + id: deposit_id, + deposit: self.deposits.try_load_by_id(ctx.deps.storage, deposit_id)?, + }) + } + + #[msg(query)] + pub fn get_deposits_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(DEPOSITS_PAGE_DEFAULT_LIMIT) + .min(DEPOSITS_PAGE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let deposits = self + .deposits + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = deposits.last().map(|deposit| deposit.id); + + Ok(PagedDepositsResponse { + deposits, + start_next_after, + }) + } + + /*===================== + ======EXECUTIONS======= + =====================*/ + + #[msg(exec)] + pub fn deposit_ticket_book_funds( + &self, + ctx: ExecCtx, + identity_key: String, + ) -> Result { + let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom; + let voucher_value = cw_utils::must_pay(&ctx.info, &mix_denom)?; + let amount = voucher_value.u128(); + + let expected_deposit = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_book_value; + if expected_deposit.amount.u128() != TICKET_BOOK_VALUE { + return Err(EcashContractError::DepositAmountChanged { + at_init: expected_deposit, + current: Coin::new(TICKET_BOOK_VALUE, mix_denom), + }); + } + + if amount != TICKET_BOOK_VALUE { + return Err(EcashContractError::WrongAmount { + received: amount, + amount: TICKET_BOOK_VALUE, + }); + } + + let deposit_id = self.deposits.save_deposit(ctx.deps.storage, identity_key)?; + + Ok(Response::new() + .add_event( + Event::new(DEPOSITED_FUNDS_EVENT_TYPE) + .add_attribute(DEPOSIT_ID, deposit_id.to_string()), + ) + .set_data(deposit_id.to_be_bytes())) + } + + #[msg(exec)] + pub fn request_redemption( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + // basic validation of commitment to make sure it's a valid sha256 digest + let Ok(digest) = bs58::decode(&commitment_bs58).into_vec() else { + return Err(EcashContractError::MalformedRedemptionCommitment); + }; + if digest.len() != 32 { + return Err(EcashContractError::MalformedRedemptionCommitment); + } + + let msg = self.create_redemption_proposal(ctx, commitment_bs58, number_of_tickets)?; + Ok(Response::new().add_submessage(msg)) + } + + #[msg(exec)] + pub fn redeem_tickets( + &self, + ctx: ExecCtx, + n: u16, + gw: String, + ) -> Result { + // only a mutlisig proposal can do that + self.multisig + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let config = self.config.load(ctx.deps.storage)?; + let denom = &config.mix_denom; + + let expected_ticket = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_value; + let current = Coin::new(TICKET_VALUE, denom); + if expected_ticket != current { + return Err(EcashContractError::TicketValueChanged { + at_init: expected_ticket, + current, + }); + } + + // TODO: we need unit tests for this + let return_amount = Uint128::new(TICKET_VALUE * n as u128); + let gw_share = config.redemption_gateway_share * return_amount; + let holding_share = return_amount - gw_share; + + Ok(Response::new() + .add_message(BankMsg::Send { + to_address: gw, + amount: vec![Coin { + denom: denom.to_owned(), + amount: gw_share, + }], + }) + .add_message(BankMsg::Send { + to_address: config.holding_account.to_string(), + amount: vec![Coin { + denom: denom.to_owned(), + amount: holding_share, + }], + })) + } + + #[msg(exec)] + pub fn propose_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + cfg.group_addr + .is_voting_member(&ctx.deps.querier, &ctx.info.sender, ctx.env.block.height)? + .ok_or(EcashContractError::Unauthorized)?; + + if let Some(blacklisted) = self + .blacklist + .may_load(ctx.deps.storage, public_key.clone())? + { + // return existing proposal id + Ok(Response::new().add_attribute( + PROPOSAL_ID_ATTRIBUTE_NAME, + blacklisted.proposal_id.to_string(), + )) + } else { + let msg = self.create_blacklist_proposal(ctx, public_key)?; + Ok(Response::new().add_submessage(msg)) + } + } + + #[msg(exec)] + pub fn add_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + //Only by multisig contract, actually add public key to blacklist + self.multisig + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let mut blacklisting = self.blacklist.load(ctx.deps.storage, public_key.clone())?; + blacklisting.finalized_at_height = Some(ctx.env.block.height); + self.blacklist + .save(ctx.deps.storage, public_key.clone(), &blacklisting)?; + + Ok(Response::new()) + } + + /*===================== + =========REPLY========= + =====================*/ + #[msg(reply)] + pub fn reply(&self, ctx: ReplyCtx, msg: Reply) -> Result { + match msg.id { + n if n == BLACKLIST_PROPOSAL_REPLY_ID => self.handle_blacklist_proposal_reply(ctx, msg), + n if n == REDEMPTION_PROPOSAL_REPLY_ID => { + self.handle_redemption_proposal_reply(ctx, msg) + } + other => Err(EcashContractError::InvalidReplyId { id: other }), + } + } + + fn handle_blacklist_proposal_reply( + &self, + ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + let proposal = self.query_multisig_proposal(ctx.deps.as_ref(), proposal_id)?; + let public_key = proposal.description; + self.blacklist.save( + ctx.deps.storage, + public_key, + &Blacklisting::new(proposal_id), + )?; + + // TODO: that `BLACKLIST_PROPOSAL_ID` might be redundant since it should be available from cw3 event + Ok(Response::new().add_attribute(PROPOSAL_ID_ATTRIBUTE_NAME, proposal_id.to_string())) + } + + fn handle_redemption_proposal_reply( + &self, + _ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + // emit the proposal_id in the response data for easy client access and to make sure it can't be tampered with + // (since it's included as part of block hash) + + Ok(Response::new().set_data(proposal_id.to_be_bytes())) + } + + /*===================== + =======MIGRATION======= + =====================*/ + #[msg(migrate)] + pub fn migrate(&self, ctx: MigrateCtx) -> Result { + set_build_information!(ctx.deps.storage)?; + cw2::ensure_from_older_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) + } +} diff --git a/contracts/ecash/src/contract/test.rs b/contracts/ecash/src/contract/test.rs new file mode 100644 index 0000000000..6ea55e8f2e --- /dev/null +++ b/contracts/ecash/src/contract/test.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{Addr, Empty, Env, MemoryStorage, OwnedDeps}; +use sylvia::types::{InstantiateCtx, QueryCtx}; + +pub const TEST_DENOM: &str = "unym"; + +#[allow(dead_code)] +pub struct TestSetup { + pub contract: NymEcashContract<'static>, + pub deps: OwnedDeps>, + pub env: Env, + + pub holding_account: Addr, + pub multisig_contract: Addr, + pub group_contract: Addr, +} + +impl TestSetup { + pub fn init() -> TestSetup { + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = mock_info("admin", &[]); + let init_ctx = InstantiateCtx::from((deps.as_mut(), env.clone(), admin)); + + let multisig_contract = "multisig"; + let group_contract = "group"; + let holding = "holding"; + + let contract = NymEcashContract::new(); + contract + .instantiate( + init_ctx, + holding.to_string(), + multisig_contract.to_string(), + group_contract.to_string(), + TEST_DENOM.to_string(), + ) + .unwrap(); + + TestSetup { + contract, + deps, + env, + holding_account: Addr::unchecked(holding), + multisig_contract: Addr::unchecked(multisig_contract), + group_contract: Addr::unchecked(group_contract), + } + } + + pub fn query_ctx(&self) -> QueryCtx { + QueryCtx::from((self.deps.as_ref(), self.env.clone())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_ecash_contract_common::deposit::Deposit; + use sylvia::anyhow; + + #[test] + fn deposit_queries() -> anyhow::Result<()> { + let mut test = TestSetup::init(); + + // no deposit + let res = test.contract.get_deposit(test.query_ctx(), 42)?; + assert!(res.deposit.is_none()); + + let deps = test.deps.as_mut(); + let deposit_id = test.contract.deposits.save_deposit( + deps.storage, + "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + )?; + + // deposit exists + let res = test.contract.get_deposit(test.query_ctx(), deposit_id)?; + let expected = Deposit { + bs58_encoded_ed25519_pubkey: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + }; + + assert_eq!(expected, res.deposit.unwrap()); + + Ok(()) + } +} diff --git a/contracts/ecash/src/deposit.rs b/contracts/ecash/src/deposit.rs new file mode 100644 index 0000000000..310b4a0a26 --- /dev/null +++ b/contracts/ecash/src/deposit.rs @@ -0,0 +1,230 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Order, StdResult, Storage}; +use cw_storage_plus::{Bound, Item, Key, Path, Prefix, PrimaryKey}; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::{deposit::Deposit, EcashContractError}; + +pub(crate) struct DepositStorage<'a> { + pub(crate) deposit_id_counter: Item<'a, DepositId>, + pub(crate) deposits: StoredDeposits, +} + +impl<'a> DepositStorage<'a> { + pub const fn new() -> Self { + DepositStorage { + deposit_id_counter: Item::new("deposit_ids"), + deposits: StoredDeposits, + } + } + + fn next_id(&self, store: &mut dyn Storage) -> Result { + let id: DepositId = self.deposit_id_counter.may_load(store)?.unwrap_or_default(); + let next_id = id + 1; + self.deposit_id_counter.save(store, &next_id)?; + Ok(id) + } + + pub fn save_deposit( + &self, + storage: &mut dyn Storage, + bs58_encoded_ed25519: String, + ) -> Result { + let id = self.next_id(storage)?; + + // ed25519 key MUST be represented with valid bs58 representation + // and after decoding it's always exactly 32 bytes which takes less + // space than its string representation (~44 bytes) + let bytes = Deposit::new(bs58_encoded_ed25519).to_bytes()?; + + let storage_key = StoredDeposits::storage_key(id); + storage.set(&storage_key, &bytes); + Ok(id) + } + + pub fn try_load_by_id( + &self, + storage: &dyn Storage, + id: DepositId, + ) -> Result, EcashContractError> { + let storage_key = StoredDeposits::storage_key(id); + + let Some(deposit_bytes) = storage.get(&storage_key) else { + return Ok(None); + }; + + Ok(Some(Deposit::try_from_bytes(&deposit_bytes)?)) + } + pub fn range( + &'a self, + store: &'a dyn Storage, + min: Option>, + max: Option>, + order: Order, + ) -> impl Iterator> + 'a { + self.deposits.no_prefix().range(store, min, max, order) + } +} + +// a helper structure for storing deposits to bypass json serialisation and use more efficient and compact representation +pub(crate) struct StoredDeposits; + +impl StoredDeposits { + const NAMESPACE: &'static [u8] = b"deposit"; + + fn deserialize_deposit_record(kv: cosmwasm_std::Record) -> StdResult<(DepositId, Deposit)> { + let (k, deposit_bytes) = kv; + let id = ::from_vec(k)?; + + Ok((id, Deposit::try_from_bytes(&deposit_bytes)?)) + } + + fn no_prefix(&self) -> Prefix { + cw_storage_plus::Prefix::with_deserialization_functions( + Self::NAMESPACE, + &[], + &[], + // explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data + |_, _, kv| Self::deserialize_deposit_record(kv), + |_, _, _| panic!("attempted to call custom de_fn_v"), + ) + } + + fn storage_key(deposit_id: u32) -> Path> { + let key = deposit_id; + Path::new( + Self::NAMESPACE, + &key.key().iter().map(Key::as_ref).collect::>(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_rng; + use cosmwasm_std::testing::mock_dependencies; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn iterating_over_deposits() { + let mut deps = mock_dependencies(); + let mut rng = test_rng(); + + let storage = DepositStorage::new(); + + let count = 10; + let mut expected = Vec::new(); + for _ in 0..count { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + + let bs58_encoded_ed25519 = ed25519_keypair.public_key().to_base58_string(); + expected.push(Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519.clone(), + }); + + storage + .save_deposit(deps.as_mut().storage, bs58_encoded_ed25519) + .unwrap(); + } + + // just first entry + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::inclusive(1u32)), + Order::Ascending, + ) + .collect::>(); + let (id, deposit) = res[0].as_ref().unwrap(); + assert_eq!(0, *id); + assert_eq!(deposit, &expected[0]); + + // first three entries + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::exclusive(4u32)), + Order::Ascending, + ) + .collect::>(); + for i in 0..3 { + let (id, deposit) = res[i].as_ref().unwrap(); + assert_eq!(i as u32, *id); + assert_eq!(deposit, &expected[i]); + } + + // two entries in the middle + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(5u32)), + Some(Bound::inclusive(6u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(5, *id1); + assert_eq!(deposit1, &expected[5]); + assert_eq!(6, *id2); + assert_eq!(deposit2, &expected[6]); + + // last 2 entries + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(9u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // last 2 entries but with iterator going beyond + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(42u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // outside the saved range + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(42u32)), + Some(Bound::inclusive(666u32)), + Order::Ascending, + ) + .collect::>(); + assert!(res.is_empty()); + + // all entries + let res = storage + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>(); + assert_eq!(res.len(), count as usize); + for (i, val) in res.into_iter().enumerate() { + let (id, deposit) = val.unwrap(); + assert_eq!(id, i as u32); + assert_eq!(&deposit, &expected[i]) + } + } +} diff --git a/contracts/ecash/src/helpers.rs b/contracts/ecash/src/helpers.rs new file mode 100644 index 0000000000..8a1b298dd5 --- /dev/null +++ b/contracts/ecash/src/helpers.rs @@ -0,0 +1,121 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, Decimal, Reply, StdError, StdResult, SubMsg, SubMsgResult, WasmMsg, +}; +use cw4::Cw4Contract; +use nym_contracts_common::events::try_find_attribute; +use nym_ecash_contract_common::events::{ + PROPOSAL_ID_ATTRIBUTE_NAME, REDEMPTION_PROPOSAL_REPLY_ID, WASM_EVENT_NAME, +}; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_contract_common::{ + events::BLACKLIST_PROPOSAL_REPLY_ID, msg::ExecuteMsg, EcashContractError, +}; +use nym_multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use serde::{Deserialize, Serialize}; + +// version info for migration info +pub(crate) const CONTRACT_NAME: &str = "crate:nym-ecash"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub group_addr: Cw4Contract, + pub mix_denom: String, + pub holding_account: Addr, + + pub redemption_gateway_share: Decimal, +} + +//type aliases for easier reasoning +pub(crate) type BlacklistKey = String; +pub(crate) type ProposalId = u64; + +// paged retrieval limits for all blacklist queries and transactions +pub(crate) const BLACKLIST_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const BLACKLIST_PAGE_DEFAULT_LIMIT: u32 = 50; + +// paged retrieval limits for all deposit queries and transactions +pub(crate) const DEPOSITS_PAGE_MAX_LIMIT: u32 = 100; +pub(crate) const DEPOSITS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) fn create_batch_redemption_proposal( + tickets_digest: String, + gw: String, + n: u16, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let release_funds_req = ExecuteMsg::RedeemTickets { n, gw }; + let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&release_funds_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: BATCH_REDEMPTION_PROPOSAL_TITLE.to_string(), + description: tickets_digest, + msgs: vec![release_funds_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, REDEMPTION_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) fn create_blacklist_proposal( + public_key: String, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let blacklist_req = ExecuteMsg::AddToBlacklist { + public_key: public_key.clone(), + }; + let blacklist_req_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&blacklist_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Add to blacklist, as ordered by Ecash Bandwidth Contract"), + description: public_key, + msgs: vec![blacklist_req_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, BLACKLIST_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) trait MultisigReply { + fn multisig_proposal_id(&self) -> Result; +} + +impl MultisigReply for Reply { + fn multisig_proposal_id(&self) -> Result { + match &self.result { + SubMsgResult::Ok(res) => { + let proposal_id: u64 = + try_find_attribute(&res.events, WASM_EVENT_NAME, PROPOSAL_ID_ATTRIBUTE_NAME) + .ok_or(EcashContractError::MissingProposalId)? + .map_err(|_| EcashContractError::MalformedProposalId)?; + Ok(proposal_id) + } + SubMsgResult::Err(reply_err) => Err(StdError::generic_err(reply_err).into()), + } + } +} diff --git a/contracts/ecash/src/lib.rs b/contracts/ecash/src/lib.rs new file mode 100644 index 0000000000..e71f86f8cc --- /dev/null +++ b/contracts/ecash/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod contract; +mod deposit; +mod helpers; +#[cfg(test)] +pub mod multitest; +mod support; diff --git a/contracts/ecash/src/multitest.rs b/contracts/ecash/src/multitest.rs new file mode 100644 index 0000000000..5baf8032db --- /dev/null +++ b/contracts/ecash/src/multitest.rs @@ -0,0 +1,73 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Addr, Coin}; +use cw_utils::PaymentError; +use nym_ecash_contract_common::EcashContractError; +use sylvia::{cw_multi_test::App as MtApp, multitest::App}; + +use crate::contract::multitest_utils::CodeId; + +#[test] +fn invalid_deposit() { + let owner = "owner"; + let denom = "unym"; + + let mtapp = MtApp::new(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(owner), + vec![ + Coin::new(10000000, denom), + Coin::new(10000000, "some_denom"), + ], + ) + .unwrap() + }); + let app = App::new(mtapp); + + let code_id = CodeId::store_code(&app); + + let contract = code_id + .instantiate( + "holding_acount".to_string(), + "multisig_addr".to_string(), + "group_addr".to_string(), + denom.to_string(), + ) + .call(owner) + .unwrap(); + + let verification_key = "Verification key"; + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::NoFunds {}) + ); + + let coin = Coin::new(1000000, denom.to_string()); + let second_coin = Coin::new(1000000, "some_denom"); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[coin, second_coin.clone()]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MultipleDenoms {}) + ); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[second_coin]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MissingDenom(denom.to_string())) + ); +} diff --git a/contracts/ecash/src/support/mod.rs b/contracts/ecash/src/support/mod.rs new file mode 100644 index 0000000000..f217570509 --- /dev/null +++ b/contracts/ecash/src/support/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod tests; diff --git a/contracts/ecash/src/support/tests.rs b/contracts/ecash/src/support/tests.rs new file mode 100644 index 0000000000..2f44544092 --- /dev/null +++ b/contracts/ecash/src/support/tests.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + ChaCha20Rng::from_seed(dummy_seed) +} diff --git a/deny.toml b/deny.toml index 812b1e6e1d..317455647c 100644 --- a/deny.toml +++ b/deny.toml @@ -74,9 +74,9 @@ notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - "RUSTSEC-2020-0159", - "RUSTSEC-2020-0071", - "RUSTSEC-2021-0145", + "RUSTSEC-2020-0159", + "RUSTSEC-2020-0071", + "RUSTSEC-2021-0145", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories @@ -115,6 +115,7 @@ allow = [ "Unicode-DFS-2016", "OpenSSL", "Zlib", + "Unicode-3.0", ] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses @@ -217,7 +218,7 @@ deny = [ # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, - { name = "openssl"}, + { name = "openssl" }, ] # List of features to allow/deny diff --git a/envs/local.env b/envs/local.env index 11338f12b4..09dd5ef227 100644 --- a/envs/local.env +++ b/envs/local.env @@ -13,7 +13,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1xsml6tm6pnx8nsuz3a54cznh55g6nuwu9ug9t92ucjkq5ewp5w9syxeqsv VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx +ECASH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m COCONUT_DKG_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p diff --git a/envs/qa.env b/envs/qa.env index f889c60b81..781d4ef19f 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -11,7 +11,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw +ECASH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw GROUP_CONTRACT_ADDRESS=n1qp35fcj0v9u3trhaps5v9q0lc42t4m6aty2wryss75ee8zuqnsqqdcreyq MULTISIG_CONTRACT_ADDRESS=n1qa4hswlcjmttulj0q9qa46jf64f93pecl6tydcsjldfe0hy5ju0sdmwzya COCONUT_DKG_CONTRACT_ADDRESS=n1ayrk6wp6w5lf6njtnfjwljmtcc9vevv5sxwkz7uq24rp2pw67t0qhmmxdd diff --git a/envs/sandbox.env b/envs/sandbox.env index 90dc8d7e7c..6deaafdfaa 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -13,7 +13,7 @@ DENOMS_EXPONENT=6 REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70 +ECASH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70 GROUP_CONTRACT_ADDRESS=n10v3rjnq4cjyccfykyams68ztce337gksuu6f0lvtl4meuwvkewaqru4uav MULTISIG_CONTRACT_ADDRESS=n1cemnu8as0ls45v3caunpesl8jlsfw2ff9rlwnltlecp7zrxct4dsqc2y42 COCONUT_DKG_CONTRACT_ADDRESS=n1zx96qgd88vqlzcxkpwzks7kqs5ctrx36xtzfc58p7q6c4ng9anlqzc4nh8 diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cc2c6c37d1..330f0a9824 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -11,28 +11,38 @@ authors = [ ] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" -rust-version = "1.70" +rust-version = "1.76" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" + +[[bin]] +name = "nym-gateway" +required-features = ["bin-deps"] +path = "src/main.rs" + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } bs58 = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive"], optional = true } colored = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } dashmap = { workspace = true } dirs = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } humantime-serde = { workspace = true } ipnetwork = { workspace = true } -log = { workspace = true } once_cell = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +si-scale = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", @@ -52,19 +62,23 @@ tokio = { workspace = true, features = [ tokio-stream = { workspace = true, features = ["fs"] } tokio-tungstenite = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } time = { workspace = true } zeroize = { workspace = true } +bloomfilter = { workspace = true } # internal nym-authenticator = { path = "../service-providers/authenticator" } nym-api-requests = { path = "../nym-api/nym-api-requests" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../common/bin-common" } nym-config = { path = "../common/config" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-crypto = { path = "../common/crypto" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } nym-gateway-requests = { path = "gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } @@ -95,6 +109,7 @@ sqlx = { workspace = true, features = [ [features] wireguard = ["nym-wireguard", "defguard_wireguard_rs"] +bin-deps = ["clap", 'nym-bin-common/output_format'] [package.metadata.deb] name = "nym-gateway" diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index 9802a6c590..365edc2a5c 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -14,14 +14,14 @@ license.workspace = true bs58 = { workspace = true } futures = { workspace = true } generic-array = { workspace = true, features = ["serde"] } -log = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } -nym-crypto = { path = "../../common/crypto" } +nym-crypto = { path = "../../common/crypto" } nym-pemstore = { path = "../../common/pemstore" } nym-sphinx = { path = "../../common/nymsphinx" } @@ -40,4 +40,7 @@ features = ["tokio"] workspace = true default-features = false +[dev-dependencies] +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } # we need specific imports in tests + diff --git a/gateway/gateway-requests/src/models.rs b/gateway/gateway-requests/src/models.rs index 339715bbf7..d4e651c3c2 100644 --- a/gateway/gateway-requests/src/models.rs +++ b/gateway/gateway-requests/src/models.rs @@ -1,329 +1,132 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::GatewayRequestsError; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, VerifyCredentialRequest}; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{Base58, Bytable, CompactEcashError}; use serde::{Deserialize, Serialize}; -// reimplements old coconut-interface::Credential for backwards compatibility sake -// (so that 'new' gateways could still understand those requests) -#[derive(Debug, PartialEq, Eq)] -pub struct OldV1Credential { - pub n_params: u32, - - pub theta: VerifyCredentialRequest, - - pub voucher_value: u64, - - pub voucher_info: String, - - pub epoch_id: u64, -} - -// attempt to convert the old request type into the new variant -impl TryFrom for CredentialSpendingRequest { - type Error = GatewayRequestsError; - - fn try_from(value: OldV1Credential) -> Result { - if value.n_params <= 2 { - return Err(GatewayRequestsError::InvalidNumberOfEmbededParameters( - value.n_params, - )); - } - let embedded_private_attributes = value.n_params as usize - 2; - let typ = value.voucher_info.parse()?; - let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info]; - - Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: value.theta, - public_attributes_plain, - typ, - epoch_id: value.epoch_id, - }, - }) - } -} - -impl OldV1Credential { - pub fn as_bytes(&self) -> Vec { - let n_params_bytes = self.n_params.to_be_bytes(); - let theta_bytes = self.theta.to_bytes(); - let theta_bytes_len = theta_bytes.len(); - let voucher_value_bytes = self.voucher_value.to_be_bytes(); - let epoch_id_bytes = self.epoch_id.to_be_bytes(); - let voucher_info_bytes = self.voucher_info.as_bytes(); - let voucher_info_len = voucher_info_bytes.len(); - - let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len); - bytes.extend_from_slice(&n_params_bytes); - bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes()); - bytes.extend_from_slice(&theta_bytes); - bytes.extend_from_slice(&voucher_value_bytes); - bytes.extend_from_slice(&epoch_id_bytes); - bytes.extend_from_slice(voucher_info_bytes); - - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < 28 { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let mut four_byte = [0u8; 4]; - let mut eight_byte = [0u8; 8]; - - four_byte.copy_from_slice(&bytes[..4]); - let n_params = u32::from_be_bytes(four_byte); - eight_byte.copy_from_slice(&bytes[4..12]); - let theta_len = u64::from_be_bytes(eight_byte); - if bytes.len() < 28 + theta_len as usize { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let theta = VerifyCredentialRequest::from_bytes(&bytes[12..12 + theta_len as usize]) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]); - let voucher_value = u64::from_be_bytes(eight_byte); - eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]); - let epoch_id = u64::from_be_bytes(eight_byte); - let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec()) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - - Ok(OldV1Credential { - n_params, - theta, - voucher_value, - voucher_info, - epoch_id, - }) - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CredentialSpendingRequest { /// The cryptographic material required for spending the underlying credential. pub data: CredentialSpendingData, } -// just a helper macro for checking required length and advancing the buffer -macro_rules! ensure_len_and_advance { - ($b:expr, $n:expr) => {{ - if $b.len() < $n { - return Err(GatewayRequestsError::CredentialDeserializationFailureEOF); - } - // create binding to the desired range - let bytes = &$b[..$n]; - - // update the initial binding - - $b = &$b[$n..]; - - bytes - }}; -} - impl CredentialSpendingRequest { pub fn new(data: CredentialSpendingData) -> Self { CredentialSpendingRequest { data } } - pub fn matches_blinded_serial_number( - &self, - blinded_serial_number_bs58: &str, - ) -> Result { - self.data - .verify_credential_request - .has_blinded_serial_number(blinded_serial_number_bs58) - } - - pub fn unchecked_voucher_value(&self) -> u64 { - self.data - .get_bandwidth_attribute() - .expect("failed to extract bandwidth attribute") - .parse() - .expect("failed to parse voucher value") - } + // pub fn matches_serial_number( + // &self, + // serial_number_bs58: &str, + // ) -> Result { + // self.data.payment.has_serial_number(serial_number_bs58) + // } pub fn to_bytes(&self) -> Vec { - // simple length prefixed serialization - // TODO: change it to a standard format instead - let mut bytes = Vec::new(); - - let embedded_private = (self.data.embedded_private_attributes as u32).to_be_bytes(); - let theta = self.data.verify_credential_request.to_bytes(); - let theta_len = (theta.len() as u32).to_be_bytes(); - - let public = (self.data.public_attributes_plain.len() as u32).to_be_bytes(); - let typ = self.data.typ.to_string(); - let typ_bytes = typ.as_bytes(); - let typ_len = (typ_bytes.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&embedded_private); - bytes.extend_from_slice(&theta_len); - bytes.extend_from_slice(&theta); - bytes.extend_from_slice(&public); - - for pub_element in &self.data.public_attributes_plain { - let bytes_el = pub_element.as_bytes(); - let len = (bytes_el.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&len); - bytes.extend_from_slice(bytes_el); - } - - bytes.extend_from_slice(&typ_len); - bytes.extend_from_slice(typ_bytes); - bytes.extend_from_slice(&self.data.epoch_id.to_be_bytes()); - - bytes + self.data.to_bytes() } - pub fn try_from_bytes(raw: &[u8]) -> Result { - // initial binding - let mut b = raw; - let embedded_private_bytes = ensure_len_and_advance!(b, 4); - let embedded_private_attributes = - u32::from_be_bytes(embedded_private_bytes.try_into().unwrap()) as usize; - - let theta_len_bytes = ensure_len_and_advance!(b, 4); - let theta_len = u32::from_be_bytes(theta_len_bytes.try_into().unwrap()) as usize; - - let theta_bytes = ensure_len_and_advance!(b, theta_len); - let theta = VerifyCredentialRequest::from_bytes(theta_bytes) - .map_err(GatewayRequestsError::CredentialDeserializationFailureMalformedTheta)?; - - let public_bytes = ensure_len_and_advance!(b, 4); - let public = u32::from_be_bytes(public_bytes.try_into().unwrap()) as usize; - - let mut public_attributes_plain = Vec::with_capacity(public); - for _ in 0..public { - let element_len_bytes = ensure_len_and_advance!(b, 4); - let element_len = u32::from_be_bytes(element_len_bytes.try_into().unwrap()) as usize; - - let element_bytes = ensure_len_and_advance!(b, element_len); - let element = String::from_utf8(element_bytes.to_vec())?; - public_attributes_plain.push(element); - } - - let typ_len_bytes = ensure_len_and_advance!(b, 4); - let typ_len = u32::from_be_bytes(typ_len_bytes.try_into().unwrap()) as usize; - - let typ_bytes = ensure_len_and_advance!(b, typ_len); - let raw_typ = String::from_utf8(typ_bytes.to_vec())?; - let typ = raw_typ.parse()?; - - // tell the linter to chill out in for this last iteration - #[allow(unused_assignments)] - let epoch_id_bytes = ensure_len_and_advance!(b, 8); - let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap()); - + pub fn try_from_bytes(raw: &[u8]) -> Result { Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: theta, - public_attributes_plain, - typ, - epoch_id, - }, + data: CredentialSpendingData::try_from_bytes(raw)?, }) } + + pub fn encoded_serial_number(&self) -> Vec { + self.data.encoded_serial_number() + } + + pub fn serial_number_bs58(&self) -> String { + self.data.serial_number_b58() + } } +impl Bytable for CredentialSpendingRequest { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingRequest {} + #[cfg(test)] mod tests { use super::*; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_credentials::IssuanceBandwidthCredential; - use nym_credentials_interface::{ - blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters, - Signature, VerificationKey, + use nym_compact_ecash::{ + issue, + tests::helpers::{generate_coin_indices_signatures, generate_expiration_date_signatures}, + ttp_keygen, PayInfo, }; - - #[test] - fn old_v1_coconut_credential_roundtrip() { - let voucher_value = 1000000u64; - let voucher_info = String::from("BandwidthVoucher"); - let serial_number = - Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap(); - let binding_number = - Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap(); - let signature = Signature::try_from_bs58( - "ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\ - 7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\ - ZR9j", - ) - .unwrap(); - let params = Parameters::new(4).unwrap(); - let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\ - woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\ - FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\ - JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\ - UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\ - wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\ - 1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\ - nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\ - ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\ - 6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\ - Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y") - .unwrap(); - let theta = prove_bandwidth_credential( - ¶ms, - &verification_key, - &signature, - &serial_number, - &binding_number, - ) - .unwrap(); - - let credential = OldV1Credential { - n_params: 4, - theta, - voucher_value, - voucher_info, - epoch_id: 42, - }; - - let serialized_credential = credential.as_bytes(); - let deserialized_credential = OldV1Credential::from_bytes(&serialized_credential).unwrap(); - - assert_eq!(credential, deserialized_credential); - } + use nym_credentials::ecash::utils::EcashTime; + use nym_credentials::IssuanceTicketBook; + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; #[test] fn credential_roundtrip() { // make valid request - let params = bandwidth_credential_params(); - let keypair = nym_credentials_interface::keygen(params); + let keypair = ttp_keygen(1, 1).unwrap().remove(0); - let issuance = IssuanceBandwidthCredential::new_freepass(None); + let mut rng = OsRng; + let signing_key = ed25519::PrivateKey::new(&mut rng); + + let issuance = IssuanceTicketBook::new(42, [], signing_key); + let expiration_date = issuance.expiration_date(); let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - keypair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, + let exp_date_sigs = generate_expiration_date_signatures( + sig_req.expiration_date.ecash_unix_timestamp(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let blind_sig = issue( + keypair.secret_key(), + sig_req.ecash_pub_key.clone(), + &sig_req.withdrawal_request, + expiration_date.ecash_unix_timestamp(), ) .unwrap(); - let sig = blind_sig.unblind( - keypair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - let issued = issuance.into_issued_credential(sig, 42); + let partial_wallet = issuance + .unblind_signature( + &keypair.verification_key(), + &sig_req, + blind_sig, + keypair.index.unwrap(), + ) + .unwrap(); + + let wallet = issuance + .aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req) + .unwrap(); + + let mut issued = issuance.into_issued_ticketbook(wallet, 1); + let coin_indices_signatures = generate_coin_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; let spending = issued - .prepare_for_spending(keypair.verification_key()) + .prepare_for_spending( + &keypair.verification_key(), + pay_info, + &coin_indices_signatures, + &exp_date_sigs, + 1, + ) .unwrap(); let with_epoch = CredentialSpendingRequest { data: spending }; diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 6212a919b5..328b5d59cb 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -6,7 +6,6 @@ use crate::registration::handshake::shared_key::{SharedKeySize, SharedKeys}; use crate::registration::handshake::WsItem; use crate::types; use futures::{Sink, SinkExt, Stream, StreamExt}; -use log::*; use nym_crypto::{ asymmetric::{encryption, identity}, generic_array::typenum::Unsigned, @@ -15,6 +14,7 @@ use nym_crypto::{ }; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; use rand::{CryptoRng, RngCore}; +use tracing::log::*; use std::str::FromStr; use std::time::Duration; diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 6c564d4ae8..6e48d0441e 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::authentication::encrypted_address::EncryptedAddressBytes; -use crate::iv::IV; -use crate::models::{CredentialSpendingRequest, OldV1Credential}; +use crate::iv::{IVConversionError, IV}; +use crate::models::CredentialSpendingRequest; use crate::registration::handshake::SharedKeys; use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; -use log::error; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, UnknownCredentialType}; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::CompactEcashError; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag; use nym_crypto::symmetric::stream_cipher; @@ -18,6 +17,7 @@ use nym_sphinx::params::packet_sizes::PacketSize; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; +use tracing::log::error; use std::str::FromStr; use std::string::FromUtf8Error; @@ -92,6 +92,9 @@ pub enum GatewayRequestsError { #[error("provided MAC is invalid")] InvalidMac, + #[error("Provided bandwidth IV is malformed: {0}")] + MalformedIV(#[from] IVConversionError), + #[error("address field was incorrectly encoded: {source}")] IncorrectlyEncodedAddress { #[from] @@ -122,18 +125,15 @@ pub enum GatewayRequestsError { source: MixPacketFormattingError, }, + #[error("failed to deserialize provided credential: {0}")] + EcashCredentialDeserializationFailure(#[from] CompactEcashError), + #[error("failed to deserialize provided credential: EOF")] CredentialDeserializationFailureEOF, #[error("failed to deserialize provided credential: malformed string: {0}")] CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), - #[error("failed to deserialize provided credential: {0}")] - CredentialDeserializationFailureUnknownType(#[from] UnknownCredentialType), - - #[error("failed to deserialize provided credential: malformed verify request: {0}")] - CredentialDeserializationFailureMalformedTheta(CoconutError), - #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), } @@ -164,6 +164,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + EcashCredential { + enc_credential: Vec, + iv: Vec, + }, ClaimFreeTestnetBandwidth, } @@ -200,37 +204,14 @@ impl ClientControlRequest { ClientControlRequest::BandwidthCredentialV2 { .. } => { "BandwidthCredentialV2".to_string() } + ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } } } - pub fn new_enc_coconut_bandwidth_credential_v1( - credential: &OldV1Credential, - shared_key: &SharedKeys, - iv: IV, - ) -> Self { - let serialized_credential = credential.as_bytes(); - let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - - ClientControlRequest::BandwidthCredential { - enc_credential, - iv: iv.to_bytes(), - } - } - - pub fn try_from_enc_coconut_bandwidth_credential_v1( - enc_credential: Vec, - shared_key: &SharedKeys, - iv: IV, - ) -> Result { - let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - OldV1Credential::from_bytes(&credential_bytes) - .map_err(|_| GatewayRequestsError::MalformedEncryption) - } - - pub fn new_enc_coconut_bandwidth_credential_v2( + pub fn new_enc_ecash_credential( credential: CredentialSpendingData, shared_key: &SharedKeys, iv: IV, @@ -239,19 +220,20 @@ impl ClientControlRequest { let serialized_credential = cred.to_bytes(); let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - ClientControlRequest::BandwidthCredentialV2 { + ClientControlRequest::EcashCredential { enc_credential, iv: iv.to_bytes(), } } - pub fn try_from_enc_coconut_bandwidth_credential_v2( + pub fn try_from_enc_ecash_credential( enc_credential: Vec, shared_key: &SharedKeys, - iv: IV, + iv: Vec, ) -> Result { + let iv = IV::try_from_bytes(&iv)?; let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - CredentialSpendingRequest::try_from_bytes(&credential_bytes) + CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } } diff --git a/gateway/migrations/20240624120000_ecash_changes.sql b/gateway/migrations/20240624120000_ecash_changes.sql new file mode 100644 index 0000000000..502de5c568 --- /dev/null +++ b/gateway/migrations/20240624120000_ecash_changes.sql @@ -0,0 +1,93 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE available_bandwidth +RENAME COLUMN freepass_expiration TO expiration; + +DROP TABLE spent_credential; + +-- we need the id field to prevent data duplication +CREATE TABLE shared_keys_tmp ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_address_bs58 TEXT NOT NULL UNIQUE, + derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT NOT NULL +); + +INSERT INTO shared_keys_tmp (client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) +SELECT * FROM shared_keys; + +-- ideally this table would be called "clients" but I don't want to cause too many breaking changes +DROP TABLE shared_keys; +ALTER TABLE shared_keys_tmp RENAME TO shared_keys; + +CREATE TABLE available_bandwidth_tmp ( + client_id INTEGER NOT NULL PRIMARY KEY REFERENCES shared_keys(id), + available INTEGER NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +INSERT INTO available_bandwidth_tmp (client_id, available, expiration) +SELECT t1.id as client_id, t2.available, t2.expiration + FROM shared_keys as t1 + JOIN available_bandwidth as t2 + ON t1.client_address_bs58 = t2.client_address_bs58; + +DROP TABLE available_bandwidth; +ALTER TABLE available_bandwidth_tmp RENAME TO available_bandwidth; + +CREATE TABLE ecash_signer ( + epoch_id INTEGER NOT NULL, + +-- unique id assigned by the DKG contract. it does not change between epochs + signer_id INTEGER NOT NULL +); + +CREATE TABLE received_ticket ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL REFERENCES shared_keys(id), + received_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + rejected BOOLEAN +); + +CREATE INDEX received_ticket_index ON received_ticket (received_at); + +-- received tickets that are in the process of verifying +CREATE TABLE ticket_data ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + + -- serial_number, alongside the entire row, will get purged after redemption is complete + serial_number BLOB NOT NULL UNIQUE, + + -- data will get purged after 80% of signers verifies it + data BLOB +); + + +-- result of a verification from a single signer (API) +CREATE TABLE ticket_verification ( + ticket_id INTEGER NOT NULL REFERENCES received_ticket(id), + signer_id INTEGER NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + accepted BOOLEAN NOT NULL, + + PRIMARY KEY (ticket_id, signer_id) +); + +CREATE INDEX ticket_verification_index ON ticket_verification (ticket_id); + +-- verified tickets that are yet to be redeemed +CREATE TABLE verified_tickets ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + proposal_id INTEGER REFERENCES redemption_proposals(proposal_id) +); + +CREATE INDEX verified_tickets_index ON verified_tickets (proposal_id); + +CREATE TABLE redemption_proposals ( + proposal_id INTEGER NOT NULL PRIMARY KEY, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + resolved_at TIMESTAMP WITHOUT TIME ZONE, -- either got executed or got rejected + rejected BOOLEAN +); \ No newline at end of file diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index c8551cc445..9711ab1583 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::commands::upgrade_helpers; use log::{error, info}; use nym_config::{save_formatted_config_to_file, OptionalSet}; use nym_crypto::asymmetric::identity; @@ -17,6 +18,7 @@ use nym_gateway::helpers::{ use nym_network_defaults::mainnet; use nym_network_defaults::var_names::NYXD; use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API}; +use tracing::{error, info}; use nym_network_requester::{ generate_new_client_keys, set_active_gateway, setup_fs_gateways_storage, setup_gateway, diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index d2100efab6..1a08e249a0 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -5,10 +5,10 @@ use crate::Cli; use anyhow::bail; use clap::CommandFactory; use clap::Subcommand; -use log::{error, warn}; use nym_bin_common::completions::{fig_generate, ArgShell}; use std::io::IsTerminal; use std::time::Duration; +use tracing::{error, warn}; pub(crate) mod build_info; pub(crate) mod helpers; diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index ceb30fcb4c..e9b4fb8d1b 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -4,13 +4,13 @@ use crate::commands::helpers::{try_load_current_config, try_override_config, OverrideConfig}; use anyhow::bail; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_config::helpers::SPECIAL_ADDRESSES; use nym_gateway::helpers::{OverrideIpPacketRouterConfig, OverrideNetworkRequesterConfig}; use nym_gateway::GatewayError; use std::net::IpAddr; use std::path::PathBuf; +use tracing::warn; #[derive(Args, Clone)] pub struct Run { diff --git a/gateway/src/commands/setup_network_requester.rs b/gateway/src/commands/setup_network_requester.rs index c489983424..3611392a56 100644 --- a/gateway/src/commands/setup_network_requester.rs +++ b/gateway/src/commands/setup_network_requester.rs @@ -3,12 +3,12 @@ use crate::commands::helpers::{initialise_local_network_requester, try_load_current_config}; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_gateway::helpers::{load_public_key, OverrideNetworkRequesterConfig}; use std::io::IsTerminal; use std::path::PathBuf; use std::time::Duration; +use tracing::warn; #[derive(Args, Clone)] pub struct CmdArgs { diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 91d1e3b92e..0750b3bf0d 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::template::CONFIG_TEMPLATE; -use log::{debug, warn}; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use nym_config::helpers::inaddr_any; @@ -17,6 +16,7 @@ use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::{debug, warn}; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -67,7 +67,7 @@ pub fn default_data_directory>(id: P) -> PathBuf { .join(DEFAULT_DATA_DIR) } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { // additional metadata holding on-disk location of this config file @@ -220,7 +220,6 @@ impl Config { self.gateway.only_coconut_credentials = only_coconut_credentials; self } - pub fn with_custom_nym_apis(mut self, nym_api_urls: Vec) -> Self { self.gateway.nym_api_urls = nym_api_urls; self @@ -415,7 +414,7 @@ impl Default for IpPacketRouter { } } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct Debug { /// Initial value of an exponential backoff to reconnect to dropped TCP connection when @@ -459,6 +458,9 @@ pub struct Debug { // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. // It shall be disabled in the subsequent releases. pub use_legacy_framed_packet_version: bool, + + #[serde(default)] + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Default for Debug { @@ -475,6 +477,51 @@ impl Default for Debug { client_bandwidth_max_delta_flushing_amount: DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, use_legacy_framed_packet_version: false, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + pub const DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION: Duration = Duration::from_secs(86400 * 25); +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION, } } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index edf34b3a95..b666a1e4b5 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -14,6 +14,7 @@ use std::path::PathBuf; use thiserror::Error; pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; #[derive(Debug, Error)] pub enum GatewayError { @@ -169,6 +170,12 @@ pub enum GatewayError { source: RequestHandlingError, }, + #[error("ecash related failure: {source}")] + EcashFailure { + #[from] + source: EcashTicketError, + }, + #[error("failed to catch an interrupt: {source}")] ShutdownFailure { source: Box, @@ -189,6 +196,9 @@ pub enum GatewayError { source: ipnetwork::IpNetworkError, }, + #[error("the current multisig contract is not using 'AbsolutePercentage' threshold!")] + InvalidMultisigThreshold, + #[cfg(all(feature = "wireguard", target_os = "linux"))] #[error("failed to remove wireguard interface: {0}")] WireguardInterfaceError(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), diff --git a/gateway/src/helpers.rs b/gateway/src/helpers.rs index 7a00000a2f..7b17da7170 100644 --- a/gateway/src/helpers.rs +++ b/gateway/src/helpers.rs @@ -13,6 +13,7 @@ use nym_types::gateway::{ GatewayIpPacketRouterDetails, GatewayNetworkRequesterDetails, GatewayNodeDetailsResponse, }; use std::path::Path; +use tracing::info; pub use crate::node::helpers::{load_ip_packet_router_config, load_network_requester_config}; @@ -72,7 +73,7 @@ pub fn override_ip_packet_router_config( // disable poisson rate in the BASE client if the IPR option is enabled if cfg.ip_packet_router.disable_poisson_rate { - log::info!("Disabling poisson rate for ip packet router"); + info!("Disabling poisson rate for ip packet router"); cfg.set_no_poisson_process(); } diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index 23bc8d8693..1a7c8f29b5 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -4,7 +4,6 @@ use crate::config::Config; use crate::error::GatewayError; use crate::helpers::load_public_key; -use log::{debug, error, warn}; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; use nym_network_requester::RequestFilter; @@ -15,6 +14,7 @@ use nym_node_http_api::NymNodeHttpError; use nym_sphinx::addressing::clients::Recipient; use nym_task::TaskClient; use std::sync::Arc; +use tracing::{debug, error, warn}; fn load_gateway_details( config: &Config, diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 3c5109817b..6759180420 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -6,12 +6,12 @@ use clap::{crate_name, crate_version, Parser}; use colored::Colorize; -use log::error; use nym_bin_common::bin_info; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; use std::io::IsTerminal; use std::sync::OnceLock; +use tracing::error; mod commands; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index df88d16213..962765067e 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -4,9 +4,9 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender}; use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; -use log::warn; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use tracing::warn; enum ActiveClient { /// Handle to a remote client connected via a network socket. diff --git a/gateway/src/node/client_handling/bandwidth.rs b/gateway/src/node/client_handling/bandwidth.rs index d4ce709c96..3f15451576 100644 --- a/gateway/src/node/client_handling/bandwidth.rs +++ b/gateway/src/node/client_handling/bandwidth.rs @@ -1,21 +1,16 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use log::{error, warn}; -use nym_credentials::coconut::bandwidth::CredentialType; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; -use time::OffsetDateTime; +use tracing::error; #[derive(Debug, Error)] pub enum BandwidthError { #[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)] UnsupportedBandwidthValue(u64), - #[error("the provided free pass has already expired (expiry was on {expiry_date})")] - ExpiredFreePass { expiry_date: OffsetDateTime }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -46,51 +41,10 @@ impl Bandwidth { Bandwidth { value } } - pub fn new(bandwidth_value: u64) -> Result { - if bandwidth_value > i64::MAX as u64 { - // note that this would have represented more than 1 exabyte, - // which is like 125,000 worth of hard drives, so I don't think we have - // to worry about it for now... - warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now"); - return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value)); + pub fn ticket_amount() -> Self { + Bandwidth { + value: nym_network_defaults::TICKET_BANDWIDTH_VALUE, } - - Ok(Bandwidth { - value: bandwidth_value, - }) - } - - pub(crate) fn parse_raw_bandwidth( - value: &str, - typ: CredentialType, - ) -> Result<(u64, Option), BandwidthError> { - let (bandwidth_value, freepass_expiration) = - match typ { - CredentialType::Voucher => { - let token_value: u64 = value - .parse() - .map_err(|source| BandwidthError::VoucherValueParsingFailure { source })?; - (token_value * nym_network_defaults::BYTES_PER_UTOKEN, None) - } - CredentialType::FreePass => { - let expiry_timestamp: i64 = value - .parse() - .map_err(|source| BandwidthError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp) - .map_err(|source| BandwidthError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date < now { - return Err(BandwidthError::ExpiredFreePass { expiry_date }); - } - (nym_network_defaults::BYTES_PER_FREEPASS, Some(expiry_date)) - } - }; - Ok((bandwidth_value, freepass_expiration)) } pub fn value(&self) -> u64 { diff --git a/gateway/src/node/client_handling/embedded_clients/mod.rs b/gateway/src/node/client_handling/embedded_clients/mod.rs index a7264f0985..7974bb9c41 100644 --- a/gateway/src/node/client_handling/embedded_clients/mod.rs +++ b/gateway/src/node/client_handling/embedded_clients/mod.rs @@ -5,11 +5,11 @@ use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; use futures::StreamExt; -use log::{debug, error}; use nym_network_requester::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; +use tracing::{debug, error, trace}; #[derive(Debug)] pub(crate) struct LocalEmbeddedClientHandle { @@ -71,12 +71,12 @@ impl MessageRouter { messages = self.mix_receiver.next() => match messages { Some(messages) => self.handle_received_messages(messages), None => { - log::trace!("embedded_clients::MessageRouter: Stopping since channel closed"); + trace!("embedded_clients::MessageRouter: Stopping since channel closed"); break; } }, _ = shutdown.recv_with_delay() => { - log::trace!("embedded_clients::MessageRouter: Received shutdown"); + trace!("embedded_clients::MessageRouter: Received shutdown"); debug_assert!(shutdown.is_shutdown()); break } diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index e112997e57..3a2a206368 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -1,15 +1,16 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::client_handling::websocket::connection_handler::BandwidthFlushingBehaviourConfig; use nym_crypto::asymmetric::identity; use std::sync::Arc; // I can see this being possible expanded with say storage or client store #[derive(Clone)] -pub(crate) struct CommonHandlerState { - pub(crate) coconut_verifier: Arc, +pub(crate) struct CommonHandlerState { + pub(crate) ecash_verifier: Arc>, + pub(crate) storage: S, pub(crate) local_identity: Arc, pub(crate) only_coconut_credentials: bool, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 249f60997f..fa31cc6960 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -1,11 +1,12 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::bandwidth::BandwidthError; +use crate::node::client_handling::bandwidth::{Bandwidth, BandwidthError}; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::client_handling::websocket::connection_handler::ClientBandwidth; use crate::node::{ client_handling::{ - bandwidth::Bandwidth, websocket::{ connection_handler::{ClientDetails, FreshHandler}, message_receiver::{ @@ -20,24 +21,24 @@ use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; -use log::*; -use nym_credentials::coconut::bandwidth::{bandwidth_credential_params, CredentialType}; -use nym_credentials_interface::{Base58, CoconutError}; +use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials_interface::CredentialSpendingData; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_requests::{ - iv::{IVConversionError, IV}, types::{BinaryRequest, ServerResponse}, ClientControlRequest, GatewayRequestsError, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use rand::{CryptoRng, Rng}; +use si_scale::helpers::bibytes2; use std::{process, time::Duration}; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; #[derive(Debug, Error)] pub enum RequestHandlingError { @@ -49,9 +50,6 @@ pub enum RequestHandlingError { )] MissingClientBandwidthEntry { client_address: String }, - #[error("Provided bandwidth IV is malformed - {0}")] - MalformedIV(#[from] IVConversionError), - #[error("Provided binary request was malformed - {0}")] InvalidBinaryRequest(#[from] GatewayRequestsError), @@ -61,8 +59,13 @@ pub enum RequestHandlingError { #[error("The received request is not valid in the current context: {additional_context}")] IllegalRequest { additional_context: String }, - #[error("Provided bandwidth credential did not verify correctly on {0}")] - InvalidBandwidthCredential(String), + #[error("credential has been rejected by the validators")] + RejectedProposal, + + #[error( + "the provided credential has an invalid spending date. got {got} but expected {expected}" + )] + InvalidCredentialSpendingDate { got: Date, expected: Date }, #[error("the provided bandwidth credential has already been spent before at this gateway")] BandwidthCredentialAlreadySpent, @@ -76,41 +79,29 @@ pub enum RequestHandlingError { #[error("Validator API error - {0}")] APIError(#[from] nym_validator_client::ValidatorClientError), - #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] - NotEnoughNymAPIs { received: usize, needed: usize }, - #[error("There was a problem with the proposal id: {reason}")] ProposalIdError { reason: String }, - #[error("coconut failure: {0}")] - CoconutError(#[from] CoconutError), + #[error("compact ecash error: {0}")] + CompactEcashError(#[from] nym_credentials_interface::CompactEcashError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("Credential error - {0}")] CredentialError(#[from] nym_credentials::error::Error), + #[error("Internal error")] + InternalError, + #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("the provided credential did not contain a valid type attribute")] - InvalidTypeAttribute, - #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] OutOfBandwidth { required: i64, available: i64 }, - #[error("the provided credential did not have a bandwidth attribute")] - MissingBandwidthAttribute, - - #[error("attempted to claim a bandwidth voucher for an account using a free pass (it expires on {expiration})")] - BandwidthVoucherForFreePassAccount { expiration: OffsetDateTime }, - - #[error("attempted to claim another free pass for the account while another free pass is still active (it expires on {expiration})")] - PreexistingFreePass { expiration: OffsetDateTime }, - - #[error("the DKG contract is unavailable")] - UnavailableDkgContract, + #[error(transparent)] + EcashFailure(EcashTicketError), } impl RequestHandlingError { @@ -119,6 +110,17 @@ impl RequestHandlingError { } } +impl From for RequestHandlingError { + fn from(err: EcashTicketError) -> Self { + // don't expose storage issue details to the user + if let EcashTicketError::InternalStorageFailure { source } = err { + RequestHandlingError::StorageError(source) + } else { + RequestHandlingError::EcashFailure(err) + } + } +} + /// Helper trait that allows converting result of handling client request into a websocket message // Note: I couldn't have implemented a normal "From" trait as both `Message` and `Result` are foreign types trait IntoWSMessage { @@ -159,7 +161,7 @@ impl AuthenticatedHandler where // TODO: those trait bounds here don't really make sense.... R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or @@ -181,8 +183,9 @@ where // so in theory we could just unwrap the value here, but since we're returning a Result anyway, // we might as well return a failure response instead let bandwidth = fresh + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client.id) .await? .ok_or(RequestHandlingError::MissingClientBandwidthEntry { client_address: client.address.as_base58_string(), @@ -205,10 +208,11 @@ where .disconnect(self.client.address) } - async fn expire_freepass(&mut self) -> Result<(), RequestHandlingError> { + async fn expire_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + self.inner.expire_bandwidth(self.client.id).await?; self.client_bandwidth.bandwidth = Default::default(); - self.client_bandwidth.update_flush_data(); - Ok(self.inner.expire_freepass(self.client.address).await?) + self.client_bandwidth.update_sync_data(); + Ok(()) } /// Increases the amount of available bandwidth of the connected client by the specified value. @@ -216,28 +220,20 @@ where /// # Arguments /// /// * `amount`: amount to increase the available bandwidth by. + /// * `expiration` : the expiration date of that bandwidth async fn increase_bandwidth( &mut self, bandwidth: Bandwidth, + expiration: OffsetDateTime, ) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64; + self.client_bandwidth.bytes_delta_since_sync += bandwidth.value() as i64; + self.client_bandwidth.bandwidth.expiration = expiration; // any increases to bandwidth should get flushed immediately // (we don't want to accidentally miss somebody claiming a gigabyte voucher) - self.flush_bandwidth().await - } - - async fn set_freepass_expiration( - &mut self, - expiration: OffsetDateTime, - ) -> Result<(), RequestHandlingError> { - self.client_bandwidth.bandwidth.freepass_expiration = Some(expiration); - self.inner - .storage - .set_freepass_expiration(self.client.address, expiration) - .await?; - self.client_bandwidth.update_flush_data(); - Ok(()) + self.sync_expiration().await?; + self.sync_bandwidth().await } /// Decreases the amount of available bandwidth of the connected client by the specified value. @@ -247,14 +243,15 @@ where /// * `amount`: amount to decrease the available bandwidth by. async fn consume_bandwidth(&mut self, amount: i64) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes -= amount; + self.client_bandwidth.bytes_delta_since_sync -= amount; // since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets // through, that's completely fine if self .client_bandwidth - .should_flush(self.inner.shared_state.bandwidth_cfg) + .should_sync(self.inner.shared_state.bandwidth_cfg) { - self.flush_bandwidth().await?; + self.sync_bandwidth().await?; } Ok(()) @@ -272,148 +269,107 @@ where } } - async fn handle_bandwidth_request( - &mut self, - credential: CredentialSpendingRequest, - ) -> Result { - // check if the credential hasn't been spent before - let serial_number = credential.data.blinded_serial_number(); - trace!("processing credential {}", serial_number.to_bs58()); + async fn check_local_db_for_double_spending( + &self, + serial_number: &[u8], + ) -> Result<(), RequestHandlingError> { + trace!("checking local db for double spending..."); - // if we already have had received a free pass (that's not expired, don't accept any additional bandwidth) - if self.client_bandwidth.bandwidth.freepass_expired() { - // the free pass we used before has expired -> reset our state and handle the request as normal - self.expire_freepass().await?; - } else if let Some(expiration) = self.client_bandwidth.bandwidth.freepass_expiration { - // the free pass is still valid -> return error - return match credential.data.typ { - CredentialType::Voucher => { - Err(RequestHandlingError::BandwidthVoucherForFreePassAccount { expiration }) - } - CredentialType::FreePass => { - Err(RequestHandlingError::PreexistingFreePass { expiration }) - } - }; - } - - let already_spent = self + let spent = self .inner + .shared_state .storage - .contains_credential(&serial_number) + .contains_ticket(serial_number) .await?; - if already_spent { - trace!("the credential has already been spent before"); + if spent { + trace!("the credential has already been spent before at this gateway"); return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); } - - trace!( - "attempting to obtain aggregate verification key for epoch {}", - credential.data.epoch_id - ); - - if !credential.data.validate_type_attribute() { - trace!("mismatch in the type attribute"); - return Err(RequestHandlingError::InvalidTypeAttribute); - } - - let Some(bandwidth_attribute) = credential.data.get_bandwidth_attribute() else { - trace!("missing bandwidth attribute"); - return Err(RequestHandlingError::MissingBandwidthAttribute); - }; - - // this will extract token amounts out of bandwidth vouchers and validate expiry of free passes - let (raw_bandwidth, freepass_expiration) = - Bandwidth::parse_raw_bandwidth(bandwidth_attribute, credential.data.typ)?; - - let bandwidth = Bandwidth::new(raw_bandwidth)?; - - trace!("embedded bandwidth: {bandwidth:?}"); - - // locally verify the credential - { - let aggregated_verification_key = self - .inner - .shared_state - .coconut_verifier - .verification_key(credential.data.epoch_id) - .await?; - - let params = bandwidth_credential_params(); - if !credential.data.verify(params, &aggregated_verification_key) { - trace!("the credential did not verify correctly"); - return Err(RequestHandlingError::InvalidBandwidthCredential( - String::from("local credential verification has failed"), - )); - } - } - - match credential.data.typ { - CredentialType::Voucher => { - trace!("the credential is a bandwidth voucher. attempting to release the funds"); - let api_clients = self - .inner - .shared_state - .coconut_verifier - .api_clients(credential.data.epoch_id) - .await?; - - self.inner - .shared_state - .coconut_verifier - .release_bandwidth_voucher_funds(&api_clients, credential) - .await?; - } - CredentialType::FreePass => { - // no need to do anything special here, we already extracted the bandwidth amount and checked expiry - info!("received a free pass credential"); - } - } - - // technically this is not atomic, i.e. checking for the spending and then marking as spent, - // but because we have the `UNIQUE` constraint on the database table - // if somebody attempts to spend the same credential in another, parallel request, - // one of them will fail - // - // mark the credential as spent - // TODO: technically this should be done under a storage transaction so that if we experience any - // failures later on, it'd get reverted - trace!("storing serial number information"); - self.inner - .storage - .insert_spent_credential( - serial_number, - freepass_expiration.is_some(), - self.client.address, - ) - .await?; - - trace!("increasing client bandwidth"); - self.increase_bandwidth(bandwidth).await?; - // set free pass expiration - if let Some(expiration) = freepass_expiration { - self.set_freepass_expiration(expiration).await?; - } - - let available_total = self.client_bandwidth.bandwidth.bytes; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(()) } - async fn handle_bandwidth_v1( - &mut self, - enc_credential: Vec, - iv: Vec, - ) -> Result { - debug!("handling v1 bandwidth request"); + async fn check_bloomfilter(&self, serial_number: &Vec) -> Result<(), RequestHandlingError> { + trace!("checking the bloomfilter..."); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v1( - enc_credential, - &self.client.shared_keys, - iv, - )?; + let spent = self + .inner + .shared_state + .ecash_verifier + .check_double_spend(serial_number) + .await; - self.handle_bandwidth_request(credential.try_into()?).await + if spent { + trace!("the credential has already been spent before at some gateway before (bloomfilter failure)"); + return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); + } + Ok(()) + } + + fn check_credential_spending_date( + &self, + proposed: Date, + today: Date, + ) -> Result<(), RequestHandlingError> { + trace!("checking ticket spending date..."); + + if today != proposed { + trace!("invalid credential spending date. received {proposed}"); + return Err(RequestHandlingError::InvalidCredentialSpendingDate { + got: proposed, + expected: today, + }); + } + Ok(()) + } + + async fn cryptographically_verify_ticket( + &self, + credential: &CredentialSpendingRequest, + ) -> Result<(), RequestHandlingError> { + trace!("attempting to perform ticket verification..."); + + let aggregated_verification_key = self + .inner + .shared_state + .ecash_verifier + .verification_key(credential.data.epoch_id) + .await?; + + self.inner + .shared_state + .ecash_verifier + .check_payment(&credential.data, &aggregated_verification_key) + .await?; + Ok(()) + } + + fn async_verify_ticket(&self, ticket: CredentialSpendingData, ticket_id: i64) { + let client_ticket = ClientTicket::new(ticket, ticket_id); + + self.inner + .shared_state + .ecash_verifier + .async_verify(client_ticket); + } + + async fn store_received_ticket( + &self, + ticket_data: &CredentialSpendingRequest, + received_at: OffsetDateTime, + ) -> Result { + trace!("storing received ticket"); + let ticket_id = self + .inner + .shared_state + .storage + .insert_received_ticket( + self.client.id, + received_at, + ticket_data.encoded_serial_number(), + ticket_data.to_bytes(), + ) + .await?; + Ok(ticket_id) } /// Tries to handle the received bandwidth request by checking correctness of the received data @@ -421,23 +377,51 @@ where /// /// # Arguments /// - /// * `enc_credential`: raw encrypted bandwidth credential to verify. + /// * `enc_credential`: raw encrypted credential to verify. /// * `iv`: fresh iv used for the credential. - async fn handle_bandwidth_v2( + async fn handle_ecash_bandwidth( &mut self, enc_credential: Vec, iv: Vec, ) -> Result { - debug!("handling v2 bandwidth request"); + let received_at = OffsetDateTime::now_utc(); + // TODO: change it into a span field instead once we move to tracing + debug!( + "handling e-cash bandwidth request from {}", + self.client.address + ); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v2( + let credential = ClientControlRequest::try_from_enc_ecash_credential( enc_credential, &self.client.shared_keys, iv, )?; + let spend_date = ecash_today(); - self.handle_bandwidth_request(credential).await + // check if the credential hasn't been spent before + let serial_number = credential.data.encoded_serial_number(); + + self.check_credential_spending_date(credential.data.spend_date, spend_date.ecash_date())?; + self.check_bloomfilter(&serial_number).await?; + self.check_local_db_for_double_spending(&serial_number) + .await?; + + // TODO: do we HAVE TO do it? + self.cryptographically_verify_ticket(&credential).await?; + + let ticket_id = self.store_received_ticket(&credential, received_at).await?; + self.async_verify_ticket(credential.data, ticket_id); + + // TODO: double storing? + // self.store_spent_credential(serial_number_bs58).await?; + + let bandwidth = Bandwidth::ticket_amount(); + + self.increase_bandwidth(bandwidth, spend_date).await?; + + let available_total = self.client_bandwidth.bandwidth.bytes; + + Ok(ServerResponse::Bandwidth { available_total }) } async fn handle_claim_testnet_bandwidth( @@ -449,29 +433,47 @@ where return Err(RequestHandlingError::OnlyCoconutCredentials); } - self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE) + self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.bandwidth.bytes; Ok(ServerResponse::Bandwidth { available_total }) } - async fn flush_bandwidth(&mut self) -> Result<(), RequestHandlingError> { - trace!("flushing client bandwidth to the underlying storage"); + async fn sync_expiration(&mut self) -> Result<(), RequestHandlingError> { self.inner + .shared_state .storage - .set_bandwidth(self.client.address, self.client_bandwidth.bandwidth.bytes) + .set_expiration(self.client.id, self.client_bandwidth.bandwidth.expiration) .await?; - self.client_bandwidth.update_flush_data(); Ok(()) } + #[instrument(level = "trace", skip_all)] + async fn sync_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + trace!("syncing client bandwidth with the underlying storage"); + let updated = self + .inner + .shared_state + .storage + .increase_bandwidth(self.client.id, self.client_bandwidth.bytes_delta_since_sync) + .await?; + + trace!(updated); + + self.client_bandwidth.bandwidth.bytes = updated; + + self.client_bandwidth.update_sync_data(); + Ok(()) + } + + #[instrument(skip_all)] async fn try_use_bandwidth( &mut self, required_bandwidth: i64, ) -> Result { - if self.client_bandwidth.bandwidth.freepass_expired() { - self.expire_freepass().await?; + if self.client_bandwidth.bandwidth.expired() { + self.expire_bandwidth().await?; } let available_bandwidth = self.client_bandwidth.bandwidth.bytes; @@ -482,8 +484,12 @@ where }); } + let available_bi2 = bibytes2(available_bandwidth as f64); + let required_bi2 = bibytes2(required_bandwidth as f64); + debug!(available = available_bi2, required = required_bi2); + self.consume_bandwidth(required_bandwidth).await?; - Ok(self.client_bandwidth.bandwidth.bytes) + Ok(available_bandwidth) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -494,6 +500,7 @@ where /// # Arguments /// /// * `mix_packet`: packet received from the client that should get forwarded into the network. + #[instrument(skip_all)] async fn handle_forward_sphinx( &mut self, mix_packet: MixPacket, @@ -543,14 +550,22 @@ where match ClientControlRequest::try_from(raw_request) { Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { - ClientControlRequest::BandwidthCredential { enc_credential, iv } => self - .handle_bandwidth_v1(enc_credential, iv) - .await - .into_ws_message(), - ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv } => self - .handle_bandwidth_v2(enc_credential, iv) + ClientControlRequest::EcashCredential { enc_credential, iv } => self + .handle_ecash_bandwidth(enc_credential, iv) .await .into_ws_message(), + ClientControlRequest::BandwidthCredential { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } + ClientControlRequest::BandwidthCredentialV2 { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } ClientControlRequest::ClaimFreeTestnetBandwidth => self .handle_claim_testnet_bandwidth() .await diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs deleted file mode 100644 index 26a7e76640..0000000000 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::authenticated::RequestHandlingError; -use log::*; -use nym_credentials_interface::VerificationKey; -use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::nyxd::contract_traits::{MultisigQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::AccountId; -use nym_validator_client::{ - nyxd::{ - contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient, MultisigSigningClient}, - cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, - Coin, - }, - CoconutApiClient, DirectSigningHttpRpcNyxdClient, -}; -use std::collections::HashMap; -use std::ops::Deref; -use tokio::sync::{RwLock, RwLockReadGuard}; - -pub(crate) struct CoconutVerifier { - address: AccountId, - nyxd_client: RwLock, - - // **CURRENTLY** api client addresses don't change during the epochs - api_clients: RwLock>>, - - // keys never change during epochs - master_keys: RwLock>, - mix_denom_base: String, -} - -impl CoconutVerifier { - pub async fn new( - nyxd_client: DirectSigningHttpRpcNyxdClient, - only_coconut_credentials: bool, - ) -> Result { - let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); - let address = nyxd_client.address(); - - let mut master_keys = HashMap::new(); - let mut api_clients = HashMap::new(); - - // don't make it a hard failure in case we're running on mainnet (where DKG hasn't been deployed yet) - if nyxd_client.dkg_contract_address().is_none() { - if !only_coconut_credentials { - warn!( - "the DKG contract address is not available - \ - no coconut credentials will be redeemable \ - (if the DKG ceremony hasn't been run yet this warning is expected)" - ); - } else { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - } - - let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { - // another case of somebody putting a placeholder address that doesn't exist - error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); - if only_coconut_credentials { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - }; - - // might as well obtain the key for the current epoch, if applicable - if current_epoch.state.is_in_progress() { - // note: even though we're constructing clients here, we will NOT be making any network requests - let epoch_api_clients = - all_coconut_api_clients(&nyxd_client, current_epoch.epoch_id).await?; - let threshold = nyxd_client.get_current_epoch_threshold().await?; - - // SAFETY: - // if epoch state is in the 'in progress' state, it means the threshold value MUST HAVE - // been established. if it wasn't, there's an underlying issue with the DKG contract in which - // case we shouldn't continue anyway because here be dragons - #[allow(clippy::expect_used)] - let threshold = threshold.expect("unavailable threshold value") as usize; - if epoch_api_clients.len() < threshold { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: epoch_api_clients.len(), - needed: threshold, - }); - } - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients)?; - - api_clients.insert(current_epoch.epoch_id, epoch_api_clients); - master_keys.insert(current_epoch.epoch_id, aggregated_verification_key); - } - - Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: RwLock::new(api_clients), - master_keys: RwLock::new(master_keys), - mix_denom_base, - }) - } - - pub async fn api_clients( - &self, - epoch_id: EpochId, - ) -> Result>, RequestHandlingError> { - let guard = self.api_clients.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |clients| clients.get(&epoch_id)) { - trace!("we already had cached api clients for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.query_api_clients(epoch_id).await?; - trace!( - "obtained {} api clients for epoch {epoch_id} from the contract", - api_clients.len() - ); - - // EDGE CASE: - // if this epoch is from the past, we can't query for its threshold - // we can only hope that enough valid keys were submitted - // the best we can do is check if we have at least a api - if api_clients.is_empty() { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: 0, - needed: 1, - }); - } - - let mut guard = self.api_clients.write().await; - guard.insert(epoch_id, api_clients); - let guard = guard.downgrade(); - trace!("stored api clients for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |clients| { - clients.get(&epoch_id).unwrap() - })) - } - - pub async fn verification_key( - &self, - epoch_id: EpochId, - ) -> Result, RequestHandlingError> { - let guard = self.master_keys.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |keys| keys.get(&epoch_id)) { - trace!("we already had cached verification key for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.api_clients(epoch_id).await?; - trace!( - "attempting to obtain verification key from {} api clients", - api_clients.len() - ); - - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&api_clients)?; - - let mut guard = self.master_keys.write().await; - guard.insert(epoch_id, aggregated_verification_key); - let guard = guard.downgrade(); - trace!("stored aggregated verification key for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |keys| { - keys.get(&epoch_id).unwrap() - })) - } - - pub async fn query_api_clients( - &self, - epoch_id: u64, - ) -> Result, RequestHandlingError> { - Ok(all_coconut_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) - } - - pub async fn release_bandwidth_voucher_funds( - &self, - api_clients: &[CoconutApiClient], - credential: CredentialSpendingRequest, - ) -> Result<(), RequestHandlingError> { - if !credential.data.typ.is_voucher() { - unimplemented!() - } - - // safety: the voucher funds are released after the credential has already been verified locally - // and the underlying bandwidth value has been extracted, so the below MUST succeed - let voucher_amount = credential.unchecked_voucher_value() as u128; - - let blinded_serial_number = credential - .data - .verify_credential_request - .blinded_serial_number_bs58(); - - let res = self - .nyxd_client - .write() - .await - .spend_credential( - Coin::new(voucher_amount, &self.mix_denom_base), - blinded_serial_number, - self.address.to_string(), - None, - ) - .await?; - let proposal_id = find_attribute(&res.logs, "wasm", BANDWIDTH_PROPOSAL_ID) - .ok_or(RequestHandlingError::ProposalIdError { - reason: String::from("proposal id not found"), - })? - .value - .parse::() - .map_err(|_| RequestHandlingError::ProposalIdError { - reason: String::from("proposal id could not be parsed to u64"), - })?; - - let proposal = self - .nyxd_client - .read() - .await - .query_proposal(proposal_id) - .await?; - if !credential.matches_blinded_serial_number(&proposal.description)? { - return Err(RequestHandlingError::ProposalIdError { - reason: String::from("proposal has different serial number"), - }); - } - - let req = nym_api_requests::coconut::VerifyCredentialBody::new( - credential.data, - proposal_id, - self.address.clone(), - ); - for client in api_clients { - let ret = client.api_client.verify_bandwidth_credential(&req).await; - let client_url = client.api_client.nym_api.current_url(); - match ret { - Ok(res) => { - if !res.verification_result { - warn!("Validator at {client_url} didn't accept the credential. It will probably vote No on the spending proposal"); - } - } - Err(err) => { - warn!("Validator at {client_url} could not be reached. There might be a problem with the coconut endpoint: {err}"); - } - } - } - - self.nyxd_client - .write() - .await - .execute_proposal(proposal_id, None) - .await?; - - Ok(()) - } -} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs new file mode 100644 index 0000000000..66eda62269 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs @@ -0,0 +1,963 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::bandwidth::Bandwidth; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::helpers::for_each_api_concurrent; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::storage::Storage; +use crate::GatewayError; +use cosmwasm_std::Fraction; +use cw_utils::ThresholdResponse; +use futures::channel::mpsc::UnboundedReceiver; +use futures::{Stream, StreamExt}; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody}; +use nym_credentials_interface::CredentialSpendingData; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cw3::Status; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use si_scale::helpers::bibytes2; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tokio::time::{interval_at, Duration, Instant}; +use tracing::{debug, error, info, instrument, trace, warn}; + +enum ProposalResult { + Executed, + Rejected, + Pending, +} + +impl ProposalResult { + fn is_pending(&self) -> bool { + matches!(self, ProposalResult::Pending) + } + + fn is_rejected(&self) -> bool { + matches!(self, ProposalResult::Rejected) + } +} + +#[derive(Clone)] +pub struct ClientTicket { + pub spending_data: CredentialSpendingData, + pub ticket_id: i64, +} + +impl ClientTicket { + pub fn new(spending_data: CredentialSpendingData, ticket_id: i64) -> Self { + ClientTicket { + spending_data, + ticket_id, + } + } +} + +struct PendingVerification { + ticket: ClientTicket, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingVerification { + fn new(ticket: ClientTicket, pending: Vec) -> Self { + PendingVerification { ticket, pending } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> VerifyEcashTicketBody { + VerifyEcashTicketBody { + // TODO: redundant clone + credential: self.ticket.spending_data.clone(), + gateway_cosmos_addr, + } + } +} + +struct PendingRedemptionVote { + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingRedemptionVote { + fn new( + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + pending: Vec, + ) -> Self { + PendingRedemptionVote { + proposal_id, + digest, + included_serial_numbers, + epoch_id, + pending, + } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody { + BatchRedeemTicketsBody::new( + self.digest.clone(), + self.proposal_id, + self.included_serial_numbers.clone(), + gateway_cosmos_addr, + ) + } +} + +pub(crate) struct CredentialHandlerConfig { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub(crate) revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + pub(crate) pending_poller: Duration, + + pub(crate) minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub(crate) minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + pub(crate) maximum_time_between_redemption: Duration, +} + +pub(crate) struct CredentialHandler { + config: CredentialHandlerConfig, + multisig_threshold: f32, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + pending_tickets: Vec, + pending_redemptions: Vec, +} + +impl CredentialHandler +where + St: Storage + Clone + 'static, +{ + async fn rebuild_pending_tickets( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unverified_tickets().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + // a lookup of ids for signers for given epoch + let mut apis_lookup = HashMap::new(); + + // 2. for each of them, reconstruct missing votes + for ticket in unverified { + let epoch = ticket.spending_data.epoch_id; + assert!(epoch <= i64::MAX as u64); + let signers = match apis_lookup.get(&epoch) { + Some(signers) => signers, + None => { + // get all signers for given epoch + let signers = shared_state.storage.get_signers(epoch as i64).await?; + apis_lookup.insert(epoch, signers); + + // safety: we just inserted that entry + #[allow(clippy::unwrap_used)] + apis_lookup.get(&epoch).unwrap() + } + }; + // get all votes the ticket received + let votes = shared_state + .storage + .get_votes(ticket.ticket_id) + .await? + .into_iter() + .collect::>(); + let mut missing_votes = Vec::new(); + for signer in signers { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(signer) { + missing_votes.push(*signer as u64) + } + } + pending.push(PendingVerification { + ticket, + pending: missing_votes, + }) + } + Ok(pending) + } + + async fn rebuild_pending_votes( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unresolved_proposals().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + let epoch_id = shared_state.current_epoch_id().await?; + let apis = shared_state + .api_clients(epoch_id) + .await? + .iter() + .map(|s| (s.cosmos_address.to_string(), s.node_id)) + .collect::>(); + + for proposal_id in unverified { + // get all of the votes + let votes = shared_state + .start_query() + .await + .get_all_votes(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)? + .into_iter() + .map(|v| v.voter) + .collect::>(); + + let mut missing_votes = Vec::new(); + + // see who hasn't voted + for (api_address, api_id) in &apis { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(api_address) { + missing_votes.push(*api_id) + } + } + + // attempt to rebuild SN and digest from the proposal info + storage data + let proposal_info = shared_state + .start_query() + .await + .query_proposal(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)?; + + let tickets = shared_state + .storage + .get_all_proposed_tickets_with_sn(proposal_id as u32) + .await?; + let digest = + BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + if encoded_digest != proposal_info.description { + error!("the lost proposal {proposal_id} does not have a matching digest!"); + continue; + } + + pending.push(PendingRedemptionVote { + proposal_id: proposal_id as u64, + digest, + included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(), + epoch_id, + pending: missing_votes, + }) + } + + Ok(pending) + } + + pub(crate) async fn new( + config: CredentialHandlerConfig, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + ) -> Result { + let multisig_threshold = shared_state + .nyxd_client + .read() + .await + .query_threshold() + .await?; + + let ThresholdResponse::AbsolutePercentage { percentage, .. } = multisig_threshold else { + return Err(GatewayError::InvalidMultisigThreshold); + }; + + // that's a nasty conversion, but it works : ) + let multisig_threshold = + percentage.numerator().u128() as f32 / percentage.denominator().u128() as f32; + + // on startup read pending credentials and api responses from the storage + let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?; + + // on startup read pending proposals from the storage + // then reconstruct the votes by querying the multisig contract for votes on those proposals + // digest from the description and count from the message + let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?; + + Ok(CredentialHandler { + config, + multisig_threshold, + ticket_receiver, + shared_state, + pending_tickets, + pending_redemptions, + }) + } + + // the argument is temporary as we'll be reading from the storage + async fn create_redemption_proposal( + &self, + commitment: &[u8], + number_of_tickets: u16, + ) -> Result { + let res = self + .shared_state + .start_tx() + .await + .request_ticket_redemption( + bs58::encode(commitment).into_string(), + number_of_tickets, + None, + ) + .await + .map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?; + + // that one is quite tricky because proposal exists on chain, but we didn't get the id... + // but it should be quite impossible to ever reach this unless we make breaking changes + let proposal_id = res + .parse_singleton_u64_contract_data() + .inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}")) + .map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?; + + info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets"); + + Ok(proposal_id) + } + + /// Attempt to send ticket verification request to the provided ecash verifier. + async fn verify_ticket( + &self, + ticket_id: i64, + request: &VerifyEcashTicketBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.verify_ecash_ticket(request).await { + Ok(res) => { + let accepted = match res.verified { + Ok(_) => { + trace!("{client} has accepted ticket {ticket_id}"); + true + } + Err(rejection) => { + warn!("{client} has rejected ticket {ticket_id}: {rejection}"); + false + } + }; + self.shared_state + .storage + .insert_ticket_verification( + ticket_id, + client.node_id as i64, + OffsetDateTime::now_utc(), + accepted, + ) + .await?; + Ok(accepted) + } + Err(err) => { + error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + #[instrument(skip(self))] + async fn revoke_ticket_bandwidth(&self, ticket_id: i64) -> Result<(), EcashTicketError> { + warn!("revoking bandwidth associated with ticket {ticket_id} since it failed verification"); + + let bytes_to_revoke = + Bandwidth::ticket_amount().value() as f32 * self.config.revocation_bandwidth_penalty; + let to_revoke_bi2 = bibytes2(bytes_to_revoke as f64); + + info!(to_revoke_bi2); + + self.shared_state + .storage + .revoke_ticket_bandwidth(ticket_id, bytes_to_revoke as i64) + .await?; + Ok(()) + } + + /// Attempt to send the pending ticket to all ecash verifiers that haven't yet returned valid response. + async fn send_pending_ticket_for_verification( + &self, + pending: &mut PendingVerification, + api_clients: Option>>, + ) -> Result { + let ticket_id = pending.ticket.ticket_id; + let api_clients = match api_clients { + Some(clients) => clients, + None => { + self.shared_state + .api_clients(pending.ticket.spending_data.epoch_id) + .await? + } + }; + + let verification_request = pending.to_request_body(self.shared_state.address.clone()); + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + api_clients + .deref() + .iter() + .filter(|client| pending.pending.contains(&client.node_id)), + ) + .for_each_concurrent(32, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .verify_ticket( + pending.ticket.ticket_id, + &verification_request, + ecash_client, + ) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket verification: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.config.minimum_api_quorum) { + error!("{rejected_perc:.2}% of signers rejected ticket {ticket_id}. we won't be able to redeem it"); + + self.shared_state + .storage + .update_rejected_ticket(pending.ticket.ticket_id) + .await?; + self.revoke_ticket_bandwidth(pending.ticket.ticket_id) + .await?; + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. we won't be able to spend it"), + n if n < self.config.minimum_api_quorum => warn!("less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. technically we could redeem it, but we'll wait for the bigger quorum"), + _ => { + trace!("{accepted_perc:.2}% of signers accepted ticket {ticket_id}"); + self.shared_state.storage.update_verified_ticket(pending.ticket.ticket_id).await?; + return Ok(true) + } + } + + Ok(false) + } + + async fn send_ticket_for_verification( + &mut self, + ticket: ClientTicket, + ) -> Result<(), EcashTicketError> { + let api_clients = self + .shared_state + .api_clients(ticket.spending_data.epoch_id) + .await?; + + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingVerification::new(ticket, ids); + + let got_quorum = self + .send_pending_ticket_for_verification(&mut pending, Some(api_clients)) + .await?; + if !got_quorum { + debug!("failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + self.pending_tickets.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + + Ok(()) + } + + async fn handle_client_ticket(&mut self, ticket: ClientTicket) { + // attempt to send for verification + let ticket_id = ticket.ticket_id; + if let Err(err) = self.send_ticket_for_verification(ticket).await { + error!("failed to verify ticket {ticket_id}: {err}") + } + } + + async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> { + let mut still_failing = Vec::new(); + + // 1. attempt to resolve all pending proposals + while let Some(mut pending) = self.pending_redemptions.pop() { + match self.try_resolve_pending_proposal(&mut pending, None).await { + Ok(resolution) => { + if resolution.is_pending() { + warn!("still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later", pending.proposal_id, pending.pending); + still_failing.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + pending.proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending proposal: {err}"); + // make sure to update internal state to not lose any data + self.pending_redemptions.push(pending); + self.pending_redemptions.append(&mut still_failing); + return Err(err); + } + } + } + + let mut still_failing = Vec::new(); + + // 2. attempt to verify the remaining tickets + while let Some(mut pending) = self.pending_tickets.pop() { + // possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients + match self + .send_pending_ticket_for_verification(&mut pending, None) + .await + { + Ok(got_quorum) => { + if !got_quorum { + warn!("still failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + still_failing.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending ticket: {err}"); + // make sure to update internal state to not lose any data + self.pending_tickets.push(pending); + self.pending_tickets.append(&mut still_failing); + return Err(err); + } + } + } + // at this point self.pending_tickets is empty + self.pending_tickets = still_failing; + Ok(()) + } + + /// Attempt to send batch redemption request to the provided ecash verifier. + async fn redeem_tickets( + &self, + proposal_id: u64, + request: &BatchRedeemTicketsBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.batch_redeem_ecash_tickets(request).await { + Ok(res) => { + let accepted = if res.proposal_accepted { + trace!("{client} has accepted proposal {proposal_id}"); + true + } else { + warn!("{client} has rejected proposal {proposal_id}"); + false + }; + + Ok(accepted) + } + Err(err) => { + error!("failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> { + self.shared_state + .start_tx() + .await + .execute_proposal(proposal_id, None) + .await + .map_err( + |source| EcashTicketError::RedemptionProposalExecutionFailure { + proposal_id, + source, + }, + )?; + Ok(()) + } + + async fn get_proposal_status(&self, proposal_id: u64) -> Result { + Ok(self + .shared_state + .start_query() + .await + .query_proposal(proposal_id) + .await + .map_err(EcashTicketError::chain_query_failure)? + .status) + } + + async fn try_finalize_proposal( + &self, + proposal_id: u64, + ) -> Result { + match self.get_proposal_status(proposal_id).await? { + Status::Pending => { + // the voting hasn't even begun! + error!("impossible case! the proposal {proposal_id} is still pending"); + Ok(ProposalResult::Pending) + } + Status::Open => { + debug!("proposal {proposal_id} is still open and needs more votes"); + Ok(ProposalResult::Pending) + } + Status::Rejected => { + warn!("proposal {proposal_id} has been rejected"); + Ok(ProposalResult::Rejected) + } + Status::Passed => { + info!( + "proposal {proposal_id} has already been passed - we just need to execute it" + ); + self.try_execute_proposal(proposal_id).await?; + info!("executed proposal {proposal_id}"); + Ok(ProposalResult::Executed) + } + Status::Executed => { + info!("proposal {proposal_id} has already been executed - nothing to do!"); + Ok(ProposalResult::Executed) + } + } + } + + async fn try_resolve_pending_proposal( + &self, + pending: &mut PendingRedemptionVote, + api_clients: Option>>, + ) -> Result { + let proposal_id = pending.proposal_id; + + info!( + "attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets", + pending.included_serial_numbers.len() + ); + + // check if the proposal still needs more votes from the apis + let result = self.try_finalize_proposal(proposal_id).await?; + if !result.is_pending() { + return Ok(result); + } + + let api_clients = match api_clients { + Some(clients) => clients, + None => self.shared_state.api_clients(pending.epoch_id).await?, + }; + + let redemption_request = pending.to_request_body(self.shared_state.address.clone()); + + // TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .redeem_tickets(pending.proposal_id, &redemption_request, ecash_client) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket redemption: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.multisig_threshold) { + error!("{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"); + // no need to query the chain as with so many rejections it's impossible it has passed. + return Ok(ProposalResult::Rejected); + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => { + error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"); + return Ok(ProposalResult::Pending); + } + n if n < self.config.minimum_api_quorum => { + warn!("the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"); + } + _ => { + trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}"); + } + } + + // attempt to execute the proposal if it reached the required threshold + self.try_finalize_proposal(proposal_id).await + } + + async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> { + if !self.pending_tickets.is_empty() { + return Err(EcashTicketError::PendingTickets); + } + + let latest_stored = self.shared_state.storage.latest_proposal().await?; + + // check if we have already created the proposal but crashed before persisting it in the db + // + // if we have some persisted proposals in storage, try to see if there's anything more recent on chain + // (i.e. the missing proposal) + // if not (i.e. this would have been our first) check the latest page of proposals. + // while this is not ideal, realistically speaking we probably crashed few minutes ago + // and worst case scenario we'll just recreate the proposal instead + // + // LIMITATION: if MULTIPLE proposals got created in between, well. though luck. + let latest_on_chain = if let Some(latest_stored) = &latest_stored { + // those are sorted in ASCENDING way + self.shared_state + .proposals_since(latest_stored.proposal_id as u64) + .await? + .pop() + } else { + // but those are DESCENDING + self.shared_state + .last_proposal_page() + .await? + .first() + .cloned() + }; + + let now = OffsetDateTime::now_utc(); + + let prior_proposal = match (&latest_stored, latest_on_chain) { + (None, None) => { + // we haven't created any proposals before + trace!("this could be our first redemption proposal"); + None + } + (Some(stored), None) => { + if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now { + trace!("too soon to create new redemption proposal"); + return Ok(()); + } + None + } + (_, Some(on_chain)) => { + warn!("we seem to have crashed after creating proposal, but before persisting it onto disk!"); + + Some(on_chain) + } + }; + + // technically we could have been just caching all of those serial numbers as we verify tickets, + // but given how infrequently we call this, there's no point in wasting this memory + let verified_tickets = self + .shared_state + .storage + .get_all_verified_tickets_with_sn() + .await?; + + // TODO: somehow simplify that nasty nested if + if verified_tickets.len() < self.config.minimum_redemption_tickets { + // bypass the number of tickets check if we're about to lose our rewards due to expiration + if let Some(latest_stored) = latest_stored { + if latest_stored.created_at + self.config.maximum_time_between_redemption < now { + {} + } else { + info!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } else { + // first proposal + info!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } + + // this should have been ensured when querying + assert!(verified_tickets.len() <= u16::MAX as usize); + + let digest = + BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + + let prior_proposal_id = if let Some(prior_proposal) = prior_proposal { + if prior_proposal.description == encoded_digest { + info!("we have already created proposal for those tickets"); + Some(prior_proposal.id) + } else { + warn!( + "our missed proposal seem to have been for different tickets - abandoning it" + ); + None + } + } else { + None + }; + + // if the proposal has already existed on chain, do use it. otherwise create a new one + let proposal_id = if let Some(prior) = prior_proposal_id { + prior + } else { + self.create_redemption_proposal(&digest, verified_tickets.len() as u16) + .await? + }; + + if proposal_id > u32::MAX as u64 { + // realistically will we ever reach it? no. + panic!( + "we have created more than {} proposals. we can't handle that.", + u32::MAX + ) + } + + self.shared_state + .storage + .insert_redemption_proposal( + &verified_tickets, + proposal_id as u32, + OffsetDateTime::now_utc(), + ) + .await?; + + let current_epoch = self.shared_state.current_epoch_id().await?; + let api_clients = self.shared_state.api_clients(current_epoch).await?; + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingRedemptionVote::new( + proposal_id, + digest, + verified_tickets + .into_iter() + .map(|t| t.serial_number) + .collect(), + current_epoch, + ids, + ); + + let resolution = self + .try_resolve_pending_proposal(&mut pending, Some(api_clients)) + .await?; + if resolution.is_pending() { + warn!("failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later", pending.pending); + self.pending_redemptions.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + + Ok(()) + } + + async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> { + trace!("attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"); + + // 1. retry all operations that have failed in the past: verification requests and pending redemption + self.resolve_pending().await?; + + // 2. if applicable, attempt to redeem all newly verified tickets + self.maybe_redeem_tickets().await?; + + Ok(()) + } + + async fn run(mut self, mut shutdown: nym_task::TaskClient) { + info!("Starting Ecash CredentialSender"); + + // attempt to clear any pending operations + info!("attempting to resolve any pending operations"); + if let Err(err) = self.periodic_operations().await { + error!("failed to resolve pending operations on startup: {err}") + } + + let start = Instant::now() + self.config.pending_poller; + let mut resolver_interval = interval_at(start, self.config.pending_poller); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("client_handling::credentialSender : received shutdown"); + }, + Some(ticket) = self.ticket_receiver.next() => { + let (queued_up, _) = self.ticket_receiver.size_hint(); + + // this will help us determine if we need to parallelize it + match queued_up { + n if n < 5 => debug!("there are {n} tickets queued up that need processing"), + n if (5..20).contains(&n) => info!("there are {n} tickets queued up that need processing"), + n if (20..50).contains(&n) => warn!("there are {n} tickets queued up that need processing!"), + n => error!("there are {n} tickets queued up that need processing!"), + } + + self.handle_client_ticket(ticket).await + }, + _ = resolver_interval.tick() => { + if let Err(err) = self.periodic_operations().await { + error!("failed to deal with periodic operations: {err}") + } + } + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs new file mode 100644 index 0000000000..a15966bbab --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs @@ -0,0 +1,92 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::Storage; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_task::TaskClient; +use nym_validator_client::EcashApiClient; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::time::{interval, Duration}; +use tracing::{info, trace, warn}; + +#[derive(Clone)] +pub(crate) struct DoubleSpendingDetector { + spent_serial_numbers: Arc>, + shared_state: SharedState, +} + +impl DoubleSpendingDetector +where + S: Storage + Clone + Send + Sync + 'static, +{ + pub(crate) fn new(shared_state: SharedState) -> Self { + DoubleSpendingDetector { + spent_serial_numbers: Arc::new(RwLock::new(DoubleSpendingFilter::new_empty_ecash())), + shared_state, + } + } + + pub(crate) async fn check(&self, serial_number: &Vec) -> bool { + self.spent_serial_numbers.read().await.check(serial_number) + } + + async fn latest_api_endpoints( + &self, + ) -> Result>, EcashTicketError> { + let epoch_id = self.shared_state.current_epoch_id().await?; + self.shared_state.api_clients(epoch_id).await + } + + async fn refresh_bloomfilter(&self) { + //here be api query and union of different results + let mut filter_builder = self.spent_serial_numbers.read().await.rebuild(); + + let api_clients = match self.latest_api_endpoints().await { + Ok(clients) => clients, + Err(err) => { + warn!("failed to obtain current api clients: {err}"); + return; + } + }; + + for ecash_client in api_clients.deref().iter() { + match ecash_client.api_client.spent_credentials_filter().await { + Ok(response) => { + let added = filter_builder.add_bytes(&response.bitmap); + if !added { + warn!("Validator {ecash_client} gave us an incompatible bitmap for the double spending detector, we're gonna ignore it"); + } + } + Err(err) => { + warn!("Validator {ecash_client} could not be reached. There might be a problem with the coconut endpoint: {err}"); + } + } + } + + *self.spent_serial_numbers.write().await = filter_builder.build(); + } + + async fn run(&self, mut shutdown: TaskClient) { + info!("Starting Ecash DoubleSpendingDetector"); + let mut interval = interval(Duration::from_secs(300)); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("ecash_verifier::DoubleSpendingDetector : received shutdown"); + }, + _ = interval.tick() => self.refresh_bloomfilter().await, + + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs new file mode 100644 index 0000000000..1494598a55 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs @@ -0,0 +1,83 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::storage::error::StorageError; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EcashTicketError { + // TODO: this should be more granual + #[error(transparent)] + ApiFailure(#[from] EcashApiError), + + #[error(transparent)] + CredentialError(#[from] nym_credentials::error::Error), + + #[error("the provided ticket failed to get verified")] + MalformedTicket, + + #[error("failed to verify provided ticket due to invalid expiration date signatures")] + MalformedTicketInvalidDateSignatures, + + #[error("provided payinfo's public key does not match provider's")] + InvalidPayInfoPublicKey, + + #[error("provided payinfo's timestamp is invalid")] + InvalidPayInfoTimestamp, + + #[error("received payinfo is a duplicate")] + DuplicatePayInfo, + + #[error("could not handle the ecash ticket due to internal storage failure: {source}")] + InternalStorageFailure { + #[from] + source: StorageError, + }, + + #[error("failed to create ticket redemption proposal: {source}")] + RedemptionProposalCreationFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to execute ticket redemption proposal {proposal_id}: {source}")] + RedemptionProposalExecutionFailure { + proposal_id: u64, + + #[source] + source: NyxdError, + }, + + #[error("failed to parse out the redemption proposal id: {source}")] + ProposalIdParsingFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to query the nyx chain: {source}")] + ChainQueryFailure { + #[source] + source: NyxdError, + }, + + #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] + NotEnoughNymAPIs { received: usize, needed: usize }, + + #[error("the DKG contract is unavailable")] + UnavailableDkgContract, + + #[error("the DKG threshold value for epoch {epoch_id} is currently unavailable. we're probably mid-epoch transition")] + DKGThresholdUnavailable { epoch_id: EpochId }, + + #[error("could not create redemption proposal as we have tickets pending full verification")] + PendingTickets, +} + +impl EcashTicketError { + pub fn chain_query_failure(source: NyxdError) -> EcashTicketError { + EcashTicketError::ChainQueryFailure { source } + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs new file mode 100644 index 0000000000..9d767fba6c --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs @@ -0,0 +1,36 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::{Stream, StreamExt}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use std::ops::Deref; +use tokio::sync::RwLockReadGuard; + +pub(crate) fn apis_stream<'a>( + // if needed we could make this argument more generic to accept either locks or iterators, etc. + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], +) -> impl Stream + 'a { + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + all_clients + .deref() + .iter() + .filter(|client| filter_by_id.contains(&client.node_id)), + ) +} + +pub(crate) async fn for_each_api_concurrent<'a, F, Fut>( + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], + f: F, +) where + F: FnMut(&'a EcashApiClient) -> Fut, + Fut: Future, +{ + apis_stream(all_clients, filter_by_id) + .for_each_concurrent(32, f) + .await +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs new file mode 100644 index 0000000000..40d82abd91 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::storage::Storage; +use crate::GatewayError; +use credential_sender::CredentialHandler; +use double_spending::DoubleSpendingDetector; +use futures::channel::mpsc::{self, UnboundedSender}; +use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::{CompactEcashError, NymPayInfo, VerificationKeyAuth}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tracing::error; + +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +pub use credential_sender::ClientTicket; + +pub(crate) mod credential_sender; +pub(crate) mod double_spending; +pub(crate) mod error; +mod helpers; +mod state; + +const TIME_RANGE_SEC: i64 = 30; + +pub struct EcashManager { + shared_state: SharedState, + + pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier + pay_infos: Mutex>, + cred_sender: UnboundedSender, + double_spend_detector: DoubleSpendingDetector, +} + +impl EcashManager +where + S: Storage + Clone + 'static, +{ + pub async fn new( + credential_handler_cfg: CredentialHandlerConfig, + nyxd_client: DirectSigningHttpRpcNyxdClient, + pk_bytes: [u8; 32], + shutdown: nym_task::TaskClient, + storage: S, + ) -> Result { + let shared_state = SharedState::new(nyxd_client, storage).await?; + + let double_spend_detector = DoubleSpendingDetector::new(shared_state.clone()); + double_spend_detector.clone().start(shutdown.clone()); + + let (cred_sender, cred_receiver) = mpsc::unbounded(); + + let cs = + CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) + .await?; + cs.start(shutdown); + + Ok(EcashManager { + shared_state, + pk_bytes, + pay_infos: Default::default(), + cred_sender, + double_spend_detector, + }) + } + + pub async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.shared_state.verification_key(epoch_id).await + } + + //Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded + pub async fn check_payment( + &self, + credential: &CredentialSpendingData, + aggregated_verification_key: &VerificationKeyAuth, + ) -> Result<(), EcashTicketError> { + let insert_index = self.verify_pay_info(credential.pay_info.into()).await?; + + credential + .verify(aggregated_verification_key) + .map_err(|err| match err { + CompactEcashError::ExpirationDateSignatureValidity => { + EcashTicketError::MalformedTicketInvalidDateSignatures + } + _ => EcashTicketError::MalformedTicket, + })?; + + self.insert_pay_info(credential.pay_info.into(), insert_index) + .await + } + + pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result { + //Public key check + if pay_info.pk() != self.pk_bytes { + return Err(EcashTicketError::InvalidPayInfoPublicKey); + } + + //Timestamp range check + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let tmin = timestamp - TIME_RANGE_SEC; + let tmax = timestamp + TIME_RANGE_SEC; + if pay_info.timestamp() > tmax || pay_info.timestamp() < tmin { + return Err(EcashTicketError::InvalidPayInfoTimestamp); + } + + let mut inner = self.pay_infos.lock().await; + + //Cleanup inner + let low = inner.partition_point(|x| x.timestamp() < tmin); + let high = inner.partition_point(|x| x.timestamp() < tmax); + inner.truncate(high); + drop(inner.drain(..low)); + + //Duplicate check + match inner.binary_search_by(|info| info.timestamp().cmp(&pay_info.timestamp())) { + Result::Err(index) => Ok(index), + Result::Ok(index) => { + if inner[index] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + //tbh, I don't expect ending up here if all parties are honest + //binary search returns an arbitrary match, so we have to check for potential multiple matches + let mut i = index as i64; + while i >= 0 && inner[i as usize].timestamp() == pay_info.timestamp() { + if inner[i as usize] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i -= 1; + } + + let mut i = index + 1; + while i < inner.len() && inner[i].timestamp() == pay_info.timestamp() { + if inner[i] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i += 1; + } + Ok(index) + } + } + } + + async fn insert_pay_info( + &self, + pay_info: NymPayInfo, + index: usize, + ) -> Result<(), EcashTicketError> { + let mut inner = self.pay_infos.lock().await; + if index > inner.len() { + inner.push(pay_info); + return Ok(()); + } + inner.insert(index, pay_info); + Ok(()) + } + + pub async fn check_double_spend(&self, serial_number: &Vec) -> bool { + self.double_spend_detector.check(serial_number).await + } + + pub fn async_verify(&self, ticket: ClientTicket) { + // TODO: I guess do something for shutdowns + let _ = self + .cred_sender + .unbounded_send(ticket) + .inspect_err(|_| error!("failed to send the client ticket for verification task")); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs new file mode 100644 index 0000000000..57f39e95fd --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs @@ -0,0 +1,257 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::Storage; +use crate::GatewayError; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, MultisigQueryClient, NymContractsProvider, +}; +use nym_validator_client::nyxd::cw3::ProposalResponse; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::collections::BTreeMap; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tracing::{error, trace, warn}; + +// state shared by different subtasks dealing with credentials +#[derive(Clone)] +pub(crate) struct SharedState { + pub(crate) nyxd_client: Arc>, + pub(crate) address: AccountId, + pub(crate) epoch_data: Arc>>, + pub(crate) storage: S, +} + +impl SharedState +where + S: Storage + Clone, +{ + pub(crate) async fn new( + nyxd_client: DirectSigningHttpRpcNyxdClient, + storage: S, + ) -> Result { + let address = nyxd_client.address(); + + if nyxd_client.dkg_contract_address().is_none() { + error!("the DKG contract address is not available"); + return Err(EcashTicketError::UnavailableDkgContract.into()); + } + + let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { + error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); + // if we require coconut credentials, we MUST have DKG contract available + return Err(EcashTicketError::UnavailableDkgContract.into()); + }; + + let this = SharedState { + nyxd_client: Arc::new(RwLock::new(nyxd_client)), + address, + epoch_data: Arc::new(RwLock::new(BTreeMap::new())), + storage, + }; + + // might as well obtain the data for the current epoch, if applicable + if current_epoch.state.is_in_progress() { + if let Err(err) = this.set_epoch_data(current_epoch.epoch_id).await { + warn!("failed to set initial epoch data: {err}") + } + } + + Ok(this) + } + + fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool { + let Some(msg) = proposal.msgs.first() else { + return false; + }; + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else { + return false; + }; + let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_binary(msg) else { + return false; + }; + + gw == self.address.as_ref() + } + + /// retrieve all redemption proposals made by this gateway since, but excluding, the provided id + pub(crate) async fn proposals_since( + &self, + proposal_id: u64, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .list_proposals(Some(proposal_id), None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + /// retrieve all redemption proposals made by this gateway that are available on the last page of the query + pub(crate) async fn last_proposal_page( + &self, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .reverse_proposals(None, None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + async fn set_epoch_data( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let Some(threshold) = self.threshold(epoch_id).await? else { + return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id }); + }; + + // TODO: optimise: query nym-apis for aggregate key instead + // (when the below code was originally written, that query didn't exist) + let api_clients = self.query_api_clients(epoch_id).await?; + + if api_clients.len() < threshold as usize { + return Err(EcashTicketError::NotEnoughNymAPIs { + received: api_clients.len(), + needed: threshold as usize, + }); + } + + let aggregated_verification_key = + nym_credentials::aggregate_verification_keys(&api_clients)?; + + let mut guard = self.epoch_data.write().await; + + self.storage + .insert_epoch_signers( + epoch_id as i64, + api_clients.iter().map(|c| c.node_id as i64).collect(), + ) + .await?; + + guard.insert( + epoch_id, + EpochState { + api_clients, + master_key: aggregated_verification_key, + threshold, + }, + ); + Ok(guard) + } + + async fn query_api_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + Ok(all_ecash_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) + } + + pub(crate) async fn threshold( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.nyxd_client + .read() + .await + .get_epoch_threshold(epoch_id) + .await + .map_err(EcashTicketError::chain_query_failure) + } + + pub(crate) async fn api_clients( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.api_clients)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.api_clients).unwrap() + })) + } + + pub(crate) async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.master_key)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.master_key).unwrap() + })) + } + + pub(crate) async fn start_tx(&self) -> RwLockWriteGuard { + self.nyxd_client.write().await + } + + pub(crate) async fn start_query(&self) -> RwLockReadGuard { + self.nyxd_client.read().await + } + + pub(crate) async fn current_epoch_id(&self) -> Result { + Ok(self + .start_query() + .await + .get_current_epoch() + .await + .map_err(EcashTicketError::chain_query_failure)? + .epoch_id) + } +} + +pub(crate) struct EpochState { + // note: **CURRENTLY** api client addresses don't change during the epochs + pub(crate) api_clients: Vec, + pub(crate) master_key: VerificationKeyAuth, + + #[allow(unused)] + pub(crate) threshold: u64, +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 4f00ebcf90..611f25c22a 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -6,7 +6,6 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use log::*; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, @@ -21,10 +20,12 @@ use nym_gateway_requests::{ use nym_mixnet_client::forwarder::MixForwardingSender; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; +use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; @@ -123,11 +124,11 @@ impl InitialAuthenticationError { pub(crate) struct FreshHandler { rng: R, - pub(crate) shared_state: CommonHandlerState, + pub(crate) shared_state: CommonHandlerState, pub(crate) active_clients_store: ActiveClientsStore, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) socket_connection: SocketStream, - pub(crate) storage: St, + pub(crate) peer_address: SocketAddr, // currently unused (but populated) pub(crate) negotiated_protocol: Option, @@ -136,7 +137,7 @@ pub(crate) struct FreshHandler { impl FreshHandler where R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult @@ -147,16 +148,16 @@ where rng: R, conn: S, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, + peer_address: SocketAddr, ) -> Self { FreshHandler { rng, active_clients_store, outbound_mix_sender, socket_connection: SocketStream::RawTcp(conn), - storage, + peer_address, negotiated_protocol: None, shared_state, } @@ -311,6 +312,7 @@ where loop { // retrieve some messages let (messages, new_start_next_after) = self + .shared_state .storage .retrieve_messages(client_address, start_next_after) .await?; @@ -326,7 +328,7 @@ where return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store - self.storage.remove_messages(ids).await?; + self.shared_state.storage.remove_messages(ids).await?; } // no more messages to grab @@ -356,7 +358,11 @@ where encrypted_address: EncryptedAddressBytes, iv: IV, ) -> Result, InitialAuthenticationError> { - let shared_keys = self.storage.get_shared_keys(client_address).await?; + let shared_keys = self + .shared_state + .storage + .get_shared_keys(client_address) + .await?; if let Some(shared_keys) = shared_keys { // this should never fail as we only ever construct persisted shared keys ourselves when inserting @@ -543,39 +549,42 @@ where .await?; } - let shared_keys = self + let Some(shared_keys) = self .authenticate_client(address, encrypted_address, iv) - .await?; - let status = shared_keys.is_some(); + .await? + else { + // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour + return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + }; - let available_bandwidth: AvailableBandwidth = - self.storage.get_available_bandwidth(address).await?.into(); + let client_id = self.shared_state.storage.get_client_id(address).await?; - let bandwidth_remaining = if available_bandwidth.freepass_expired() { - self.expire_freepass(address).await?; + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .into(); + + let bandwidth_remaining = if available_bandwidth.expired() { + self.expire_bandwidth(client_id).await?; 0 } else { available_bandwidth.bytes }; - let client_details = - shared_keys.map(|shared_keys| ClientDetails::new(address, shared_keys)); - Ok(InitialAuthResult::new( - client_details, + Some(ClientDetails::new(client_id, address, shared_keys)), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), - status, + status: true, bandwidth_remaining, }, )) } - pub(crate) async fn expire_freepass( - &self, - client: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.storage.reset_freepass_bandwidth(client).await + pub(crate) async fn expire_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.shared_state.storage.reset_bandwidth(client_id).await } /// Attempts to finalize registration of the client by storing the derived shared keys in the @@ -588,34 +597,41 @@ where /// * `client`: details (i.e. address and shared keys) of the registered client async fn register_client( &mut self, - client: &ClientDetails, - ) -> Result + client_address: DestinationAddressBytes, + client_shared_keys: &SharedKeys, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin, { debug!( "Processing register client request for: {}", - client.address.as_base58_string() + client_address.as_base58_string() ); - self.storage - .insert_shared_keys(client.address, &client.shared_keys) + let client_id = self + .shared_state + .storage + .insert_shared_keys(client_address, client_shared_keys) .await?; // see if we have bandwidth entry for the client already, if not, create one with zero value if self + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client_id) .await? .is_none() { - self.storage.create_bandwidth_entry(client.address).await?; + self.shared_state + .storage + .create_bandwidth_entry(client_id) + .await?; } - self.push_stored_messages_to_client(client.address, &client.shared_keys) + self.push_stored_messages_to_client(client_address, client_shared_keys) .await?; - Ok(true) + Ok(client_id) } /// Tries to handle the received register request by checking attempting to complete registration @@ -644,15 +660,15 @@ where } let shared_keys = self.perform_registration_handshake(init_data).await?; - let client_details = ClientDetails::new(remote_address, shared_keys); + let client_id = self.register_client(remote_address, &shared_keys).await?; - let status = self.register_client(&client_details).await?; + let client_details = ClientDetails::new(client_id, remote_address, shared_keys); Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { protocol_version: Some(negotiated_protocol), - status, + status: true, }, )) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index ee880d4832..94ff8398f7 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::node::storage::Storage; -use log::{trace, warn}; +use nym_credentials::ecash::utils::ecash_today; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; @@ -13,13 +13,14 @@ use std::time::Duration; use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; +use tracing::{instrument, trace, warn}; use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; -pub(crate) mod coconut; +pub(crate) mod ecash; mod fresh; // TODO: note for my future self to consider the following idea: @@ -44,13 +45,15 @@ impl SocketStream { pub(crate) struct ClientDetails { #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, + pub(crate) id: i64, pub(crate) shared_keys: SharedKeys, } impl ClientDetails { - pub(crate) fn new(address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { + pub(crate) fn new(id: i64, address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { ClientDetails { address, + id, shared_keys, } } @@ -68,15 +71,28 @@ impl InitialAuthResult { server_response, } } + + fn new_failed(protocol_version: Option) -> Self { + InitialAuthResult { + client_details: None, + server_response: ServerResponse::Authenticate { + protocol_version, + status: false, + bandwidth_remaining: 0, + }, + } + } } +// imo there's no point in including the peer address in anything higher than debug +#[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))] pub(crate) async fn handle_connection( mut handle: FreshHandler, mut shutdown: TaskClient, ) where R: Rng + CryptoRng, S: AsyncRead + AsyncWrite + Unpin + Send, - St: Storage, + St: Storage + Clone + 'static, { // If the connection handler abruptly stops, we shouldn't signal global shutdown shutdown.mark_as_success(); @@ -136,27 +152,36 @@ impl<'a> From<&'a Config> for BandwidthFlushingBehaviourConfig { } } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub(crate) struct AvailableBandwidth { pub(crate) bytes: i64, - pub(crate) freepass_expiration: Option, + pub(crate) expiration: OffsetDateTime, } impl AvailableBandwidth { - pub(crate) fn freepass_expired(&self) -> bool { - if let Some(expiration) = self.freepass_expiration { - if expiration < OffsetDateTime::now_utc() { - return true; - } + pub(crate) fn expired(&self) -> bool { + self.expiration < ecash_today() + } +} + +impl Default for AvailableBandwidth { + fn default() -> Self { + Self { + bytes: 0, + expiration: OffsetDateTime::UNIX_EPOCH, } - false } } pub(crate) struct ClientBandwidth { pub(crate) bandwidth: AvailableBandwidth, pub(crate) last_flushed: OffsetDateTime, - pub(crate) bytes_at_last_flush: i64, + + /// the number of bytes the client had during the last sync. + /// it is used to determine whether the current value should be synced with the storage + /// by checking the delta with the known amount + pub(crate) bytes_at_last_sync: i64, + pub(crate) bytes_delta_since_sync: i64, } impl ClientBandwidth { @@ -164,14 +189,13 @@ impl ClientBandwidth { ClientBandwidth { bandwidth, last_flushed: OffsetDateTime::now_utc(), - bytes_at_last_flush: bandwidth.bytes, + bytes_at_last_sync: bandwidth.bytes, + bytes_delta_since_sync: 0, } } - pub(crate) fn should_flush(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { - if (self.bytes_at_last_flush - self.bandwidth.bytes).abs() - >= cfg.client_bandwidth_max_delta_flushing_amount - { + pub(crate) fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { + if self.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount { return true; } @@ -182,8 +206,9 @@ impl ClientBandwidth { false } - pub(crate) fn update_flush_data(&mut self) { + pub(crate) fn update_sync_data(&mut self) { self.last_flushed = OffsetDateTime::now_utc(); - self.bytes_at_last_flush = self.bandwidth.bytes; + self.bytes_at_last_sync = self.bandwidth.bytes; + self.bytes_delta_since_sync = 0; } } diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index a33f6a0fb5..383e1b4b7d 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -5,20 +5,23 @@ use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::FreshHandler; use crate::node::storage::Storage; -use log::*; use nym_mixnet_client::forwarder::MixForwardingSender; use rand::rngs::OsRng; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; -pub(crate) struct Listener { +pub(crate) struct Listener { address: SocketAddr, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, } -impl Listener { - pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { +impl Listener +where + S: Storage + Send + Sync + Clone + 'static, +{ + pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { Listener { address, shared_state, @@ -27,15 +30,12 @@ impl Listener { // TODO: change the signature to pub(crate) async fn run(&self, handler: Handler) - pub(crate) async fn run( + pub(crate) async fn run( &mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, mut shutdown: nym_task::TaskClient, - ) where - St: Storage + Clone + 'static, - { + ) { info!("Starting websocket listener at {}", self.address); let tcp_listener = match tokio::net::TcpListener::bind(self.address).await { Ok(listener) => listener, @@ -61,9 +61,9 @@ impl Listener { OsRng, socket, outbound_mix_sender.clone(), - storage.clone(), active_clients_store.clone(), self.shared_state.clone(), + remote_addr, ); let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}")); tokio::spawn(async move { handle.start_handling(shutdown).await }); @@ -76,18 +76,14 @@ impl Listener { } } - pub(crate) fn start( + pub(crate) fn start( mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, shutdown: nym_task::TaskClient, - ) -> JoinHandle<()> - where - St: Storage + Clone + 'static, - { + ) -> JoinHandle<()> { tokio::spawn(async move { - self.run(outbound_mix_sender, storage, active_clients_store, shutdown) + self.run(outbound_mix_sender, active_clients_store, shutdown) .await }) } diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index d07dc6489e..4cd1f20160 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -8,7 +8,6 @@ use crate::node::storage::error::StorageError; use crate::node::storage::Storage; use futures::channel::mpsc::SendError; use futures::StreamExt; -use log::*; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop; use nym_sphinx::forwarding::packet::MixPacket; @@ -21,6 +20,7 @@ use std::net::SocketAddr; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; +use tracing::*; // defines errors that warrant a panic if not thrown in the context of a shutdown #[derive(Debug, Error)] diff --git a/gateway/src/node/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs index 9ca22c77af..39d21cd814 100644 --- a/gateway/src/node/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -3,11 +3,11 @@ use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::storage::Storage; -use log::*; use nym_task::TaskClient; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; pub(crate) struct Listener { address: SocketAddr, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index c17c9dd4a8..a306a82779 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -12,11 +12,10 @@ use crate::http::HttpApiBuilder; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use futures::channel::{mpsc, oneshot}; -use log::*; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; @@ -30,13 +29,15 @@ use rand::thread_rng; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use tracing::*; pub(crate) mod client_handling; pub(crate) mod helpers; pub(crate) mod mixnet_handling; pub(crate) mod storage; -pub use storage::{InMemStorage, PersistentStorage, Storage}; +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +pub use storage::{PersistentStorage, Storage}; // TODO: should this struct live here? struct StartedNetworkRequester { @@ -327,9 +328,9 @@ impl Gateway { forwarding_channel: MixForwardingSender, active_clients_store: ActiveClientsStore, shutdown: TaskClient, - coconut_verifier: Arc, + ecash_verifier: Arc>, ) where - St: Storage + Clone + 'static, + St: Storage + Send + Sync + Clone + 'static, { info!("Starting client [web]socket listener..."); @@ -339,7 +340,8 @@ impl Gateway { ); let shared_state = websocket::CommonHandlerState { - coconut_verifier, + ecash_verifier, + storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), only_coconut_credentials: self.config.gateway.only_coconut_credentials, bandwidth_cfg: (&self.config).into(), @@ -347,7 +349,6 @@ impl Gateway { websocket::Listener::new(listening_address, shared_state).start( forwarding_channel, - self.storage.clone(), active_clients_store, shutdown, ); @@ -576,8 +577,32 @@ impl Gateway { } } - let coconut_verifier = - CoconutVerifier::new(nyxd_client, self.config.gateway.only_coconut_credentials).await?; + let handler_config = CredentialHandlerConfig { + revocation_bandwidth_penalty: self + .config + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: self.config.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: self.config.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: self.config.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: self + .config + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }; + + let ecash_manager = { + EcashManager::new( + handler_config, + nyxd_client, + self.identity_keypair.public_key().to_bytes(), + shutdown.fork("EcashVerifier"), + self.storage.clone(), + ) + .await + }?; let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); @@ -592,7 +617,7 @@ impl Gateway { mix_forwarding_channel.clone(), active_clients_store.clone(), shutdown.fork("websocket::Listener"), - Arc::new(coconut_verifier), + Arc::new(ecash_manager), ); let nr_request_filter = if self.config.network_requester.enabled { diff --git a/gateway/src/node/storage/bandwidth.rs b/gateway/src/node/storage/bandwidth.rs index 1ebcccea85..991c77d82e 100644 --- a/gateway/src/node/storage/bandwidth.rs +++ b/gateway/src/node/storage/bandwidth.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::storage::models::{PersistedBandwidth, SpentCredential}; +use crate::node::storage::models::PersistedBandwidth; use time::OffsetDateTime; #[derive(Clone)] @@ -20,42 +20,32 @@ impl BandwidthManager { } /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn insert_new_client( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + pub(crate) async fn insert_new_client(&self, client_id: i64) -> Result<(), sqlx::Error> { + // FIXME: hack; we need to change api slightly sqlx::query!( - "INSERT INTO available_bandwidth(client_address_bs58, available) VALUES (?, 0)", - client_address_bs58 + "INSERT INTO available_bandwidth(client_id, available, expiration) VALUES (?, 0, ?)", + client_id, + OffsetDateTime::UNIX_EPOCH, ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Set the freepass expiration date of the particular client to the provided date. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - /// * `freepass_expiration`: the expiration date of the associated free pass. - pub(crate) async fn set_freepass_expiration( + /// Set the expiration date of the particular client to the provided date. + pub(crate) async fn set_expiration( &self, - client_address_bs58: &str, - freepass_expiration: OffsetDateTime, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET freepass_expiration = ? - WHERE client_address_bs58 = ? + SET expiration = ? + WHERE client_id = ? "#, - freepass_expiration, - client_address_bs58 + expiration, + client_id ) .execute(&self.connection_pool) .await?; @@ -63,21 +53,15 @@ impl BandwidthManager { } /// Reset all the bandwidth associated with the freepass and reset its expiration date - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn reset_freepass_bandwidth( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + pub(crate) async fn reset_bandwidth(&self, client_id: i64) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET available = 0, freepass_expiration = NULL - WHERE client_address_bs58 = ? + SET available = 0, expiration = ? + WHERE client_id = ? "#, - client_address_bs58 + OffsetDateTime::UNIX_EPOCH, + client_id ) .execute(&self.connection_pool) .await?; @@ -85,92 +69,92 @@ impl BandwidthManager { } /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. pub(crate) async fn get_available_bandwidth( &self, - client_address_bs58: &str, + client_id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_address_bs58 = ?") - .bind(client_address_bs58) + sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_id = ?") + .bind(client_id) .fetch_optional(&self.connection_pool) .await } - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - pub(crate) async fn set_available_bandwidth( + pub(crate) async fn increase_bandwidth( &self, - client_address_bs58: &str, + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = available + ? + WHERE client_id = ? + "#, + amount, + client_id + ) + .execute(&mut tx) + .await?; + + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id + ) + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) + } + + pub(crate) async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, amount: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET available = ? - WHERE client_address_bs58 = ? + SET available = available - ? + WHERE client_id = (SELECT client_id FROM received_ticket WHERE id = ?) "#, amount, - client_address_bs58 + ticket_id, ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - /// * `was_freepass`: indicates whether the spent credential was a freepass - /// * `client_address_bs58`: address of the client that spent the credential - pub(crate) async fn insert_spent_credential( + pub(crate) async fn decrease_bandwidth( &self, - blinded_serial_number_bs58: &str, - was_freepass: bool, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; sqlx::query!( r#" - INSERT INTO spent_credential - (blinded_serial_number_bs58, was_freepass, client_address_bs58) - VALUES (?, ?, ?) + UPDATE available_bandwidth + SET available = available - ? + WHERE client_id = ? "#, - blinded_serial_number_bs58, - was_freepass, - client_address_bs58 + amount, + client_id ) - .execute(&self.connection_pool) + .execute(&mut tx) .await?; - Ok(()) - } - /// Retrieve the spent credential with the provided blinded serial number from the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - pub(crate) async fn retrieve_spent_credential( - &self, - blinded_serial_number_bs58: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - SpentCredential, - r#" - SELECT * FROM spent_credential - WHERE blinded_serial_number_bs58 = ? - LIMIT 1 - "#, - blinded_serial_number_bs58, + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id ) - .fetch_optional(&self.connection_pool) - .await + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) } } diff --git a/gateway/src/node/storage/error.rs b/gateway/src/node/storage/error.rs index f908eac083..4cf292ea57 100644 --- a/gateway/src/node/storage/error.rs +++ b/gateway/src/node/storage/error.rs @@ -10,4 +10,10 @@ pub enum StorageError { #[error("Failed to perform database migration: {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), + + #[error("Somehow stored data is incorrect: {0}")] + DataCorruption(String), + + #[error("the stored data associated with ticket {ticket_id} is malformed!")] + MalformedStoredTicketData { ticket_id: i64 }, } diff --git a/gateway/src/node/storage/mod.rs b/gateway/src/node/storage/mod.rs index e7123a44a6..d339fc779f 100644 --- a/gateway/src/node/storage/mod.rs +++ b/gateway/src/node/storage/mod.rs @@ -1,28 +1,37 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::storage::bandwidth::BandwidthManager; use crate::node::storage::error::StorageError; use crate::node::storage::inboxes::InboxManager; -use crate::node::storage::models::{PersistedBandwidth, PersistedSharedKeys, StoredMessage}; +use crate::node::storage::models::{ + PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage, VerifiedTicket, +}; use crate::node::storage::shared_keys::SharedKeysManager; +use crate::node::storage::tickets::TicketStorageManager; use async_trait::async_trait; -use log::{debug, error}; -use nym_credentials_interface::{Base58, BlindedSerialNumber}; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_sphinx::DestinationAddressBytes; use sqlx::ConnectOptions; use std::path::Path; use time::OffsetDateTime; +use tracing::{debug, error}; mod bandwidth; pub(crate) mod error; mod inboxes; -mod models; +pub(crate) mod models; mod shared_keys; +mod tickets; #[async_trait] pub trait Storage: Send + Sync { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result; + /// Inserts provided derived shared keys into the database. /// If keys previously existed for the provided client, they are overwritten with the new data. /// @@ -34,7 +43,7 @@ pub trait Storage: Send + Sync { &self, client_address: DestinationAddressBytes, shared_keys: &SharedKeys, - ) -> Result<(), StorageError>; + ) -> Result; /// Tries to retrieve shared keys stored for the particular client. /// @@ -94,81 +103,110 @@ pub trait Storage: Send + Sync { async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError>; /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn create_bandwidth_entry( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError>; /// Set the freepass expiration date of the particular client to the provided date. /// /// # Arguments /// /// * `client_address`: address of the client - /// * `freepass_expiration`: the expiration date of the associated free pass. - async fn set_freepass_expiration( + /// * `expiration`: the expiration date of the associated free pass. + async fn set_expiration( &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), StorageError>; - /// Reset all the bandwidth associated with the freepass and reset its expiration date + /// Reset all the bandwidth /// /// # Arguments /// /// * `client_address`: address of the client - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError>; /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client async fn get_available_bandwidth( &self, - client_address: DestinationAddressBytes, + client_id: i64, ) -> Result, StorageError>; - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - async fn set_bandwidth( + /// Increases specified client's bandwidth by the provided amount and returns the current value. + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn revoke_ticket_bandwidth( &self, - client_address: DestinationAddressBytes, + ticket_id: i64, amount: i64, ) -> Result<(), StorageError>; - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - /// * `client_address`: address of the client that spent the credential - async fn insert_spent_credential( + #[allow(dead_code)] + /// Decreases specified client's bandwidth by the provided amount and returns the current value. + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn insert_epoch_signers( &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, + epoch_id: i64, + signer_ids: Vec, ) -> Result<(), StorageError>; - /// Check if the credential with the provided blinded serial number if already present in the storage. + async fn insert_received_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result; + + // note: this only checks very recent tickets that haven't yet been redeemed + // (but it's better than nothing) + /// Check if the ticket with the provided serial number if already present in the storage. /// /// # Arguments /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - async fn contains_credential( + /// * `serial_number`: the unique serial number embedded in the ticket + async fn contains_ticket(&self, serial_number: &[u8]) -> Result; + + async fn insert_ticket_verification( &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result; + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), StorageError>; + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError>; + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError>; + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError>; + + async fn clear_post_proposal_data( + &self, + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError>; + + async fn latest_proposal(&self) -> Result, StorageError>; + + async fn get_all_unverified_tickets(&self) -> Result, StorageError>; + async fn get_all_unresolved_proposals(&self) -> Result, StorageError>; + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError>; + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError>; } // note that clone here is fine as upon cloning the same underlying pool will be used @@ -177,6 +215,7 @@ pub struct PersistentStorage { shared_key_manager: SharedKeysManager, inbox_manager: InboxManager, bandwidth_manager: BandwidthManager, + ticket_manager: TicketStorageManager, } impl PersistentStorage { @@ -222,26 +261,37 @@ impl PersistentStorage { Ok(PersistentStorage { shared_key_manager: SharedKeysManager::new(connection_pool.clone()), inbox_manager: InboxManager::new(connection_pool.clone(), message_retrieval_limit), - bandwidth_manager: BandwidthManager::new(connection_pool), + bandwidth_manager: BandwidthManager::new(connection_pool.clone()), + ticket_manager: TicketStorageManager::new(connection_pool), }) } } #[async_trait] impl Storage for PersistentStorage { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + Ok(self + .shared_key_manager + .client_id(&client_address.as_base58_string()) + .await?) + } + async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - let persisted_shared_keys = PersistedSharedKeys { - client_address_bs58: client_address.as_base58_string(), - derived_aes128_ctr_blake3_hmac_keys_bs58: shared_keys.to_base58_string(), - }; - self.shared_key_manager - .insert_shared_keys(persisted_shared_keys) + ) -> Result { + let client_id = self + .shared_key_manager + .insert_shared_keys( + client_address.as_base58_string(), + shared_keys.to_base58_string(), + ) .await?; - Ok(()) + Ok(client_id) } async fn get_shared_keys( @@ -296,194 +346,237 @@ impl Storage for PersistentStorage { Ok(()) } - async fn create_bandwidth_entry( + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.insert_new_client(client_id).await?; + Ok(()) + } + + async fn set_expiration( &self, - client_address: DestinationAddressBytes, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), StorageError> { self.bandwidth_manager - .insert_new_client(&client_address.as_base58_string()) + .set_expiration(client_id, expiration) .await?; Ok(()) } - async fn set_freepass_expiration( - &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_freepass_expiration(&client_address.as_base58_string(), freepass_expiration) - .await?; - Ok(()) - } - - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .reset_freepass_bandwidth(&client_address.as_base58_string()) - .await?; + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.reset_bandwidth(client_id).await?; Ok(()) } async fn get_available_bandwidth( &self, - client_address: DestinationAddressBytes, + client_id: i64, ) -> Result, StorageError> { Ok(self .bandwidth_manager - .get_available_bandwidth(&client_address.as_base58_string()) + .get_available_bandwidth(client_id) .await?) } - async fn set_bandwidth( + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .increase_bandwidth(client_id, amount) + .await?) + } + + async fn revoke_ticket_bandwidth( &self, - client_address: DestinationAddressBytes, + ticket_id: i64, amount: i64, ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_available_bandwidth(&client_address.as_base58_string(), amount) + Ok(self + .bandwidth_manager + .revoke_ticket_bandwidth(ticket_id, amount) + .await?) + } + + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .decrease_bandwidth(client_id, amount) + .await?) + } + + async fn insert_epoch_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), StorageError> { + self.ticket_manager + .insert_ecash_signers(epoch_id, signer_ids) .await?; Ok(()) } - async fn insert_spent_credential( + async fn insert_received_ticket( &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result { + // technically if we crash between those 2 calls we'll have a bit of data inconsistency, + // but nothing too tragic. we just won't get paid for a single ticket + let ticket_id = self + .ticket_manager + .insert_new_ticket(client_id, received_at) + .await?; + self.ticket_manager + .insert_ticket_data(ticket_id, &serial_number, &data) + .await?; + + Ok(ticket_id) + } + + async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + Ok(self.ticket_manager.has_ticket_data(serial_number).await?) + } + + async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, ) -> Result<(), StorageError> { - self.bandwidth_manager - .insert_spent_credential( - &blinded_serial_number.to_bs58(), - was_freepass, - &client_address.as_base58_string(), + self.ticket_manager + .insert_ticket_verification(ticket_id, signer_id, verified_at, accepted) + .await?; + Ok(()) + } + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // TODO: + error!("unimplemented: decrease clients bandwidth"); + + // set the ticket as rejected + self.ticket_manager.set_rejected_ticket(ticket_id).await?; + + // drop all ticket_data - we no longer need it + // TODO: or maybe we do as a proof of receiving bad data? + self.ticket_manager.remove_ticket_data(ticket_id).await?; + + Ok(()) + } + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // 1. insert into verified table + self.ticket_manager + .insert_verified_ticket(ticket_id) + .await?; + + // TODO: maybe we want to leave that be until ticket gets fully redeemed instead? + // 2. remove individual verifications + self.ticket_manager + .remove_ticket_verification(ticket_id) + .await?; + Ok(()) + } + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError> { + self.ticket_manager + .remove_binary_ticket_data(ticket_id) + .await?; + Ok(()) + } + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_verified_tickets_with_sn() + .await?) + } + + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_proposed_tickets_with_sn(proposal_id as i64) + .await?) + } + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError> { + // if we crash between those, there might a bit of an issue. we should revisit it later + + // 1. insert the actual proposal + self.ticket_manager + .insert_redemption_proposal(proposal_id as i64, created_at) + .await?; + + // 2. update all the associated tickets + self.ticket_manager + .insert_verified_tickets_proposal_id( + tickets.iter().map(|t| t.ticket_id), + proposal_id as i64, ) .await?; Ok(()) } - async fn contains_credential( + async fn clear_post_proposal_data( &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - let cred = self - .bandwidth_manager - .retrieve_spent_credential(&blinded_serial_number.to_bs58()) + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError> { + // 1. update proposal metadata + self.ticket_manager + .update_redemption_proposal(proposal_id as i64, resolved_at, rejected) .await?; - Ok(cred.is_some()) - } -} - -/// In-memory implementation of `Storage`. The intention is primarily in testing environments. -#[derive(Clone)] -pub struct InMemStorage; - -//#[cfg(test)] -//impl InMemStorage { -// #[allow(unused)] -// async fn init + Send>() -> Result { -// todo!() -// } -//} - -#[cfg(test)] -#[async_trait] -impl Storage for InMemStorage { - async fn insert_shared_keys( - &self, - _client_address: DestinationAddressBytes, - _shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn remove_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn store_message( - &self, - _client_address: DestinationAddressBytes, - _message: Vec, - ) -> Result<(), StorageError> { - todo!() - } - - async fn retrieve_messages( - &self, - _client_address: DestinationAddressBytes, - _start_after: Option, - ) -> Result<(Vec, Option), StorageError> { - todo!() - } - - async fn remove_messages(&self, _ids: Vec) -> Result<(), StorageError> { - todo!() - } - - async fn create_bandwidth_entry( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn set_freepass_expiration( - &self, - _client_address: DestinationAddressBytes, - _freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - todo!() - } - - async fn reset_freepass_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_available_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn set_bandwidth( - &self, - _client_address: DestinationAddressBytes, - _amount: i64, - ) -> Result<(), StorageError> { - todo!() - } - - async fn insert_spent_credential( - &self, - _blinded_serial_number: BlindedSerialNumber, - _was_freepass: bool, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn contains_credential( - &self, - _blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - todo!() + // 2. remove ticket data rows (we can drop serial numbers) + self.ticket_manager + .remove_redeemed_tickets_data(proposal_id as i64) + .await?; + + // 3. remove verified tickets rows + self.ticket_manager + .remove_verified_tickets(proposal_id as i64) + .await?; + + Ok(()) + } + + async fn latest_proposal(&self) -> Result, StorageError> { + Ok(self.ticket_manager.get_latest_redemption_proposal().await?) + } + + async fn get_all_unverified_tickets(&self) -> Result, StorageError> { + self.ticket_manager + .get_unverified_tickets() + .await? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + async fn get_all_unresolved_proposals(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_unresolved_redemption_proposal_ids() + .await?) + } + + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_verification_votes(ticket_id) + .await?) + } + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError> { + Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?) } } diff --git a/gateway/src/node/storage/models.rs b/gateway/src/node/storage/models.rs index 999c9f827f..9a9f0145c0 100644 --- a/gateway/src/node/storage/models.rs +++ b/gateway/src/node/storage/models.rs @@ -1,11 +1,18 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; +use crate::node::storage::error::StorageError; +use nym_credentials_interface::CredentialSpendingData; use sqlx::FromRow; use time::OffsetDateTime; pub struct PersistedSharedKeys { + #[allow(dead_code)] + pub(crate) id: i64, + + #[allow(dead_code)] pub(crate) client_address_bs58: String, pub(crate) derived_aes128_ctr_blake3_hmac_keys_bs58: String, } @@ -22,14 +29,14 @@ pub struct PersistedBandwidth { #[allow(dead_code)] pub(crate) client_address_bs58: String, pub(crate) available: i64, - pub(crate) freepass_expiration: Option, + pub(crate) expiration: Option, } impl From for AvailableBandwidth { fn from(value: PersistedBandwidth) -> Self { AvailableBandwidth { bytes: value.available, - freepass_expiration: value.freepass_expiration, + expiration: value.expiration.unwrap_or(OffsetDateTime::UNIX_EPOCH), } } } @@ -43,12 +50,35 @@ impl From> for AvailableBandwidth { } } -#[derive(Debug, Clone, FromRow)] -pub struct SpentCredential { - #[allow(dead_code)] - pub(crate) blinded_serial_number_bs58: String, - #[allow(dead_code)] - pub(crate) was_freepass: bool, - #[allow(dead_code)] - pub(crate) client_address_bs58: String, +#[derive(FromRow)] +pub struct VerifiedTicket { + pub(crate) serial_number: Vec, + pub(crate) ticket_id: i64, +} + +#[derive(FromRow)] +pub struct RedemptionProposal { + pub(crate) proposal_id: i64, + pub(crate) created_at: OffsetDateTime, +} + +#[derive(FromRow)] +pub struct UnverifiedTicketData { + pub(crate) data: Vec, + pub(crate) ticket_id: i64, +} + +impl TryFrom for ClientTicket { + type Error = StorageError; + + fn try_from(value: UnverifiedTicketData) -> Result { + Ok(ClientTicket { + spending_data: CredentialSpendingData::try_from_bytes(&value.data).map_err(|_| { + StorageError::MalformedStoredTicketData { + ticket_id: value.ticket_id, + } + })?, + ticket_id: value.ticket_id, + }) + } } diff --git a/gateway/src/node/storage/shared_keys.rs b/gateway/src/node/storage/shared_keys.rs index d04ab12492..de1ce0abf7 100644 --- a/gateway/src/node/storage/shared_keys.rs +++ b/gateway/src/node/storage/shared_keys.rs @@ -18,6 +18,17 @@ impl SharedKeysManager { SharedKeysManager { connection_pool } } + pub(crate) async fn client_id(&self, client_address_bs58: &str) -> Result { + let client_id = sqlx::query!( + "SELECT id FROM shared_keys WHERE client_address_bs58 = ?", + client_address_bs58 + ) + .fetch_one(&self.connection_pool) + .await? + .id; + Ok(client_id) + } + /// Inserts provided derived shared keys into the database. /// If keys previously existed for the provided client, they are overwritten with the new data. /// @@ -26,13 +37,15 @@ impl SharedKeysManager { /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. pub(crate) async fn insert_shared_keys( &self, - shared_keys: PersistedSharedKeys, - ) -> Result<(), sqlx::Error> { + client_address_bs58: String, + derived_aes128_ctr_blake3_hmac_keys_bs58: String, + ) -> Result { sqlx::query!("INSERT OR REPLACE INTO shared_keys(client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) VALUES (?, ?)", - shared_keys.client_address_bs58, - shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, + client_address_bs58, + derived_aes128_ctr_blake3_hmac_keys_bs58, ).execute(&self.connection_pool).await?; - Ok(()) + + self.client_id(&client_address_bs58).await } /// Tries to retrieve shared keys stored for the particular client. diff --git a/gateway/src/node/storage/tickets.rs b/gateway/src/node/storage/tickets.rs new file mode 100644 index 0000000000..19a4287801 --- /dev/null +++ b/gateway/src/node/storage/tickets.rs @@ -0,0 +1,358 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::storage::models::{RedemptionProposal, UnverifiedTicketData, VerifiedTicket}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct TicketStorageManager { + connection_pool: sqlx::SqlitePool, +} + +impl TicketStorageManager { + /// Creates new instance of the `CredentialManager` with the provided sqlite connection pool. + /// + /// # Arguments + /// + /// * `connection_pool`: database connection pool to use. + pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { + TicketStorageManager { connection_pool } + } + + pub(crate) async fn insert_ecash_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), sqlx::Error> { + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_signer (epoch_id, signer_id) "); + + query_builder.push_values(signer_ids, |mut b, signer_id| { + b.push_bind(epoch_id).push_bind(signer_id); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn insert_new_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + ) -> Result { + Ok(sqlx::query!( + "INSERT INTO received_ticket (client_id, received_at) VALUES (?, ?)", + client_id, + received_at + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid()) + } + + pub(crate) async fn set_rejected_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE received_ticket SET rejected = true WHERE id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_ticket_data( + &self, + ticket_id: i64, + serial_number: &[u8], + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO ticket_data(ticket_id, serial_number, data) VALUES (?, ?, ?)", + ticket_id, + serial_number, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_ticket_data(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM ticket_data WHERE ticket_id = ?", ticket_id) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn has_ticket_data(&self, serial_number: &[u8]) -> Result { + sqlx::query!( + "SELECT EXISTS (SELECT 1 FROM ticket_data WHERE serial_number = ?) AS 'exists'", + serial_number + ) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn remove_binary_ticket_data( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE ticket_data SET data = NULL WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_redeemed_tickets_data( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM ticket_data + WHERE ticket_id IN ( + SELECT ticket_id + FROM verified_tickets + WHERE proposal_id = ? + ) + "#, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO ticket_verification (ticket_id, signer_id, verified_at, accepted) + VALUES (?, ?, ?, ?) + "#, + ticket_id, + signer_id, + verified_at, + accepted + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn remove_ticket_verification( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_verified_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO verified_tickets (ticket_id) VALUES (?)", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Attempts to retrieve ticket data for all tickets that that are **NOT** present in `verified_tickets` table + pub(crate) async fn get_unverified_tickets( + &self, + ) -> Result, sqlx::Error> { + // force not nullable `data` field as we explicitly ensured this via the query + sqlx::query_as!( + UnverifiedTicketData, + r#" + SELECT t1.ticket_id, t1.data as "data!" + FROM ticket_data as t1 + LEFT JOIN verified_tickets as t2 + ON t1.ticket_id = t2.ticket_id + WHERE + t2.ticket_id IS NULL + AND + t1.data IS NOT NULL + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_epoch_signers(&self, epoch_id: i64) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ecash_signer WHERE epoch_id = ?", + epoch_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_verification_votes( + &self, + ticket_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + JOIN received_ticket as t3 + ON t1.ticket_id = t3.id + + ORDER BY t3.received_at ASC + LIMIT 65535 + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + WHERE t1.proposal_id = ? + "#, + proposal_id + ) + .fetch_all(&self.connection_pool) + .await + } + + /// for each ticket in `verified_tickets` where the `ticket_id` is present in the provided iterator, + /// set the associated `proposal_id` to the provided value. + pub(crate) async fn insert_verified_tickets_proposal_id( + &self, + ticket_ids: I, + proposal_id: i64, + ) -> Result<(), sqlx::Error> + where + I: Iterator, + { + // UPDATE verified_tickets SET proposal_id = ... WHERE ticket_id IN (1,2,3,...) + let mut query_builder = + sqlx::QueryBuilder::new("UPDATE verified_tickets SET proposal_id = "); + query_builder + .push_bind(proposal_id) + .push("WHERE ticket_id IN ("); + + let mut separated = query_builder.separated(", "); + for ticket_id in ticket_ids { + separated.push_bind(ticket_id); + } + separated.push_unseparated(") "); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn remove_verified_tickets( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE proposal_id = ?", + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_redemption_proposal( + &self, + proposal_id: i64, + created_at: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO redemption_proposals (proposal_id, created_at) VALUES (?, ?)", + proposal_id, + created_at + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn update_redemption_proposal( + &self, + proposal_id: i64, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE redemption_proposals SET resolved_at = ?, rejected = ? WHERE proposal_id = ?", + resolved_at, + rejected, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_latest_redemption_proposal( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT proposal_id, created_at + FROM redemption_proposals + ORDER BY created_at DESC + LIMIT 1 + "#, + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_unresolved_redemption_proposal_ids( + &self, + ) -> Result, sqlx::Error> { + sqlx::query!("SELECT proposal_id FROM redemption_proposals WHERE resolved_at IS NULL") + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.proposal_id).collect()) + } +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index a3a22e8154..d36551edd4 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -19,6 +19,8 @@ rust-version = "1.76.0" async-trait = { workspace = true } bs58 = { workspace = true } bip39 = { workspace = true } +bincode.workspace = true +bloomfilter = "1.0.13" cfg-if = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" @@ -52,6 +54,7 @@ sqlx = { workspace = true, features = [ "sqlite", "macros", "migrate", + "time", ] } okapi = { workspace = true, features = ["impl_json_schema"] } @@ -70,12 +73,16 @@ zeroize = { workspace = true } ## internal #ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } -nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } +nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } +nym-credentials-interface = { path = "../common/credentials-interface" } #nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } -nym-credential-storage = { path = "../common/credential-storage" } +nym-credential-storage = { path = "../common/credential-storage", features = ["persistent-storage"] } nym-credentials = { path = "../common/credentials" } nym-crypto = { path = "../common/crypto" } cw2 = { workspace = true } @@ -93,7 +100,7 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["request-parsing"] } +nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } diff --git a/nym-api/migrations/20240708120000_ecash_tables.sql b/nym-api/migrations/20240708120000_ecash_tables.sql new file mode 100644 index 0000000000..68037fe83d --- /dev/null +++ b/nym-api/migrations/20240708120000_ecash_tables.sql @@ -0,0 +1,101 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE bloomfilter_parameters ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + num_hashes INTEGER NOT NULL, + bitmap_size INTEGER NOT NULL, + + sip0_key0 BLOB NOT NULL, + sip0_key1 BLOB NOT NULL, + + sip1_key0 BLOB NOT NULL, + sip1_key1 BLOB NOT NULL +); + +-- table containing partial bloomfilters produced from tickets spent on particular date +-- the 'current' bloomfilter is always OR(last_30) +CREATE TABLE partial_bloomfilter ( + date DATE NOT NULL UNIQUE PRIMARY KEY, + parameters INTEGER NOT NULL REFERENCES bloomfilter_parameters(id), + bitmap BLOB NOT NULL +); + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialised_key BLOB NOT NULL +); + +CREATE TABLE global_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + -- combined signatures for all indices + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE global_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + serialised_signatures BLOB NOT NULL +); + + +DROP TABLE issued_credential; + +-- particular **partial** ecash credential issued in this epoch +CREATE TABLE issued_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL UNIQUE, +-- at some point those should be blobified + bs58_partial_credential VARCHAR NOT NULL, + bs58_signature VARCHAR NOT NULL, + joined_private_commitments VARCHAR NOT NULL, + expiration_date DATE NOT NULL +); + +CREATE TABLE ticket_providers +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + gateway_address TEXT NOT NULL UNIQUE, + last_batch_verification TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE verified_tickets +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ticket_data BLOB NOT NULL, + serial_number BLOB NOT NULL UNIQUE, + spending_date DATE NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + gateway_id INTEGER NOT NULL REFERENCES ticket_providers(id) +); + +-- helper index for getting tickets verified by particular gateway +CREATE INDEX verified_tickets_index ON verified_tickets (gateway_id, verified_at desc); + +-- helper index for getting all tickets with particular spending date for rebuilding the bloomfilters +CREATE INDEX verified_tickets_spending_index ON verified_tickets (spending_date); \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 7ed5d2ad18..d908694c5d 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -16,14 +16,20 @@ serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } tendermint = { workspace = true } time = { workspace = true, features = ["serde", "parsing", "formatting"] } +thiserror.workspace = true rocket = { workspace = true, optional = true } +sha2 = "0.10.8" + # for serde on secp256k1 signatures ecdsa = { workspace = true, features = ["serde"] } +serde-helpers = { path = "../../common/serde-helpers", features = ["bs58", "base64"] } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"] } +nym-ecash-time = { path = "../../common/ecash-time" } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } @@ -33,5 +39,5 @@ serde_json.workspace = true [features] default = [] -request-parsing = ["rocket"] +rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs deleted file mode 100644 index 50df750a82..0000000000 --- a/nym-api/nym-api-requests/src/coconut/models.rs +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::helpers::issued_credential_plaintext; -use cosmrs::AccountId; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError, - CredentialSpendingData, VerificationKey, -}; -use nym_crypto::asymmetric::identity; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use tendermint::hash::Hash; - -#[derive(Serialize, Deserialize)] -pub struct VerifyCredentialBody { - /// The cryptographic material required for spending the underlying credential. - pub credential_data: CredentialSpendingData, - - /// Multisig proposal for releasing funds for the provided bandwidth credential - pub proposal_id: u64, - - /// Cosmos address of the spender of the credential - pub gateway_cosmos_addr: AccountId, -} - -impl VerifyCredentialBody { - pub fn new( - credential_data: CredentialSpendingData, - proposal_id: u64, - gateway_cosmos_addr: AccountId, - ) -> VerifyCredentialBody { - VerifyCredentialBody { - credential_data, - proposal_id, - gateway_cosmos_addr, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VerifyCredentialResponse { - pub verification_result: bool, -} - -impl VerifyCredentialResponse { - pub fn new(verification_result: bool) -> Self { - VerifyCredentialResponse { - verification_result, - } - } -} - -// All strings are base58 encoded representations of structs -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct BlindSignRequestBody { - pub inner_sign_request: BlindSignRequest, - - /// Hash of the deposit transaction - pub tx_hash: Hash, - - /// Signature on the inner sign request and the tx hash - pub signature: identity::Signature, - - pub public_attributes_plain: Vec, -} - -impl BlindSignRequestBody { - pub fn new( - inner_sign_request: BlindSignRequest, - tx_hash: Hash, - signature: identity::Signature, - public_attributes_plain: Vec, - ) -> BlindSignRequestBody { - BlindSignRequestBody { - inner_sign_request, - tx_hash, - signature, - public_attributes_plain, - } - } - - pub fn attributes(&self) -> u32 { - (self.public_attributes_plain.len() + self.inner_sign_request.num_private_attributes()) - as u32 - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } - - pub fn encode_commitments(&self) -> Vec { - use nym_credentials_interface::Base58; - - self.inner_sign_request - .get_private_attributes_pedersen_commitments() - .iter() - .map(|c| c.to_bs58()) - .collect() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassNonceResponse { - pub current_nonce: [u8; 16], -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BlindedSignatureResponse { - pub blinded_signature: BlindedSignature, -} - -impl BlindedSignatureResponse { - pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { - BlindedSignatureResponse { blinded_signature } - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string>(val: I) -> Result { - let bytes = bs58::decode(val).into_vec()?; - Self::from_bytes(&bytes) - } - - pub fn to_bytes(&self) -> Vec { - self.blinded_signature.to_byte_vec() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::from_bytes(bytes)?, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassRequest { - // secp256k1 key associated with the admin account - pub cosmos_pubkey: cosmrs::crypto::PublicKey, - - pub inner_sign_request: BlindSignRequest, - - // we need to include a nonce here to prevent replay attacks - // (and not making the nym-api store the serial numbers of all issued credential) - pub used_nonce: [u8; 16], - - /// Signature on the nonce - /// to prove the possession of the cosmos key/address - pub nonce_signature: cosmrs::crypto::secp256k1::Signature, - - pub public_attributes_plain: Vec, -} - -impl FreePassRequest { - pub fn new( - cosmos_pubkey: cosmrs::crypto::PublicKey, - inner_sign_request: BlindSignRequest, - used_nonce: [u8; 16], - nonce_signature: cosmrs::crypto::secp256k1::Signature, - public_attributes_plain: Vec, - ) -> Self { - FreePassRequest { - cosmos_pubkey, - inner_sign_request, - used_nonce, - nonce_signature, - public_attributes_plain, - } - } - - pub fn tendermint_pubkey(&self) -> tendermint::PublicKey { - self.cosmos_pubkey.into() - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } -} - -#[derive(Serialize, Deserialize)] -pub struct VerificationKeyResponse { - pub key: VerificationKey, -} - -impl VerificationKeyResponse { - pub fn new(key: VerificationKey) -> VerificationKeyResponse { - VerificationKeyResponse { key } - } -} - -#[derive(Serialize, Deserialize)] -pub struct CosmosAddressResponse { - pub addr: AccountId, -} - -impl CosmosAddressResponse { - pub fn new(addr: AccountId) -> CosmosAddressResponse { - CosmosAddressResponse { addr } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Pagination { - /// last_key is the last value returned in the previous query. - /// it's used to indicate the start of the next (this) page. - /// the value itself is not included in the response. - pub last_key: Option, - - /// limit is the total number of results to be returned in the result page. - /// If left empty it will default to a value to be set by each app. - pub limit: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CredentialsRequestBody { - /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. - pub credential_ids: Vec, - - /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. - pub pagination: Option>, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct EpochCredentialsResponse { - pub epoch_id: u64, - pub first_epoch_credential_id: Option, - pub total_issued: u32, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialsResponse { - // note: BTreeMap returns ordered results so it's fine to use it with pagination - pub credentials: BTreeMap, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialResponse { - pub credential: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialBody { - pub credential: IssuedCredential, - - pub signature: identity::Signature, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: Hash, - - // NOTE: if we find creation of this guy takes too long, - // change `BlindedSignature` to `BlindedSignatureBytes` - // so that nym-api wouldn't need to parse the value out of its storage - pub blinded_partial_credential: BlindedSignature, - pub bs58_encoded_private_attributes_commitments: Vec, - pub public_attributes: Vec, -} - -impl IssuedCredential { - // this method doesn't have to be reversible so just naively concatenate everything - pub fn signable_plaintext(&self) -> Vec { - issued_credential_plaintext( - self.epoch_id, - self.tx_hash, - &self.blinded_partial_credential, - &self.bs58_encoded_private_attributes_commitments, - &self.public_attributes, - ) - } -} diff --git a/nym-api/nym-api-requests/src/constants.rs b/nym-api/nym-api-requests/src/constants.rs new file mode 100644 index 0000000000..f3bee4522c --- /dev/null +++ b/nym-api/nym-api-requests/src/constants.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::Duration; + +// we should probably monitor this constant and adjust it when/if required +pub const MIN_BATCH_REDEMPTION_DELAY: Duration = Duration::DAY; diff --git a/nym-api/nym-api-requests/src/coconut/helpers.rs b/nym-api/nym-api-requests/src/ecash/helpers.rs similarity index 69% rename from nym-api/nym-api-requests/src/coconut/helpers.rs rename to nym-api/nym-api-requests/src/ecash/helpers.rs index 73d1a7f42f..154b733f9f 100644 --- a/nym-api/nym-api-requests/src/coconut/helpers.rs +++ b/nym-api/nym-api-requests/src/ecash/helpers.rs @@ -1,33 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::BlindedSignature; -use tendermint::hash::Hash; +use nym_compact_ecash::BlindedSignature; +use nym_ecash_time::EcashTime; +use time::Date; // recomputes plaintext on the credential nym-api has used for signing // // note: this method doesn't have to be reversible so just naively concatenate everything pub fn issued_credential_plaintext( epoch_id: u32, - tx_hash: Hash, + deposit_id: u32, blinded_partial_credential: &BlindedSignature, bs58_encoded_private_attributes_commitments: &[String], - public_attributes: &[String], + expiration_date: Date, ) -> Vec { epoch_id .to_be_bytes() .into_iter() - .chain(tx_hash.as_bytes().iter().copied()) + .chain(deposit_id.to_be_bytes()) .chain(blinded_partial_credential.to_bytes()) .chain( bs58_encoded_private_attributes_commitments .iter() .flat_map(|attr| attr.as_bytes().iter().copied()), ) - .chain( - public_attributes - .iter() - .flat_map(|attr| attr.as_bytes().iter().copied()), - ) + .chain(expiration_date.ecash_unix_timestamp().to_be_bytes()) .collect() } diff --git a/nym-api/nym-api-requests/src/coconut/mod.rs b/nym-api/nym-api-requests/src/ecash/mod.rs similarity index 59% rename from nym-api/nym-api-requests/src/coconut/mod.rs rename to nym-api/nym-api-requests/src/ecash/mod.rs index 1971f0b8f8..60f3c1c41c 100644 --- a/nym-api/nym-api-requests/src/coconut/mod.rs +++ b/nym-api/nym-api-requests/src/ecash/mod.rs @@ -5,6 +5,7 @@ pub mod helpers; pub mod models; pub use models::{ - BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest, - VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerificationKeyResponse, VerifyEcashCredentialBody, }; diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs new file mode 100644 index 0000000000..b22fcd7e1d --- /dev/null +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -0,0 +1,447 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::helpers::issued_credential_plaintext; +use crate::helpers::PlaceholderJsonSchemaImpl; +use cosmrs::AccountId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_credentials_interface::{ + BlindedSignature, CompactEcashError, CredentialSpendingData, PublicKeyUser, + VerificationKeyAuth, WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sha2::Digest; +use std::collections::BTreeMap; +use std::ops::Deref; +use thiserror::Error; +use time::Date; + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashTicketBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashCredentialBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, + + /// Multisig proposal for releasing funds for the provided bandwidth credential + pub proposal_id: u64, +} + +impl VerifyEcashCredentialBody { + pub fn new( + credential: CredentialSpendingData, + gateway_cosmos_addr: AccountId, + proposal_id: u64, + ) -> VerifyEcashCredentialBody { + VerifyEcashCredentialBody { + credential, + gateway_cosmos_addr, + proposal_id, + } + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashTicketVerificationResponse { + pub verified: Result<(), EcashTicketVerificationRejection>, +} + +impl EcashTicketVerificationResponse { + pub fn reject(reason: EcashTicketVerificationRejection) -> Self { + EcashTicketVerificationResponse { + verified: Err(reason), + } + } +} + +#[derive(Debug, Error, Serialize, Deserialize, JsonSchema)] +pub enum EcashTicketVerificationRejection { + #[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC")] + InvalidSpentDate { + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + today: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + yesterday: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + received: Date, + }, + + #[error("this ticket has already been received before")] + ReplayedTicket, + + #[error("this ticket has already been spent before")] + DoubleSpend, + + #[error("failed to verify the provided ticket")] + InvalidTicket, +} + +// All strings are base58 encoded representations of structs +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +pub struct BlindSignRequestBody { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub inner_sign_request: WithdrawalRequest, + + /// the id of the associated deposit + pub deposit_id: u32, + + /// Signature on the inner sign request and the tx hash + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub ecash_pubkey: PublicKeyUser, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, +} + +impl BlindSignRequestBody { + pub fn new( + inner_sign_request: WithdrawalRequest, + deposit_id: u32, + signature: identity::Signature, + ecash_pubkey: PublicKeyUser, + expiration_date: Date, + ) -> BlindSignRequestBody { + BlindSignRequestBody { + inner_sign_request, + deposit_id, + signature, + ecash_pubkey, + expiration_date, + } + } + + pub fn encode_commitments(&self) -> Vec { + use nym_compact_ecash::Base58; + + self.inner_sign_request + .get_private_attributes_commitments() + .iter() + .map(|c| c.to_bs58()) + .collect() + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BlindedSignatureResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_signature: BlindedSignature, +} + +impl BlindedSignatureResponse { + pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { + BlindedSignatureResponse { blinded_signature } + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.blinded_signature.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::from_bytes(bytes)?, + }) + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct MasterVerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl MasterVerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> MasterVerificationKeyResponse { + MasterVerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct VerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl VerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> VerificationKeyResponse { + VerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CosmosAddressResponse { + #[schemars(with = "String")] + pub addr: AccountId, +} + +impl CosmosAddressResponse { + pub fn new(addr: AccountId) -> CosmosAddressResponse { + CosmosAddressResponse { addr } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +pub struct Pagination { + /// last_key is the last value returned in the previous query. + /// it's used to indicate the start of the next (this) page. + /// the value itself is not included in the response. + pub last_key: Option, + + /// limit is the total number of results to be returned in the result page. + /// If left empty it will default to a value to be set by each app. + pub limit: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CredentialsRequestBody { + /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. + pub credential_ids: Vec, + + /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. + pub pagination: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct SerialNumberWrapper( + #[serde(with = "serde_helpers::bs58")] + #[schemars(with = "String")] + Vec, +); + +impl Deref for SerialNumberWrapper { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for SerialNumberWrapper { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From> for SerialNumberWrapper { + fn from(value: Vec) -> Self { + SerialNumberWrapper(value) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct BatchRedeemTicketsBody { + #[serde(with = "serde_helpers::bs58")] + #[schemars(with = "String")] + pub digest: Vec, + pub included_serial_numbers: Vec, + pub proposal_id: u64, + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +impl BatchRedeemTicketsBody { + pub fn make_digest(serial_numbers: I) -> Vec + where + I: Iterator, + T: AsRef<[u8]>, + { + let mut hasher = sha2::Sha256::new(); + for sn in serial_numbers { + hasher.update(sn) + } + hasher.finalize().to_vec() + } + + pub fn new( + digest: Vec, + proposal_id: u64, + serial_numbers: Vec>, + redeemer: AccountId, + ) -> Self { + BatchRedeemTicketsBody { + digest, + included_serial_numbers: serial_numbers.into_iter().map(Into::into).collect(), + proposal_id, + gateway_cosmos_addr: redeemer, + } + } + + pub fn verify_digest(&self) -> bool { + Self::make_digest(self.included_serial_numbers.iter()) == self.digest + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashBatchTicketRedemptionResponse { + pub proposal_accepted: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SpentCredentialsResponse { + #[serde(with = "serde_helpers::base64")] + #[schemars(with = "String")] + pub bitmap: Vec, +} + +impl SpentCredentialsResponse { + pub fn new(bitmap: Vec) -> SpentCredentialsResponse { + SpentCredentialsResponse { bitmap } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EpochCredentialsResponse { + pub epoch_id: u64, + pub first_epoch_credential_id: Option, + pub total_issued: u32, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialsResponse { + // note: BTreeMap returns ordered results so it's fine to use it with pagination + pub credentials: BTreeMap, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialResponse { + pub credential: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialBody { + pub credential: IssuedCredential, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: u32, + + // NOTE: if we find creation of this guy takes too long, + // change `BlindedSignature` to `BlindedSignatureBytes` + // so that nym-api wouldn't need to parse the value out of its storage + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_partial_credential: BlindedSignature, + pub bs58_encoded_private_attributes_commitments: Vec, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, +} + +impl IssuedCredential { + // this method doesn't have to be reversible so just naively concatenate everything + pub fn signable_plaintext(&self) -> Vec { + issued_credential_plaintext( + self.epoch_id, + self.deposit_id, + &self.blinded_partial_credential, + &self.bs58_encoded_private_attributes_commitments, + self.expiration_date, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // had some issues with `Date` and serde... + // so might as well leave this unit test in case we do something to the helper + #[test] + fn aggregated_expiration_date_signature_responses_deserialises_correctly() { + let raw = r#"{"epoch_id":0,"expiration_date":"2024-08-03","signatures":[{"signature":{"h":"9379384af5236bffd7d6c533782d74863f892cf6e0fc8b4b64647e8bd5bccfe63df639a4a556d21826ab7ef54e4adafe","s":"83124c907935eba10c791cbc2cedf8714b68c6a266efead7bc4897c3b9652680bd151493689577ac790b9ccef1112f9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720137600},{"signature":{"h":"817b9fa0eef617ce751963d9c853df56fdf38a643b9ef8649b658d7ec3da61d49d9279f22509c66dd07051132a418c62","s":"af9fd22c2fde4cb36a0ee021c5a756f97f871ea404d639af1be845b277bed583f715d228d3556e79d15eba23a5b0d5e0"},"expiration_timestamp":1722643200,"spending_timestamp":1720224000},{"signature":{"h":"89f04f5ae1a1532551c2e17166775eac3c5ebba0198eff2919073efd733d6e17e0a6496e31061b0797f51b69d0fa17fc","s":"b752cdb2f4d8a4254d24d5773a8490de1ad84207cbe693638d1fdcbffd8426be96fae3586f19c570beb005bb6bf27b9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720310400},{"signature":{"h":"b1072da2612a6ebc0c02c7f673898dfea88eeeeb076a9dd2ab981d47a8176b87804ca6090cfb5b0c128f06adc6fdf0de","s":"8746a62f6333c3948f1fbb05543f0095cd44f610926c71f339511bf3bb97d98fd019d44c4a4dfc6710a9d5c718c5bfc2"},"expiration_timestamp":1722643200,"spending_timestamp":1720396800},{"signature":{"h":"8d266e7e56af7364d79c4190b24d5f0a601d7771c359815f642115123f5418d0304a46226d70e873b10a051f15178630","s":"a11d67c3293b224abb1256050031b32236484dc0f0fe66a9363d322e6b8c9b24719f4d4da0584d6106eb0f59fa6bb3c8"},"expiration_timestamp":1722643200,"spending_timestamp":1720483200},{"signature":{"h":"8ee31ef821949be316528b7b75b81baa6d0cf2846b615ff6d34e478f25746a07287ae6944279550879735740803c7922","s":"aa26b0df4445d8a10a226f1c70908451d211f787458c0743ba891c46bbbbde3bc59777f4edb9cabdd5fd181d772cfa43"},"expiration_timestamp":1722643200,"spending_timestamp":1720569600},{"signature":{"h":"b8fbacde3f9d507fb5bf6d5f96a33dd5c9c40cc9a062e92f9cdc6a2bddf8cbd873ffcc5112de816f98fbf47b74330abb","s":"a791aee82bdcf3cc4203ce657fbd2c3e9c2f0decd032c4ab91713d3cfd0ac57ebb9bb20e133d9a8a4ba1b8692a3b2706"},"expiration_timestamp":1722643200,"spending_timestamp":1720656000},{"signature":{"h":"b9ed48b47c1a1e6a1ae847ffaad01e0b7d22b2c9f9075beedf4924958ca044579f1e9ca18e268e2a0ef743c214934692","s":"a3e9191d0c3651c656a376e99f3d8c121a742ba55522765479ce866df8aaecc0281ae08003439debf7977f23e450a45f"},"expiration_timestamp":1722643200,"spending_timestamp":1720742400},{"signature":{"h":"a6f1fbdcc28953f318f067cde6e3d6ad0362fd46a501a74492be0884308cf306a7f30006181710f8883a380a8d4885f7","s":"9260d035ea97fc1b93d2db3802678005c95a4acdb0ce0c947cb0b9866ba4028a3b833d742f02ed0227ed33cee4ed17b4"},"expiration_timestamp":1722643200,"spending_timestamp":1720828800},{"signature":{"h":"95109fd35f7808084f224aa1fde645728c0c163751fe24d97e0002a7c38dc5a3a592dcb770893ddd25fc67f84e29e0a1","s":"b85279c4566aa365d208b57817ce96f1a1245895fbe25363202da89559fc454a2cfb09972fc3d1c468b96e8bc11b424c"},"expiration_timestamp":1722643200,"spending_timestamp":1720915200},{"signature":{"h":"8f4b7f93871d8f995a248d31664b02e1fe7107d67512f248a8f550d477846ca9f9862a5d44f5da458c6b83f4d8e62494","s":"b87e46a5d25de574eb4e16cf24199b2e0a658e0c1ef965b632165ed2d695637df76d373484dc93cec30e2f0a29ac91f8"},"expiration_timestamp":1722643200,"spending_timestamp":1721001600},{"signature":{"h":"b740491d3527f8fa0b8bed76d234d0737c09a049f653bb1e8a552b29f568aabd077604b08e7337d44f551fbda565e52e","s":"a08634a2560b3a79c8b56ff5387ef76c397c277810d78cc6dfac2161673df761d0ba61e6075e8a4781ec1b2ba7715b93"},"expiration_timestamp":1722643200,"spending_timestamp":1721088000},{"signature":{"h":"8d4b92fe48257d2574223fbe12afcdabd00db976e8d20faaddb5fb570373edb2a15af4ad9a7c10ceda3bb1304b0210aa","s":"8f8592c45a1cca753e5d0b04ffc2947fd27c7716f2751e632f2bc1bf0e73b179ce8080325951beb0886c5cf0fca79372"},"expiration_timestamp":1722643200,"spending_timestamp":1721174400},{"signature":{"h":"891d412e631041d07115cacf847575462d2389402a0e243a2f106b5e1c8f1431fa8e1aedc55cf943aef9fa8125d105a1","s":"ad2038ab95b823a8119bcc473be22101c4b58fd44ad375235c3664341fc617fe129702aec8765d8dc32ac845aec58a6a"},"expiration_timestamp":1722643200,"spending_timestamp":1721260800},{"signature":{"h":"84b97558e7889af167ba01ed608418ddabfc7fa71ba8f9cb045027f994fe9b5f9bb0ccc15822d1c73a37a48b041cd759","s":"adc370bc182ff9c7057591a3d4afcb21c714093e480374a07608c9f9d2baeff8c14fc5de3f0f457ea340e445ecc28487"},"expiration_timestamp":1722643200,"spending_timestamp":1721347200},{"signature":{"h":"b29b245acf66082c0f4e52cbf9db00ecb9cee070ddb1f5b56a9e85e2563dae04186ed03b7b590e0a9f4b4304ae1cd370","s":"8196445495ed1ee098bbe6083ece3522d4669e23dc4f3e2226e0e63294f1bc791e66380deedd21e5679e3019e93c7727"},"expiration_timestamp":1722643200,"spending_timestamp":1721433600},{"signature":{"h":"9750b9bdf1e86e8e4e6de44392f2160059da64838357a16e64155a5b7989fe7958ae95501e7c1f4039cf8b372ac3b214","s":"80707aa94af054fedcb4947809dab7a9317e303a12b7fb30858115c95bf8e46fec7ea9b0ce5097b75d8f7905c205f757"},"expiration_timestamp":1722643200,"spending_timestamp":1721520000},{"signature":{"h":"828be64b5cd147b5c43e68a2084cb77a762967d5621769136a0894c3d1ee50890412120d7c765cb3c835435d57ac4438","s":"956ca67f8f8591e72b8feeb53cbe52a0f69202313aa729c09760fb2a19b346fda1b7afd326822889fb228ca77a503072"},"expiration_timestamp":1722643200,"spending_timestamp":1721606400},{"signature":{"h":"83ac2c778f0b847af8a4bcc37bd5c2734544fe4427ee7ec5c4690738005cc805864fd5944266417ba2795d7333bd8c4a","s":"88936b7f61a0febfbedc8c0c40dad8f9c4eacb41d276ba00b7d03602ca69614faacca0d945713a272e9748dd6dd71f81"},"expiration_timestamp":1722643200,"spending_timestamp":1721692800},{"signature":{"h":"911c126da663478023efd1157586681937ac7f6c5a33b34a102b3611a63780b244ecdf05ecad09701e65ad20c5246f6a","s":"a1188f3b6ced776890b038e2d07b6af67f1a2c08f54a32d48dbfce61be3295e8ea910b6197b111a6ff16dd668eeda9f5"},"expiration_timestamp":1722643200,"spending_timestamp":1721779200},{"signature":{"h":"8c03c749b424e8fb3f26d51e45d21a7e9f59e6836f89f4a1f27b952e2bc8c038f3372f593dfb3bd284feb36918c2f194","s":"b20d3fd194f73142f0dfc68981519b76aa013e6743b2bdf63d41f3aa1e754d1f74c46012adf0332723f861e5aaa69dba"},"expiration_timestamp":1722643200,"spending_timestamp":1721865600},{"signature":{"h":"872de60a19f3ece61a70519b1f7a63476d13a20721ff00ae923a984bd710703af55112092e209970f074a8950b9e246e","s":"854906bbce5a0c2e47fa829441a03f5c3ac9357dc6d107547198f17505cd17852806e6d69b78264921a8c13c4a9fe50c"},"expiration_timestamp":1722643200,"spending_timestamp":1721952000},{"signature":{"h":"a2a57fd2b675fccccae34f9ff465f36638ee3809291c9b37b5830015a1a7c4d873528105f030a05efd16c6411672fe88","s":"b42f31e4ed05d3b026a62c5df97e04568c0c92d3fafaf95fa732ce69fbe7ee091ac025d04e3ce526b3474edaaf278398"},"expiration_timestamp":1722643200,"spending_timestamp":1722038400},{"signature":{"h":"b9be127e812602ac7a612897fd95e5869b360046993139590c82c2844654b6a5a72e9d74ed38eea6ee99ba0978382a3b","s":"b027db49bb9b0ec3931fc1cb4a6a739c968c45a59698d199056ebfcbeca7ddb363a123e55d05fbf5635d915be2a16ffb"},"expiration_timestamp":1722643200,"spending_timestamp":1722124800},{"signature":{"h":"8c40b2a5e17e1f9cdbe85eaaa419126374d35a255b8338474d442d0d40a4e10c3f2668f579155dfdad3fd403523683df","s":"a40cec4b00c9a975db6c307f25425f3a4d0e49eca2c05d5122630c54bd5d114f74e4abb78a7261752a9dfd4c020f8b91"},"expiration_timestamp":1722643200,"spending_timestamp":1722211200},{"signature":{"h":"b72821bc38167e7f422dcc669c19e0aeffb8247192c4bd50da1503b6aa28041bb5605598639bdb8b945dbc804fc852aa","s":"9443cad7c9d6194b601daf21cbc5fe3019d3350dd725f6d9e3ae366162f93e3b738779617f77ec146f0a05f9a40b602b"},"expiration_timestamp":1722643200,"spending_timestamp":1722297600},{"signature":{"h":"8fe36994ac7e92b5ad0869f2b1bfa3247aac27599459cce9d94cf84eeead8bba1ca4b294d16db3588a0eb9d9b28d4051","s":"8eae0ef0e966f9f473bd829f18e28cf9730447452040aaef114701f409ff237965f203f7394a7d61ca745a4b2f058d9b"},"expiration_timestamp":1722643200,"spending_timestamp":1722384000},{"signature":{"h":"a9f91a83a1db0c5609fcacd1290f1650e4e5ab98c3dd9f0b8fa1c54664c12ed17478ae6e598f0e900ace30addf1b0559","s":"801658058f8de9ba1c25518905196d7e4e0bc5e26a7a810c9dce3ac141f01c538c62808b4728ff683dee78670b2846f9"},"expiration_timestamp":1722643200,"spending_timestamp":1722470400},{"signature":{"h":"94ecd95c1d5ae03270079476aad6334dafdcf615157a6a34b05bb755a047bc2515e33a7e099f9fddc4eec3def8904cdf","s":"b59ff6ded76926a9ae97447ba00eac53d9aa0e1168218d990c5b97cc128a4f5a0c42f961f84cad8ad344630b2a15f3a9"},"expiration_timestamp":1722643200,"spending_timestamp":1722556800},{"signature":{"h":"ace778e70775d349c1e679e5ccef634a533738e7af8b51daa45ccbd920f976d15c4ba744d6ae8fdf711b84b20703cef6","s":"80581faceda2bc35b81240017e1ce8f121476c5ba4d75acddfc58ba6aaa2f12d626eaaab185548e6dd716b6e582740b8"},"expiration_timestamp":1722643200,"spending_timestamp":1722643200}]}"#; + let _: AggregatedExpirationDateSignatureResponse = serde_json::from_str(raw).unwrap(); + } + + #[test] + fn batch_redemption_body_roundtrip() { + let sn = vec![b"foomp".to_vec(), b"bar".to_vec()]; + let gateway: AccountId = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf".parse().unwrap(); + let digest = [42u8; 32].to_vec(); + + let req = BatchRedeemTicketsBody::new(digest, 69, sn, gateway); + let bytes = serde_json::to_vec(&req).unwrap(); + + let de: BatchRedeemTicketsBody = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(req, de); + } +} diff --git a/nym-api/nym-api-requests/src/helpers.rs b/nym-api/nym-api-requests/src/helpers.rs new file mode 100644 index 0000000000..dc2a28c1f5 --- /dev/null +++ b/nym-api/nym-api-requests/src/helpers.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use time::format_description::{modifier, BorrowedFormatItem, Component}; +use time::OffsetDateTime; + +// just to have something, even if not accurate to generate the swagger docs +#[derive(JsonSchema)] +pub struct PlaceholderJsonSchemaImpl {} + +const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), +]; + +pub(crate) const fn unix_epoch() -> OffsetDateTime { + OffsetDateTime::UNIX_EPOCH +} + +pub(crate) mod overengineered_offset_date_time_serde { + use crate::helpers::{unix_epoch, DATE_FORMAT}; + use serde::de::Visitor; + use serde::ser::Error; + use serde::{Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + use time::format_description::well_known::Rfc3339; + use time::format_description::{modifier, BorrowedFormatItem, Component}; + use time::OffsetDateTime; + + struct OffsetDateTimeVisitor; + + // copied from time library because they keep it private -.- + const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Compound(DATE_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(TIME_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), + ]; + + const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), + BorrowedFormatItem::Literal(b"."), + BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), + ]; + + const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::OffsetHour({ + let mut m = modifier::OffsetHour::default(); + m.sign_is_mandatory = true; + m + })), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetMinute( + modifier::OffsetMinute::default(), + )), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetSecond( + modifier::OffsetSecond::default(), + )), + ])), + ])), + ]; + + impl<'de> Visitor<'de> for OffsetDateTimeVisitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + // first try rfc3339, if that fails use default human-readable impl from time, + // finally fallback to default unix epoch + Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { + OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) + .unwrap_or_else(|_| unix_epoch()) + })) + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OffsetDateTimeVisitor) + } + + pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +pub(crate) mod date_serde { + use crate::helpers::DATE_FORMAT; + use serde::ser::Error; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use time::Date; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + Date::parse(&s, DATE_FORMAT).map_err(de::Error::custom) + } + + pub(crate) fn serialize(datetime: &Date, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DATE_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 5074e16146..7ea5db1f6d 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -4,7 +4,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -pub mod coconut; +pub mod constants; +pub mod ecash; +mod helpers; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 05cf9f702e..8245d26d70 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::helpers::unix_epoch; use crate::nym_nodes::NodeRole; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal}; @@ -16,7 +17,7 @@ use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; @@ -41,7 +42,7 @@ impl RequestError { impl Display for RequestError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.message.fmt(f) + Display::fmt(&self.message, f) } } @@ -417,10 +418,6 @@ impl From for WebSocket } } -const fn unix_epoch() -> OffsetDateTime { - OffsetDateTime::UNIX_EPOCH -} - pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -428,110 +425,14 @@ where Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) } -pub(crate) mod overengineered_offset_date_time_serde { - use crate::models::unix_epoch; - use serde::de::Visitor; - use serde::ser::Error; - use serde::{Deserializer, Serialize, Serializer}; - use std::fmt::Formatter; - use time::format_description::well_known::Rfc3339; - use time::format_description::{modifier, BorrowedFormatItem, Component}; - use time::OffsetDateTime; - - struct OffsetDateTimeVisitor; - - // copied from time library because they keep it private -.- - const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Compound(DATE_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(TIME_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), - ]; - - const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), - ]; - - const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), - BorrowedFormatItem::Literal(b"."), - BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), - ]; - - const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::OffsetHour({ - let mut m = modifier::OffsetHour::default(); - m.sign_is_mandatory = true; - m - })), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetMinute( - modifier::OffsetMinute::default(), - )), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetSecond( - modifier::OffsetSecond::default(), - )), - ])), - ])), - ]; - - impl<'de> Visitor<'de> for OffsetDateTimeVisitor { - type Value = OffsetDateTime; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - // first try rfc3339, if that fails use default human-readable impl from time, - // finally fallback to default unix epoch - Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { - OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) - .unwrap_or_else(|_| unix_epoch()) - })) - } - } - - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(OffsetDateTimeVisitor) - } - - pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result - where - S: Serializer, - { - // serialize it with human-readable format for compatibility with eclipse and nutella clients - // in the future change it back to rfc3339 - datetime - .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) - .map_err(S::Error::custom)? - .serialize(serializer) - } -} - // for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct OffsetDateTimeJsonSchemaWrapper( - #[serde(default = "unix_epoch", with = "overengineered_offset_date_time_serde")] - pub OffsetDateTime, + #[serde( + default = "unix_epoch", + with = "crate::helpers::overengineered_offset_date_time_serde" + )] + pub OffsetDateTime, ); impl Default for OffsetDateTimeJsonSchemaWrapper { @@ -540,6 +441,12 @@ impl Default for OffsetDateTimeJsonSchemaWrapper { } } +impl Display for OffsetDateTimeJsonSchemaWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + impl From for OffsetDateTime { fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self { value.0 diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index ea874b90e7..50b527070a 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -17,7 +17,7 @@ pub struct CachedNodesResponse { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "request-parsing", derive(rocket::form::FromFormField))] +#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs deleted file mode 100644 index d56202f067..0000000000 --- a/nym-api/src/coconut/api_routes/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use std::ops::Deref; - -use k256::ecdsa::signature::Verifier; -use rand::rngs::OsRng; -use rand::RngCore; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use time::OffsetDateTime; - -use nym_api_requests::coconut::models::{ - CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest, - IssuedCredentialResponse, IssuedCredentialsResponse, -}; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - funds_from_cosmos_msgs, SpendCredentialStatus, -}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::coconut::bandwidth::{ - bandwidth_credential_params, CredentialType, IssuanceBandwidthCredential, -}; -use nym_validator_client::nyxd::Coin; - -use crate::coconut::api_routes::helpers::build_credentials_response; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::helpers::{accepted_vote_err, blind_sign}; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; - -mod helpers; - -fn validate_freepass_public_attributes(res: &FreePassRequest) -> Result<()> { - let public_attributes = &res.public_attributes_plain; - - if public_attributes.len() != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize { - return Err(CoconutError::InvalidFreePassAttributes { - got: public_attributes.len(), - expected: IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize, - }); - } - - // SAFETY: we just ensured correct number of attributes - let expiry_raw = public_attributes.first().unwrap(); - let type_raw = public_attributes.get(1).unwrap(); - - let parsed_type = type_raw.parse::()?; - if parsed_type != CredentialType::FreePass { - return Err(CoconutError::InvalidFreePassTypeAttribute { got: parsed_type }); - } - - let expiry_timestamp: i64 = expiry_raw - .parse() - .map_err(|source| CoconutError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp).map_err(|source| { - CoconutError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - } - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date > now + MAX_FREE_PASS_VALIDITY { - return Err(CoconutError::TooLongFreePass { expiry_date }); - } - - if expiry_date < now { - return Err(CoconutError::FreePassExpiryInThePast { expiry_date }); - } - - Ok(()) -} - -#[get("/free-pass-nonce")] -pub async fn get_current_free_pass_nonce( - state: &RocketState, -) -> Result> { - debug!("Received free pass nonce request"); - - let current_nonce = state.freepass_nonce.read().await; - debug!("the current expected nonce is {current_nonce:?}"); - - Ok(Json(FreePassNonceResponse { - current_nonce: *current_nonce, - })) -} - -#[post("/free-pass", data = "")] -pub async fn post_free_pass( - freepass_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received free pass sign request"); - trace!("body: {:?}", freepass_request_body); - - validate_freepass_public_attributes(&freepass_request_body)?; - - // check for explicit admin - let explicit_admin = state.get_authorised_freepass_requester().await; - - // otherwise fallback to bandwidth contract admin - let bandwidth_contract_admin = state - .get_bandwidth_contract_admin() - .await - .cloned() - .inspect_err(|_| error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP")) - .ok() - .flatten(); - - // extract account prefix - let prefix = match (&explicit_admin, &bandwidth_contract_admin) { - (None, None) => { - error!("neither explicit admin nor bandwidth contract admin has been set!"); - return Err(CoconutError::MissingBandwidthContractAddress); - } - (Some(addr), _) => addr.prefix(), - (None, Some(addr)) => addr.prefix(), - }; - - // derive the address out of the provided pubkey - let requester = match freepass_request_body.cosmos_pubkey.account_id(prefix) { - Ok(address) => address, - Err(err) => { - return Err(CoconutError::AdminAccountDerivationFailure { - formatted_source: err.to_string(), - }) - } - }; - debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({explicit_admin:?}) and fallback to bandwidth contract admin: {bandwidth_contract_admin:?}"); - - // check if request matches any address - if Some(&requester) != explicit_admin.as_ref() - && Some(&requester) != bandwidth_contract_admin.as_ref() - { - return Err(CoconutError::UnauthorisedFreePassAccount { - requester, - explicit_admin, - bandwidth_contract_admin, - }); - } - - // get the write lock on the nonce to block other requests (since we don't need concurrency and nym is the only one getting them) - let mut current_nonce = state.freepass_nonce.write().await; - debug!("the current expected nonce is {current_nonce:?}"); - - if *current_nonce != freepass_request_body.used_nonce { - return Err(CoconutError::InvalidNonce { - current: *current_nonce, - received: freepass_request_body.used_nonce, - }); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - let tm_pubkey = freepass_request_body.tendermint_pubkey(); - - // currently accounts (excluding validators) don't use ed25519 and are secp256k1-based - let Some(secp256k1_pubkey) = tm_pubkey.secp256k1() else { - return Err(CoconutError::UnsupportedNonSecp256k1Key); - }; - - // make sure the signature actually verifies - secp256k1_pubkey - .verify( - &freepass_request_body.used_nonce, - &freepass_request_body.nonce_signature, - ) - .map_err(|_| CoconutError::FreePassSignatureVerificationFailure)?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = - blind_sign(freepass_request_body.deref(), signing_key.keys.secret_key())?; - - // update the number of issued free passes - state.storage.increment_issued_freepasses().await?; - - // update the nonce - OsRng.fill_bytes(current_nonce.as_mut_slice()); - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/blind-sign", data = "")] -// Until we have serialization and deserialization traits we'll be using a crutch -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received blind sign request"); - trace!("body: {:?}", blind_sign_request_body); - - // early check: does the request have the expected number of public attributes? - debug!("performing basic request validation"); - if blind_sign_request_body.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // check if we already issued a credential for this tx hash - debug!( - "checking if we have already issued credential for this tx_hash (hash: {})", - blind_sign_request_body.tx_hash - ); - if let Some(blinded_signature) = state - .already_issued(blind_sign_request_body.tx_hash) - .await? - { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - // get the transaction details of the claimed deposit - debug!("getting transaction details from the chain"); - let tx = state - .get_transaction(blind_sign_request_body.tx_hash) - .await?; - - // check validity of the request - debug!("fully validating received request"); - state.validate_request(&blind_sign_request_body, tx).await?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = blind_sign( - blind_sign_request_body.deref(), - signing_key.keys.secret_key(), - )?; - - // store the information locally - debug!("storing the issued credential in the database"); - state - .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) - .await?; - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/verify-bandwidth-credential", data = "")] -pub async fn verify_bandwidth_credential( - verify_credential_body: Json, - state: &RocketState, -) -> Result> { - let proposal_id = verify_credential_body.proposal_id; - let credential_data = &verify_credential_body.credential_data; - let epoch_id = credential_data.epoch_id; - let theta = &credential_data.verify_credential_request; - - let voucher_value: u64 = if credential_data.typ.is_voucher() { - credential_data - .get_bandwidth_attribute() - .ok_or(CoconutError::MissingBandwidthValue)? - .parse() - .map_err(|source| CoconutError::VoucherValueParsingFailure { source })? - } else { - return Err(CoconutError::NotABandwidthVoucher { - typ: credential_data.typ, - }); - }; - - // TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS - - let proposal = state.client.get_proposal(proposal_id).await?; - - // Proposal description is the blinded serial number - if !theta.has_blinded_serial_number(&proposal.description)? { - return Err(CoconutError::IncorrectProposal { - reason: String::from("incorrect blinded serial number in description"), - }); - } - let proposed_release_funds = - funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { - reason: String::from("action is not to release funds"), - })?; - // Credential has not been spent before, and is on its way of being spent - let credential_status = state - .client - .get_spent_credential(theta.blinded_serial_number_bs58()) - .await? - .spend_credential - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("Inexistent"), - })? - .status(); - if credential_status != SpendCredentialStatus::InProgress { - return Err(CoconutError::InvalidCredentialStatus { - status: format!("{:?}", credential_status), - }); - } - let verification_key = state.verification_key(epoch_id).await?; - let params = bandwidth_credential_params(); - let mut vote_yes = credential_data.verify(params, &verification_key); - - vote_yes &= Coin::from(proposed_release_funds) - == Coin::new(voucher_value as u128, state.mix_denom.clone()); - - // Vote yes or no on the proposal based on the verification result - let ret = state - .client - .vote_proposal(proposal_id, vote_yes, None) - .await; - accepted_vote_err(ret)?; - - Ok(Json(VerifyCredentialResponse::new(vote_yes))) -} - -#[get("/epoch-credentials/")] -pub async fn epoch_credentials( - epoch: EpochId, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_epoch_credentials(epoch).await?; - - let response = if let Some(issued) = issued { - issued.into() - } else { - EpochCredentialsResponse { - epoch_id: epoch, - first_epoch_credential_id: None, - total_issued: 0, - } - }; - - Ok(Json(response)) -} - -#[get("/issued-credential/")] -pub async fn issued_credential( - id: i64, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_issued_credential(id).await?; - - let credential = if let Some(issued) = issued { - Some(issued.try_into()?) - } else { - None - }; - - Ok(Json(IssuedCredentialResponse { credential })) -} - -#[post("/issued-credentials", data = "")] -pub async fn issued_credentials( - params: Json, - state: &RocketState, -) -> Result> { - let params = params.into_inner(); - - if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(CoconutError::InvalidQueryArguments); - } - - let credentials = if let Some(pagination) = params.pagination { - state - .storage - .get_issued_credentials_paged(pagination) - .await? - } else { - state - .storage - .get_issued_credentials(params.credential_ids) - .await? - }; - - build_credentials_response(credentials).map(Json) -} diff --git a/nym-api/src/coconut/comm.rs b/nym-api/src/coconut/comm.rs deleted file mode 100644 index 48e7823fce..0000000000 --- a/nym-api/src/coconut/comm.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::Result; -use crate::nyxd; -use crate::support::nyxd::ClientInner; -use nym_coconut::VerificationKey; -use nym_coconut_dkg_common::types::{Epoch, EpochId}; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use std::cmp::min; -use std::collections::HashMap; -use std::ops::Deref; -use time::OffsetDateTime; -use tokio::sync::RwLock; - -#[async_trait] -pub trait APICommunicationChannel { - async fn current_epoch(&self) -> Result; - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result; -} - -struct CachedEpoch { - valid_until: OffsetDateTime, - current_epoch_id: EpochId, -} - -impl Default for CachedEpoch { - fn default() -> Self { - CachedEpoch { - valid_until: OffsetDateTime::UNIX_EPOCH, - current_epoch_id: 0, - } - } -} - -impl CachedEpoch { - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - async fn update(&mut self, epoch: Epoch) -> Result<()> { - let now = OffsetDateTime::now_utc(); - - let validity_duration = if let Some(epoch_finish) = epoch.deadline { - let state_end = - OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); - let until_epoch_state_end = state_end - now; - // make it valid until the next epoch transition or next 5min, whichever is smaller - min(until_epoch_state_end, 5 * time::Duration::MINUTE) - } else { - 5 * time::Duration::MINUTE - }; - - self.valid_until = now + validity_duration; - self.current_epoch_id = epoch.epoch_id; - - Ok(()) - } -} - -pub(crate) struct QueryCommunicationChannel { - nyxd_client: nyxd::Client, - - epoch_keys: RwLock>, - cached_epoch: RwLock, -} - -impl QueryCommunicationChannel { - pub fn new(nyxd_client: nyxd::Client) -> Self { - QueryCommunicationChannel { - nyxd_client, - epoch_keys: Default::default(), - cached_epoch: Default::default(), - } - } -} - -#[async_trait] -impl APICommunicationChannel for QueryCommunicationChannel { - async fn current_epoch(&self) -> Result { - let guard = self.cached_epoch.read().await; - if guard.is_valid() { - return Ok(guard.current_epoch_id); - } - - // update cache - drop(guard); - let mut guard = self.cached_epoch.write().await; - - let epoch = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => client.get_current_epoch().await?, - ClientInner::Signing(client) => client.get_current_epoch().await?, - }; - - guard.update(epoch).await?; - - return Ok(guard.current_epoch_id); - } - - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result { - if let Some(vk) = self.epoch_keys.read().await.get(&epoch_id) { - return Ok(vk.clone()); - } - - let mut guard = self.epoch_keys.write().await; - let coconut_api_clients = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => all_coconut_api_clients(client, epoch_id).await?, - ClientInner::Signing(client) => all_coconut_api_clients(client, epoch_id).await?, - }; - - let vk = obtain_aggregate_verification_key(&coconut_api_clients)?; - - guard.insert(epoch_id, vk.clone()); - - Ok(vk) - } -} diff --git a/nym-api/src/coconut/deposit.rs b/nym-api/src/coconut/deposit.rs deleted file mode 100644 index 985e9fadaa..0000000000 --- a/nym-api/src/coconut/deposit.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::{CoconutError, Result}; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, - DEPOSIT_INFO, DEPOSIT_VALUE, -}; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::helpers::find_tx_attribute; -use nym_validator_client::nyxd::TxResponse; - -pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> { - if request.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // extract actual public attributes + associated x25519 public key - let deposit_value = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(CoconutError::DepositValueNotFound)?; - - let deposit_info = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(CoconutError::DepositInfoNotFound)?; - - let x25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_IDENTITY_KEY, - ) - .ok_or(CoconutError::DepositVerifKeyNotFound)?; - - // we're not using it anymore, but for legacy reasons it must be present - let _ed25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_ENCRYPTION_KEY, - ) - .ok_or(CoconutError::DepositEncrKeyNotFound)?; - - // check public attributes against request data - // (thinking about it attaching that data might be redundant since we have the source of truth on the chain) - // safety: we won't read data out of bounds since we just checked we have BandwidthVoucher::PUBLIC_ATTRIBUTES values in the vec - if deposit_value != request.public_attributes_plain[0] { - return Err(CoconutError::InconsistentDepositValue { - request: request.public_attributes_plain[0].clone(), - on_chain: deposit_value, - }); - } - - if deposit_info != request.public_attributes_plain[1] { - return Err(CoconutError::InconsistentDepositInfo { - request: request.public_attributes_plain[1].clone(), - on_chain: deposit_info, - }); - } - - // verify signature - let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?; - let plaintext = BandwidthVoucherIssuanceData::request_plaintext( - &request.inner_sign_request, - request.tx_hash, - ); - x25519.verify(plaintext, &request.signature)?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::coconut::tests::{tx_entry_fixture, voucher_fixture}; - use cosmwasm_std::coin; - use nym_coconut::BlindSignRequest; - use nym_coconut_bandwidth_contract_common::events::DEPOSITED_FUNDS_EVENT_TYPE; - use nym_credentials::coconut::bandwidth::CredentialType; - use nym_validator_client::nyxd::{Event, EventAttribute}; - - #[tokio::test] - async fn validate_deposit_tx_test() { - let voucher = voucher_fixture(coin(1234, "unym"), None); - let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let correct_request = voucher_data.create_blind_sign_request_body(&signing_data); - - let mut tx_entry = tx_entry_fixture(correct_request.tx_hash); - let good_deposit_attribute = EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: correct_request.public_attributes_plain[0].clone(), - index: false, - }; - let good_info_attribute = EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: correct_request.public_attributes_plain[1].clone(), - index: false, - }; - let good_identity_key_attribute = EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - let good_encryption_key_attribute = EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - - tx_entry.tx_result.events.push(Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![], - }); - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositValueNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "10".parse().unwrap(), - index: false, - }, - good_info_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositValue { - request: "1234".to_string(), - on_chain: "10".to_string() - } - .to_string() - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositInfoNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: "bandwidth deposit info".parse().unwrap(), - index: false, - }, - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositInfo { - on_chain: "bandwidth deposit info".to_string(), - request: CredentialType::Voucher.to_string(), - } - .to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositVerifKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "verification key".parse().unwrap(), - index: false, - }, - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!( - err, - CoconutError::Ed25519ParseError( - nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } - ) - )); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He" - .parse() - .unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositEncrKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "encryption key".parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!(err, CoconutError::SignatureVerificationError(..))); - - let expected_encryption_key = "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6"; - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: expected_encryption_key.parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::SignatureVerificationError( - nym_crypto::asymmetric::identity::SignatureError::default(), - ) - .to_string(), - ); - - // hard-coded values, that generate a correct signature - let blind_sign_req = BlindSignRequest::from_bytes(&[ - 176, 113, 19, 237, 218, 252, 113, 20, 225, 238, 59, 88, 217, 45, 233, 178, 65, 28, 242, - 0, 222, 48, 110, 216, 26, 111, 51, 235, 61, 74, 200, 15, 130, 245, 45, 170, 155, 190, - 156, 77, 180, 142, 29, 63, 15, 224, 150, 31, 139, 24, 65, 175, 143, 153, 11, 203, 33, - 16, 152, 22, 221, 203, 99, 233, 208, 142, 161, 194, 46, 227, 177, 96, 119, 30, 175, 69, - 104, 14, 2, 191, 26, 94, 30, 165, 15, 28, 40, 176, 1, 78, 253, 79, 20, 137, 102, 74, 2, - 0, 0, 0, 0, 0, 0, 0, 131, 133, 112, 115, 53, 98, 58, 166, 240, 70, 185, 210, 203, 12, - 114, 66, 180, 38, 139, 12, 187, 45, 250, 201, 68, 102, 159, 172, 218, 124, 151, 23, - 172, 18, 216, 122, 246, 7, 185, 76, 20, 167, 123, 122, 152, 241, 175, 226, 176, 8, 170, - 70, 140, 252, 36, 130, 67, 204, 111, 116, 107, 92, 200, 77, 252, 31, 138, 18, 10, 215, - 165, 243, 95, 199, 193, 61, 200, 187, 22, 198, 109, 213, 145, 71, 171, 132, 174, 68, - 105, 248, 0, 115, 50, 55, 199, 84, 67, 16, 125, 216, 250, 154, 115, 174, 9, 206, 44, - 88, 63, 163, 124, 10, 239, 64, 158, 191, 27, 169, 177, 194, 223, 142, 202, 206, 189, - 122, 123, 91, 171, 15, 40, 192, 148, 75, 174, 24, 116, 229, 127, 170, 110, 183, 151, 2, - 118, 168, 22, 113, 87, 237, 91, 228, 249, 120, 114, 255, 53, 175, 245, 39, 2, 0, 0, 0, - 0, 0, 0, 0, 225, 45, 230, 25, 62, 202, 96, 166, 171, 241, 206, 137, 254, 51, 154, 255, - 122, 130, 107, 54, 5, 206, 207, 120, 193, 214, 64, 10, 111, 195, 86, 55, 201, 36, 10, - 18, 154, 158, 183, 87, 185, 59, 228, 89, 134, 193, 217, 188, 64, 164, 249, 21, 248, 20, - 207, 58, 31, 10, 19, 176, 246, 150, 45, 48, 2, 0, 0, 0, 0, 0, 0, 0, 173, 60, 65, 209, - 100, 114, 138, 186, 158, 150, 109, 230, 111, 86, 101, 72, 194, 237, 173, 195, 139, 175, - 238, 25, 169, 18, 188, 75, 77, 54, 111, 20, 115, 235, 195, 2, 123, 133, 164, 81, 15, - 45, 11, 84, 139, 38, 8, 224, 197, 181, 95, 147, 49, 77, 193, 207, 52, 141, 195, 195, - 66, 137, 17, 32, - ]) - .unwrap(); - - let correct_request = BlindSignRequestBody::new( - blind_sign_req, - "7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B" - .parse() - .unwrap(), - "3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(), - voucher.get_plain_public_attributes(), - ); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "3xoM5GmUSq7YW4YNQrax1fEFLw1GbZozxe6UUoJcrqLG" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6" - .parse() - .unwrap(), - index: false, - }, - ]; - let res = validate_deposit_tx(&correct_request, tx_entry.clone()).await; - assert!(res.is_ok()) - } -} diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs deleted file mode 100644 index 699c9b8823..0000000000 --- a/nym-api/src/coconut/helpers.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; -use nym_api_requests::coconut::models::FreePassRequest; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{Attribute, BlindSignRequest, BlindedSignature, SecretKey}; -use nym_validator_client::nyxd::error::NyxdError::AbciError; - -// If the result is already established, the vote might be redundant and -// thus the transaction might fail -pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), CoconutError> { - if let Err(CoconutError::NyxdError(AbciError { ref log, .. })) = ret { - let accepted_err = - nym_multisig_contract_common::error::ContractError::NotOpen {}.to_string(); - // If redundant voting is not the case, error out on all other error variants - if !log.contains(&accepted_err) { - ret?; - } - } - Ok(()) -} - -pub(crate) trait CredentialRequest { - fn blind_sign_request(&self) -> &BlindSignRequest; - - fn public_attributes(&self) -> Vec; -} - -impl CredentialRequest for BlindSignRequestBody { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -impl CredentialRequest for FreePassRequest { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -pub(crate) fn blind_sign( - request: &C, - signing_key: &SecretKey, -) -> Result { - let public_attributes = request.public_attributes(); - let attributes_ref = public_attributes.iter().collect::>(); - - Ok(nym_coconut::blind_sign( - bandwidth_credential_params(), - signing_key, - request.blind_sign_request(), - &attributes_ref, - )?) -} diff --git a/nym-api/src/coconut/keys/persistence.rs b/nym-api/src/coconut/keys/persistence.rs deleted file mode 100644 index 4a67876036..0000000000 --- a/nym-api/src/coconut/keys/persistence.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; -use nym_coconut::{CoconutError, KeyPair, SecretKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_pemstore::traits::PemStorableKey; -use std::mem; - -impl PemStorableKey for KeyPairWithEpoch { - // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose - type Error = CoconutError; - - fn pem_type() -> &'static str { - "COCONUT KEY WITH EPOCH" - } - - fn to_bytes(&self) -> Vec { - let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); - bytes.append(&mut self.keys.secret_key().to_bytes()); - bytes - } - - fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() <= mem::size_of::() { - return Err(CoconutError::Deserialization( - "insufficient number of bytes to decode secret key with epoch id".into(), - )); - } - let epoch_id = EpochId::from_be_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]); - - let sk = SecretKey::from_bytes(&bytes[mem::size_of::()..])?; - let vk = sk.verification_key(bandwidth_credential_params()); - - Ok(KeyPairWithEpoch { - keys: KeyPair::from_keys(sk, vk), - issued_for_epoch: epoch_id, - }) - } -} diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs deleted file mode 100644 index 5047982304..0000000000 --- a/nym-api/src/coconut/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use self::comm::APICommunicationChannel; -use crate::coconut::client::Client as LocalClient; -use crate::coconut::state::State; -use crate::support::storage::NymApiStorage; -use keys::KeyPair; -use nym_config::defaults::NYM_API_VERSION; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES}; -use rocket::fairing::AdHoc; - -pub(crate) mod api_routes; -pub(crate) mod client; -pub(crate) mod comm; -mod deposit; -pub(crate) mod dkg; -pub(crate) mod error; -pub(crate) mod helpers; -pub(crate) mod keys; -pub(crate) mod state; -pub(crate) mod storage; -#[cfg(test)] -pub(crate) mod tests; - -// equivalent of 10nym -pub(crate) const MINIMUM_BALANCE: u128 = 10_000000; - -pub fn stage( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, -) -> AdHoc -where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, -{ - let state = State::new( - client, - mix_denom, - identity_keypair, - key_pair, - comm_channel, - storage, - ); - AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { - rocket.manage(state).mount( - // this format! is so ugly... - format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"), - routes![ - api_routes::get_current_free_pass_nonce, - api_routes::post_free_pass, - api_routes::post_blind_sign, - api_routes::verify_bandwidth_credential, - api_routes::epoch_credentials, - api_routes::issued_credential, - api_routes::issued_credentials, - ], - ) - }) -} diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs deleted file mode 100644 index ac7c3632f8..0000000000 --- a/nym-api/src/coconut/state.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::client::Client as LocalClient; -use crate::coconut::comm::APICommunicationChannel; -use crate::coconut::deposit::validate_deposit_tx; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPair; -use crate::coconut::storage::CoconutStorageExt; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::helpers::issued_credential_plaintext; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{BlindedSignature, VerificationKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::{AccountId, Hash, TxResponse}; -use rand::rngs::OsRng; -use rand::RngCore; -use std::sync::Arc; -use time::{Duration, OffsetDateTime}; -use tokio::sync::{OnceCell, RwLock}; - -pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - -pub struct State { - pub(crate) client: Arc, - pub(crate) bandwidth_contract_admin: OnceCell>, - pub(crate) mix_denom: String, - pub(crate) coconut_keypair: KeyPair, - pub(crate) identity_keypair: identity::KeyPair, - pub(crate) comm_channel: Arc, - pub(crate) storage: NymApiStorage, - pub(crate) freepass_nonce: Arc>, - pub(crate) authorised_freepass_requester: Arc>, -} - -const FREEPASS_REQUESTER_TTL: Duration = Duration::hours(1); -const AUTHORISED_FREEPASS_REQUESTER_ENDPOINT: &str = - "https://nymtech.net/.wellknown/authorised-freepass-requester.txt"; - -pub struct AuthorisedFreepassRequester { - address: Option, - refreshed_at: OffsetDateTime, -} - -impl Default for AuthorisedFreepassRequester { - fn default() -> Self { - AuthorisedFreepassRequester { - address: None, - refreshed_at: OffsetDateTime::UNIX_EPOCH, - } - } -} - -impl State { - pub(crate) fn new( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, - ) -> Self - where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, - { - let client = Arc::new(client); - let comm_channel = Arc::new(comm_channel); - - let mut nonce = [0u8; 16]; - OsRng.fill_bytes(&mut nonce); - - Self { - client, - bandwidth_contract_admin: OnceCell::new(), - mix_denom, - coconut_keypair: key_pair, - identity_keypair, - comm_channel, - storage, - freepass_nonce: Arc::new(RwLock::new(nonce)), - authorised_freepass_requester: Arc::new(Default::default()), - } - } - - /// Check if this nym-api has already issued a credential for the provided deposit hash. - /// If so, return it. - pub async fn already_issued(&self, tx_hash: Hash) -> Result> { - self.storage - .get_issued_bandwidth_credential_by_hash(&tx_hash.to_string()) - .await? - .map(|cred| cred.try_into()) - .transpose() - } - - pub async fn get_transaction(&self, tx_hash: Hash) -> Result { - self.client.get_tx(tx_hash).await - } - - pub async fn get_bandwidth_contract_admin(&self) -> Result<&Option> { - self.bandwidth_contract_admin - .get_or_try_init(|| async { self.client.bandwidth_contract_admin().await }) - .await - } - - async fn try_get_authorised_freepass_requester(&self) -> Result { - let address = reqwest::Client::builder() - .user_agent(format!( - "nym-api / {} identity: {}", - env!("CARGO_PKG_VERSION"), - self.identity_keypair.public_key().to_base58_string() - )) - .build()? - .get(AUTHORISED_FREEPASS_REQUESTER_ENDPOINT) - .send() - .await? - .text() - .await?; - let trimmed = address.trim(); - - address.parse().map_err( - |_| CoconutError::MalformedAuthorisedFreepassRequesterAddress { - address: trimmed.to_string(), - }, - ) - } - - pub async fn get_authorised_freepass_requester(&self) -> Option { - { - let cached = self.authorised_freepass_requester.read().await; - - // the entry hasn't expired - if cached.refreshed_at + FREEPASS_REQUESTER_TTL >= OffsetDateTime::now_utc() { - if let Some(cached_address) = cached.address.as_ref() { - return Some(cached_address.clone()); - } - } - } - - // refresh cache - let mut cache = self.authorised_freepass_requester.write().await; - - // whatever happens, update refresh time - cache.refreshed_at = OffsetDateTime::now_utc(); - - let refreshed = match self.try_get_authorised_freepass_requester().await { - Ok(upstream) => upstream, - Err(err) => { - warn!("failed to obtain authorised freepass requester address: {err}"); - return None; - } - }; - - cache.address = Some(refreshed.clone()); - Some(refreshed) - } - - pub async fn validate_request( - &self, - request: &BlindSignRequestBody, - tx: TxResponse, - ) -> Result<()> { - validate_deposit_tx(request, tx).await - } - - pub(crate) async fn sign_and_store_credential( - &self, - current_epoch: EpochId, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result { - let encoded_commitments = request_body.encode_commitments(); - - let plaintext = issued_credential_plaintext( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - &encoded_commitments, - &request_body.public_attributes_plain, - ); - - let signature = self.identity_keypair.private_key().sign(plaintext); - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .storage - .store_issued_credential( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - signature, - encoded_commitments, - request_body.public_attributes_plain, - ) - .await?; - - Ok(credential_id) - } - - pub async fn store_issued_credential( - &self, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result<()> { - let current_epoch = self.comm_channel.current_epoch().await?; - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .sign_and_store_credential(current_epoch, request_body, blinded_signature) - .await?; - self.storage - .update_epoch_credentials_entry(current_epoch, credential_id) - .await?; - debug!("the stored credential has id {credential_id}"); - - Ok(()) - } - - pub async fn verification_key(&self, epoch_id: EpochId) -> Result { - self.comm_channel - .aggregated_verification_key(epoch_id) - .await - } -} diff --git a/nym-api/src/coconut/storage/manager.rs b/nym-api/src/coconut/storage/manager.rs deleted file mode 100644 index c22d295459..0000000000 --- a/nym-api/src/coconut/storage/manager.rs +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::models::{EpochCredentials, IssuedCredential}; -use crate::support::storage::manager::StorageManager; -use nym_coconut_dkg_common::types::EpochId; - -#[async_trait] -pub trait CoconutStorageManagerExt { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error>; - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - #[allow(dead_code)] - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; - - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error>; - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result; - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error>; - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error>; -} - -#[async_trait] -impl CoconutStorageManagerExt for StorageManager { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - -1, - 0 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - // the logic in this function can be summarised with: - // 1. get the current EpochCredentials for this epoch - // 2. if it exists -> increment `total_issued` - // 3. it has invalid (i.e. -1) `start_id` set it to the provided value - // 4. if it didn't exist, create new entry - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - // make the atomic transaction in case other tasks are attempting to use the pool - let mut tx = self.connection_pool.begin().await?; - - if let Some(existing) = sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&mut tx) - .await? - { - // the entry has existed before -> update it - if existing.total_issued == 0 { - // no credentials has been issued -> we have to set the `start_id` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = 1, start_id = ? - WHERE epoch_id = ? - "#, - credential_id, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } else { - // we have issued credentials in this epoch before -> just increment `total_issued` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = total_issued + 1 - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } - } else { - // the entry has never been created -> probably some race condition; create it instead - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - credential_id, - 1 - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id = ? - "#, - credential_id - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE tx_hash = ? - "#, - tx_hash - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result { - let row_id = sqlx::query!( - r#" - INSERT INTO issued_credential - (epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes) - VALUES - (?, ?, ?, ?, ?, ?) - "#, - epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes - ).execute(&self.connection_pool).await?.last_insert_rowid(); - - Ok(row_id) - } - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error> { - // that sucks : ( - // https://stackoverflow.com/a/70032524 - let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); - let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); - let mut query = sqlx::query_as(&query_str); - for id in credential_ids { - query = query.bind(id) - } - - query.fetch_all(&self.connection_pool).await - } - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id > ? - ORDER BY id - LIMIT ? - "#, - start_after, - limit - ) - .fetch_all(&self.connection_pool) - .await - } - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error> { - sqlx::query!("UPDATE issued_freepass SET issued = issued + 1",) - .execute(&self.connection_pool) - .await?; - Ok(()) - } -} diff --git a/nym-api/src/coconut/storage/mod.rs b/nym-api/src/coconut/storage/mod.rs deleted file mode 100644 index 1b29f51b43..0000000000 --- a/nym-api/src/coconut/storage/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::manager::CoconutStorageManagerExt; -use crate::coconut::storage::models::{join_attributes, EpochCredentials, IssuedCredential}; -use crate::node_status_api::models::NymApiStorageError; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::models::Pagination; -use nym_coconut::{Base58, BlindedSignature}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Hash; - -pub(crate) mod manager; -pub(crate) mod models; - -const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; - -#[async_trait] -pub trait CoconutStorageExt { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError>; - - #[allow(dead_code)] - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError>; - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError>; - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError>; - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError>; - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result; - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError>; - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError>; - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError>; -} - -#[async_trait] -impl CoconutStorageExt for NymApiStorage { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_epoch_credentials(epoch_id).await?) - } - - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .create_epoch_credentials_entry(epoch_id) - .await?) - } - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .update_epoch_credentials_entry(epoch_id, credential_id) - .await?) - } - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credential(credential_id).await?) - } - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .manager - .get_issued_bandwidth_credential_by_hash(tx_hash) - .await?) - } - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result { - Ok(self - .manager - .store_issued_credential( - epoch_id, - tx_hash.to_string(), - partial_credential.to_bs58(), - signature.to_base58_string(), - join_attributes(private_commitments), - join_attributes(public_attributes), - ) - .await?) - } - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credentials(credential_ids).await?) - } - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError> { - // rows start at 1 - let start_after = pagination.last_key.unwrap_or(0); - let limit = match pagination.limit { - Some(v) => { - if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { - DEFAULT_CREDENTIALS_PAGE_LIMIT - } else { - v - } - } - None => DEFAULT_CREDENTIALS_PAGE_LIMIT, - }; - - Ok(self - .manager - .get_issued_credentials_paged(start_after, limit) - .await?) - } - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError> { - Ok(self.manager.increment_issued_freepasses().await?) - } -} diff --git a/nym-api/src/coconut/storage/models.rs b/nym-api/src/coconut/storage/models.rs deleted file mode 100644 index 4d2935c057..0000000000 --- a/nym-api/src/coconut/storage/models.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use nym_api_requests::coconut::models::{ - EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, - IssuedCredentialBody as ApiIssuedCredentialInner, -}; -use nym_api_requests::coconut::BlindedSignatureResponse; -use nym_coconut::{Base58, BlindedSignature}; -use sqlx::FromRow; -use std::fmt::Display; - -pub struct EpochCredentials { - pub epoch_id: u32, - pub start_id: i64, - pub total_issued: u32, -} - -impl From for EpochCredentialsResponse { - fn from(value: EpochCredentials) -> Self { - let first_epoch_credential_id = if value.start_id == -1 { - None - } else { - Some(value.start_id) - }; - - EpochCredentialsResponse { - epoch_id: value.epoch_id as u64, - first_epoch_credential_id, - total_issued: value.total_issued, - } - } -} - -#[derive(FromRow)] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: String, - - /// base58-encoded issued credential - pub bs58_partial_credential: String, - - /// base58-encoded signature on the issued credential (and the attributes) - pub bs58_signature: String, - - // i.e. "'attr1','attr2',..." - pub joined_private_commitments: String, - - // i.e. "'attr1','attr2',..." - pub joined_public_attributes: String, -} - -impl TryFrom for ApiIssuedCredentialInner { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(ApiIssuedCredentialInner { - credential: ApiIssuedCredential { - id: value.id, - epoch_id: value.epoch_id, - tx_hash: value - .tx_hash - .parse() - .map_err(|source| CoconutError::TxHashParseError { source })?, - blinded_partial_credential: BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?, - bs58_encoded_private_attributes_commitments: split_attributes( - &value.joined_private_commitments, - ), - public_attributes: split_attributes(&value.joined_public_attributes), - }, - signature: value.bs58_signature.parse()?, - }) - } -} - -impl TryFrom for BlindedSignatureResponse { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, - }) - } -} - -impl TryFrom for BlindedSignature { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?) - } -} - -pub fn join_attributes(attrs: I) -> String -where - I: IntoIterator, - M: Display, -{ - // I could have used `attrs.into_iter().join(",")`, - // but my IDE didn't like it (compiler was fine) - itertools::Itertools::join(&mut attrs.into_iter(), ",") -} - -pub fn split_attributes(attrs: &str) -> Vec { - attrs.split(',').map(|s| s.to_string()).collect() -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs new file mode 100644 index 0000000000..f552698d86 --- /dev/null +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -0,0 +1,81 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::state::EcashState; +use log::trace; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, +}; +use nym_api_requests::ecash::VerificationKeyResponse; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use time::Date; + +// routes with globally aggregated keys, signatures, etc. + +#[openapi(tag = "Ecash Global Data")] +#[get("/master-verification-key?")] +pub async fn master_verification_key( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_verification_key request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let key = state.master_verification_key(epoch_id).await?; + + Ok(Json(VerificationKeyResponse::new(key.clone()))) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-expiration-date-signatures?")] +pub async fn expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_expiration_date_signatures request"); + + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .master_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-coin-indices-signatures?")] +pub async fn coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_coin_indices_signatures request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; + + Ok(Json(AggregatedCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/coconut/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs similarity index 81% rename from nym-api/src/coconut/api_routes/helpers.rs rename to nym-api/src/ecash/api_routes/helpers.rs index 93446d0d15..b05272d472 100644 --- a/nym-api/src/coconut/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -1,10 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; -use crate::coconut::storage::models::IssuedCredential; -use nym_api_requests::coconut::models::IssuedCredentialBody; -use nym_api_requests::coconut::models::IssuedCredentialsResponse; +use crate::ecash::error::Result; +use crate::ecash::storage::models::IssuedCredential; +use nym_api_requests::ecash::models::IssuedCredentialBody; +use nym_api_requests::ecash::models::IssuedCredentialsResponse; use std::collections::BTreeMap; pub(crate) fn build_credentials_response( diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs new file mode 100644 index 0000000000..5b85e6b0cf --- /dev/null +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::build_credentials_response; +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; + +#[openapi(tag = "Ecash")] +#[get("/epoch-credentials/")] +pub async fn epoch_credentials( + epoch: EpochId, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; + + let response = if let Some(issued) = issued { + issued.into() + } else { + EpochCredentialsResponse { + epoch_id: epoch, + first_epoch_credential_id: None, + total_issued: 0, + } + }; + + Ok(Json(response)) +} + +#[openapi(tag = "Ecash")] +#[get("/issued-credential/")] +pub async fn issued_credential( + id: i64, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_issued_credential(id).await?; + + let credential = if let Some(issued) = issued { + Some(issued.try_into()?) + } else { + None + }; + + Ok(Json(IssuedCredentialResponse { credential })) +} + +#[openapi(tag = "Ecash")] +#[post("/issued-credentials", data = "")] +pub async fn issued_credentials( + params: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let params = params.into_inner(); + + if params.pagination.is_some() && !params.credential_ids.is_empty() { + return Err(EcashError::InvalidQueryArguments); + } + + let credentials = if let Some(pagination) = params.pagination { + state + .aux + .storage + .get_issued_credentials_paged(pagination) + .await? + } else { + state + .aux + .storage + .get_issued_credentials(params.credential_ids) + .await? + }; + + build_credentials_response(credentials).map(Json) +} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs new file mode 100644 index 0000000000..6037a8d18f --- /dev/null +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod aggregation; +mod helpers; +pub(crate) mod issued; +pub(crate) mod partial_signing; +pub(crate) mod spending; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs new file mode 100644 index 0000000000..a0958a2a6b --- /dev/null +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -0,0 +1,118 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::helpers::blind_sign; +use crate::ecash::state::EcashState; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, +}; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::ops::Deref; +use time::Date; + +#[openapi(tag = "Ecash")] +#[post("/blind-sign", data = "")] +pub async fn post_blind_sign( + blind_sign_request_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + debug!("Received blind sign request"); + trace!("body: {:?}", blind_sign_request_body); + + // check if we have the signing key available + debug!("checking if we actually have ecash keys derived..."); + let signing_key = state.ecash_signing_key().await?; + + // basic check of expiration date validity + if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { + return Err(EcashError::ExpirationDateTooLate); + } + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + // check if we already issued a credential for this deposit + let deposit_id = blind_sign_request_body.deposit_id; + debug!( + "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", + ); + if let Some(blinded_signature) = state.already_issued(deposit_id).await? { + return Ok(Json(BlindedSignatureResponse { blinded_signature })); + } + + //check if account was blacklisted + let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); + state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; + + // get the deposit details of the claimed id + debug!("getting deposit details from the chain"); + let deposit = state.get_deposit(deposit_id).await?; + + // check validity of the request + debug!("fully validating received request"); + state + .validate_request(&blind_sign_request_body, deposit) + .await?; + + // produce the partial signature + debug!("producing the partial credential"); + let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?; + + // store the information locally + debug!("storing the issued credential in the database"); + state + .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .await?; + + // finally return the credential to the client + Ok(Json(BlindedSignatureResponse { blinded_signature })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-expiration-date-signatures?")] +pub async fn partial_expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .partial_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(PartialExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-coin-indices-signatures?")] +pub async fn partial_coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; + + Ok(Json(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs new file mode 100644 index 0000000000..d6bb91ea21 --- /dev/null +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -0,0 +1,196 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{ + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, + EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_compact_ecash::identify::IdentifyResult; +use nym_ecash_time::EcashTime; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::collections::HashSet; +use std::ops::Deref; +use time::macros::time; +use time::{OffsetDateTime, Time}; + +const ONE_AM: Time = time!(1:00); + +fn reject_ticket( + reason: EcashTicketVerificationRejection, +) -> crate::ecash::error::Result> { + Ok(Json(EcashTicketVerificationResponse::reject(reason))) +} + +// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` +// introduce bloomfilter checks without touching storage first, etc. +#[openapi(tag = "Ecash")] +#[post("/verify-ecash-ticket", data = "")] +pub async fn verify_ticket( + // TODO in the future: make it send binary data rather than json + verify_ticket_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let credential_data = &verify_ticket_body.credential; + let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; + let sn = &credential_data.encoded_serial_number(); + let spend_date = credential_data.spend_date; + let epoch_id = credential_data.epoch_id; + + let now = OffsetDateTime::now_utc(); + let today_ecash = now.ecash_date(); + + #[allow(clippy::unwrap_used)] + let yesterday_ecash = today_ecash.previous_day().unwrap(); + + // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC + if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { + return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }); + } + + // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification + // TODO: when blacklisting is implemented, this should get removed + if state.check_bloomfilter(sn).await { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + // actual double spend detection with storage + if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { + match nym_compact_ecash::identify::identify( + &credential_data.payment, + &previous_payment.payment, + credential_data.pay_info, + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + } + } + } + + let verification_key = state.master_verification_key(Some(epoch_id)).await?; + + // perform actual crypto verification + if credential_data.verify(&verification_key).is_err() { + return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + } + + // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN + let was_present = state + .update_bloomfilter(sn, spend_date, today_ecash) + .await?; + if was_present { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + //store credential + state + .store_verified_ticket(credential_data, gateway_cosmos_addr) + .await?; + + Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) +} + +// // for particular SN returns what gateway has submitted it and whether it has been verified correctly +// pub async fn credential_status() -> ! { +// todo!() +// } + +#[openapi(tag = "Ecash")] +#[post( + "/batch-redeem-ecash-tickets", + data = "" +)] +pub async fn batch_redeem_tickets( + // TODO in the future: make it send binary data rather than json + batch_redeem_credentials_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + // 1. see if that gateway has even submitted any tickets + let Some(provider_info) = state + .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) + .await? + else { + return Err(EcashError::NotTicketsProvided); + }; + + // 2. check if the gateway is not trying to spam the redemption requests + // (we have to protect our poor chain) + if let Some(last_redemption) = provider_info.last_batch_verification { + let now = OffsetDateTime::now_utc(); + let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; + + if next_allowed > now { + return Err(EcashError::TooFrequentRedemption { + last_redemption, + next_allowed, + }); + } + } + + // 3. verify the request digest + if !batch_redeem_credentials_body.verify_digest() { + return Err(EcashError::MismatchedRequestDigest); + } + + // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) + state + .validate_redemption_proposal(&batch_redeem_credentials_body) + .await?; + + let proposal_id = batch_redeem_credentials_body.proposal_id; + let received = batch_redeem_credentials_body + .into_inner() + .included_serial_numbers; + + // 5. check if **every** serial number included in the request has been verified by us + // if we have more than requested, tough luck, they're going to lose them + let verified = state.get_redeemable_tickets(provider_info).await?; + let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); + + for sn in &received { + if !verified_tickets.contains(sn.deref()) { + return Err(EcashError::TicketNotVerified { + serial_number_bs58: bs58::encode(sn).into_string(), + }); + } + } + + // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx + state.accept_proposal(proposal_id).await?; + Ok(Json(EcashBatchTicketRedemptionResponse { + proposal_accepted: true, + })) +} + +// explicitly mark it as v1 in the URL because the response type WILL change; +// it will probably be compressed bincode or something +#[openapi(tag = "Ecash")] +#[get("/double-spending-filter-v1")] +pub async fn double_spending_filter_v1( + state: &RocketState, +) -> crate::ecash::error::Result> { + let spent_credentials_export = state.export_bloomfilter().await; + Ok(Json(SpentCredentialsResponse::new( + spent_credentials_export, + ))) +} diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/ecash/client.rs similarity index 85% rename from nym-api/src/coconut/client.rs rename to nym-api/src/ecash/client.rs index d009989996..e0970f2155 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/ecash/client.rs @@ -1,10 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; +use crate::ecash::error::Result; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, RegisteredDealerDetails, }; @@ -19,8 +18,11 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; +use nym_validator_client::nyxd::{AccountId, Fee}; +use nym_validator_client::EcashApiClient; #[async_trait] pub trait Client { @@ -28,9 +30,7 @@ pub trait Client { async fn dkg_contract_address(&self) -> Result; - async fn bandwidth_contract_admin(&self) -> Result>; - - async fn get_tx(&self, tx_hash: Hash) -> Result; + async fn get_deposit(&self, deposit_id: DepositId) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; @@ -38,10 +38,10 @@ pub trait Client { async fn get_vote(&self, proposal_id: u64, voter: String) -> Result; - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result; + public_key: String, + ) -> Result; async fn contract_state(&self) -> Result; @@ -50,6 +50,7 @@ pub trait Client { async fn group_member(&self, addr: String) -> Result; async fn get_current_epoch_threshold(&self) -> Result>; + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result>; async fn get_self_registered_dealer_details(&self) -> Result; @@ -99,6 +100,8 @@ pub trait Client { async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result>; + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result>; + async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs new file mode 100644 index 0000000000..20941c484c --- /dev/null +++ b/nym-api/src/ecash/comm.rs @@ -0,0 +1,147 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::helpers::CachedImmutableEpochItem; +use crate::{ecash, nyxd}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; +use nym_dkg::Threshold; +use nym_validator_client::EcashApiClient; +use std::cmp::min; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockWriteGuard}; + +#[async_trait] +pub trait APICommunicationChannel { + async fn current_epoch(&self) -> Result; + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result>; + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result; + + async fn dkg_in_progress(&self) -> Result; +} + +struct CachedEpoch { + valid_until: OffsetDateTime, + current_epoch: Epoch, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch: Epoch::default(), + } + } +} + +impl CachedEpoch { + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + async fn update(&mut self, epoch: Epoch) -> Result<()> { + let now = OffsetDateTime::now_utc(); + + let validity_duration = if let Some(epoch_finish) = epoch.deadline { + let state_end = + OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + // make it valid until the next epoch transition or next 5min, whichever is smaller + min(until_epoch_state_end, 5 * time::Duration::MINUTE) + } else { + 5 * time::Duration::MINUTE + }; + + self.valid_until = now + validity_duration; + self.current_epoch = epoch; + + Ok(()) + } +} + +pub(crate) struct QueryCommunicationChannel { + nyxd_client: nyxd::Client, + + epoch_clients: CachedImmutableEpochItem>, + cached_epoch: RwLock, + threshold_values: CachedImmutableEpochItem, +} + +impl QueryCommunicationChannel { + pub fn new(nyxd_client: nyxd::Client) -> Self { + QueryCommunicationChannel { + nyxd_client, + epoch_clients: Default::default(), + cached_epoch: Default::default(), + threshold_values: Default::default(), + } + } + + async fn update_epoch_cache(&self) -> Result> { + let mut guard = self.cached_epoch.write().await; + + let epoch = ecash::client::Client::get_current_epoch(&self.nyxd_client).await?; + + guard.update(epoch).await?; + Ok(guard) + } +} + +#[async_trait] +impl APICommunicationChannel for QueryCommunicationChannel { + async fn current_epoch(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(guard.current_epoch.epoch_id); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(guard.current_epoch.epoch_id); + } + + // TODO: perhaps this should be returning a ReadGuard instead? + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + self.epoch_clients + .get_or_init(epoch_id, || async { + self.nyxd_client + .get_registered_ecash_clients(epoch_id) + .await + }) + .await + .map(|guard| guard.clone()) + } + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = + ecash::client::Client::get_epoch_threshold(&self.nyxd_client, epoch_id).await? + { + Ok(threshold) + } else { + Err(EcashError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } + + async fn dkg_in_progress(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(!guard.current_epoch.state.is_in_progress()); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(!guard.current_epoch.state.is_in_progress()); + } +} diff --git a/nym-api/src/ecash/deposit.rs b/nym-api/src/ecash/deposit.rs new file mode 100644 index 0000000000..cc14884895 --- /dev/null +++ b/nym-api/src/ecash/deposit.rs @@ -0,0 +1,107 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::Result; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::Deposit; + +pub async fn validate_deposit(request: &BlindSignRequestBody, deposit: Deposit) -> Result<()> { + // verify signature with the pubkey used in deposit + let ed25519 = ed25519::PublicKey::from_base58_string(deposit.bs58_encoded_ed25519_pubkey)?; + let plaintext = + IssuanceTicketBook::request_plaintext(&request.inner_sign_request, request.deposit_id); + ed25519.verify(plaintext, &request.signature)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::ecash::error::EcashError; + use crate::ecash::tests::voucher_fixture; + use nym_compact_ecash::{generate_keypair_user, scheme::withdrawal::WithdrawalRequest}; + use rand::rngs::OsRng; + use time::macros::date; + + #[tokio::test] + async fn validate_deposit_test() { + let mut rng = OsRng; + let deposit_id = 42; + let voucher = voucher_fixture(Some(deposit_id)); + let signing_data = voucher.prepare_for_signing(); + let correct_request = voucher.create_blind_sign_request_body(&signing_data); + + let valid_ed25519 = ed25519::KeyPair::new(&mut rng); + let bs58_encoded_ed25519 = valid_ed25519.public_key().to_base58_string(); + + let malformed_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "invalided25519pubkey".to_string(), + }; + + let err = validate_deposit(&correct_request, malformed_deposit) + .await + .unwrap_err(); + + assert!(matches!( + err, + EcashError::Ed25519ParseError( + nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } + ) + )); + + let wrong_deposit = Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519, + }; + + let err = validate_deposit(&correct_request, wrong_deposit) + .await + .unwrap_err(); + assert!(matches!(err, EcashError::SignatureVerificationError(..))); + + //hard-coded values, that generate a correct signature + let blind_sign_req = WithdrawalRequest::from_bytes(&[ + 48, 168, 84, 109, 206, 194, 237, 227, 205, 67, 60, 127, 85, 54, 246, 120, 88, 129, 121, + 50, 255, 133, 50, 54, 155, 172, 179, 52, 16, 250, 6, 209, 67, 54, 251, 20, 37, 124, + 115, 63, 182, 101, 188, 68, 149, 18, 149, 57, 167, 48, 149, 100, 41, 48, 143, 115, 93, + 90, 244, 164, 161, 224, 65, 160, 63, 141, 65, 86, 128, 136, 128, 194, 40, 106, 158, 40, + 235, 242, 51, 108, 3, 109, 120, 11, 100, 82, 188, 61, 41, 12, 232, 54, 162, 243, 43, + 222, 215, 216, 2, 48, 137, 243, 126, 118, 124, 83, 221, 53, 252, 163, 175, 215, 94, 90, + 249, 172, 3, 222, 13, 45, 166, 245, 126, 173, 199, 89, 206, 11, 22, 204, 47, 26, 40, + 191, 217, 139, 75, 101, 45, 5, 62, 251, 52, 36, 117, 101, 166, 63, 48, 152, 195, 163, + 179, 117, 148, 93, 223, 210, 119, 105, 59, 71, 88, 155, 17, 33, 4, 87, 203, 169, 40, + 93, 203, 153, 213, 105, 107, 181, 214, 2, 25, 19, 187, 217, 243, 246, 185, 152, 81, + 118, 11, 169, 100, 74, 88, 215, 37, 32, 31, 123, 5, 222, 103, 255, 236, 74, 37, 222, + 170, 136, 5, 49, 4, 183, 156, 223, 33, 112, 122, 81, 122, 221, 166, 27, 5, 44, 153, 37, + 229, 107, 32, 95, 45, 147, 187, 40, 141, 22, 9, 222, 232, 125, 34, 52, 152, 157, 14, + 228, 200, 183, 29, 62, 24, 201, 228, 103, 119, 89, 186, 79, 116, 75, 53, 2, 32, 219, + 72, 52, 255, 108, 74, 76, 126, 233, 46, 34, 70, 188, 47, 57, 66, 153, 14, 6, 242, 112, + 129, 108, 166, 188, 226, 183, 51, 45, 195, 190, 58, 32, 170, 54, 18, 64, 215, 82, 118, + 243, 66, 186, 137, 175, 230, 172, 174, 226, 104, 188, 123, 239, 77, 180, 32, 225, 73, + 208, 255, 27, 195, 181, 201, 21, 2, 32, 34, 231, 200, 93, 64, 117, 244, 169, 58, 64, + 39, 5, 228, 205, 119, 135, 221, 130, 241, 205, 184, 182, 34, 248, 85, 26, 241, 233, 52, + 244, 17, 15, 32, 157, 211, 145, 238, 16, 101, 55, 132, 233, 11, 249, 129, 41, 226, 250, + 146, 160, 155, 154, 81, 241, 129, 154, 24, 221, 196, 54, 210, 16, 24, 116, 31, + ]) + .unwrap(); + let expiration_date = date!(2024 - 02 - 19); + let ecash_keypair = generate_keypair_user(); + + let correct_request = BlindSignRequestBody::new( + blind_sign_req, + deposit_id, + "3MpHDLYMCmuMvZ9zkZXPkTK6nKArvQW3dJA1notoPPxnbBW2ommkR2dkpRWoeWSkUjQSLv1nRyiRzMWbobGLw1eh".parse().unwrap(), + ecash_keypair.public_key(), + expiration_date, + ); + + let good_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "JDTnyotGw3TtbohEamWNjhvGpj3tJz2C4X2Au9PrSTEx".to_string(), + }; + + let res = validate_deposit(&correct_request, good_deposit).await; + assert!(res.is_ok()) + } +} diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/ecash/dkg/client.rs similarity index 82% rename from nym-api/src/coconut/dkg/client.rs rename to nym-api/src/ecash/dkg/client.rs index f85902ba48..1471083060 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/ecash/dkg/client.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::error::CoconutError; +use crate::ecash::client::Client; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; @@ -38,19 +38,19 @@ impl DkgClient { self.inner.address().await } - pub(crate) async fn dkg_contract_address(&self) -> Result { + pub(crate) async fn dkg_contract_address(&self) -> Result { self.inner.dkg_contract_address().await } - pub(crate) async fn get_current_epoch(&self) -> Result { + pub(crate) async fn get_current_epoch(&self) -> Result { self.inner.get_current_epoch().await } - pub(crate) async fn get_contract_state(&self) -> Result { + pub(crate) async fn get_contract_state(&self) -> Result { self.inner.contract_state().await } - pub(crate) async fn group_member(&self) -> Result { + pub(crate) async fn group_member(&self) -> Result { self.inner .group_member(self.get_address().await.to_string()) .await @@ -58,13 +58,13 @@ impl DkgClient { pub(crate) async fn get_current_epoch_threshold( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_current_epoch_threshold().await } pub(crate) async fn get_self_registered_dealer_details( &self, - ) -> Result { + ) -> Result { self.inner.get_self_registered_dealer_details().await } @@ -72,14 +72,14 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_registered_dealer_details(epoch_id, dealer) .await .map(|d| d.details.is_some()) } - pub(crate) async fn get_current_dealers(&self) -> Result, CoconutError> { + pub(crate) async fn get_current_dealers(&self) -> Result, EcashError> { self.inner.get_current_dealers().await } @@ -87,7 +87,7 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_dealer_dealings_status(epoch_id, dealer) .await @@ -99,11 +99,11 @@ impl DkgClient { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> Result { + ) -> Result { self.inner .get_dealing_chunk(epoch_id, dealer, dealing_index, chunk_index) .await? - .ok_or(CoconutError::MissingDealingChunk { + .ok_or(EcashError::MissingDealingChunk { epoch_id, dealer: dealer.to_string(), dealing_index, @@ -115,7 +115,7 @@ impl DkgClient { &self, epoch_id: EpochId, address: S, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner .get_verification_key_share(epoch_id, address.into()) .await @@ -124,7 +124,7 @@ impl DkgClient { pub(crate) async fn get_verification_own_key_share( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let address = self.inner.address().await; self.get_verification_key_share(epoch_id, address).await } @@ -132,31 +132,28 @@ impl DkgClient { pub(crate) async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_verification_key_shares(epoch_id).await } - pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { + pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { let address = self.get_address().await.to_string(); self.inner.get_vote(proposal_id, address).await } - pub(crate) async fn list_proposals(&self) -> Result, CoconutError> { + pub(crate) async fn list_proposals(&self) -> Result, EcashError> { self.inner.list_proposals().await } - pub(crate) async fn get_proposal_status( - &self, - proposal_id: u64, - ) -> Result { + pub(crate) async fn get_proposal_status(&self, proposal_id: u64) -> Result { self.inner.get_proposal(proposal_id).await.map(|p| p.status) } - pub(crate) async fn advance_epoch_state(&self) -> Result<(), CoconutError> { + pub(crate) async fn advance_epoch_state(&self) -> Result<(), EcashError> { self.inner.advance_epoch_state().await } - pub(crate) async fn can_advance_epoch_state(&self) -> Result { + pub(crate) async fn can_advance_epoch_state(&self) -> Result { self.inner.can_advance_epoch_state().await } @@ -166,18 +163,18 @@ impl DkgClient { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { let res = self .inner .register_dealer(bte_key, identity_key, announce_address, resharing) .await?; let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) - .ok_or(CoconutError::NodeIndexRecoveryError { + .ok_or(EcashError::NodeIndexRecoveryError { reason: String::from("node index not found"), })? .value .parse::() - .map_err(|_| CoconutError::NodeIndexRecoveryError { + .map_err(|_| EcashError::NodeIndexRecoveryError { reason: String::from("node index could not be parsed"), })?; @@ -189,7 +186,7 @@ impl DkgClient { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner .submit_dealing_metadata(dealing_index, chunks, resharing) .await?; @@ -199,7 +196,7 @@ impl DkgClient { pub(crate) async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.submit_dealing_chunk(chunk).await?; Ok(()) } @@ -208,7 +205,7 @@ impl DkgClient { &self, share: VerificationKeyShare, resharing: bool, - ) -> Result { + ) -> Result { self.inner .submit_verification_key_share(share.clone(), resharing) .await @@ -218,14 +215,14 @@ impl DkgClient { &self, proposal_id: u64, vote_yes: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.vote_proposal(proposal_id, vote_yes, None).await } pub(crate) async fn execute_verification_key_share( &self, proposal_id: u64, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.execute_proposal(proposal_id).await } } diff --git a/nym-api/src/coconut/dkg/controller/error.rs b/nym-api/src/ecash/dkg/controller/error.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/error.rs rename to nym-api/src/ecash/dkg/controller/error.rs index e634477db6..4311ff7db5 100644 --- a/nym-api/src/coconut/dkg/controller/error.rs +++ b/nym-api/src/ecash/dkg/controller/error.rs @@ -1,12 +1,12 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::dealing::DealingGenerationError; -use crate::coconut::dkg::key_derivation::KeyDerivationError; -use crate::coconut::dkg::key_finalization::KeyFinalizationError; -use crate::coconut::dkg::key_validation::KeyValidationError; -use crate::coconut::dkg::public_key::PublicKeySubmissionError; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::dealing::DealingGenerationError; +use crate::ecash::dkg::key_derivation::KeyDerivationError; +use crate::ecash::dkg::key_finalization::KeyFinalizationError; +use crate::ecash::dkg::key_validation::KeyValidationError; +use crate::ecash::dkg::public_key::PublicKeySubmissionError; +use crate::ecash::error::EcashError; use std::path::PathBuf; use thiserror::Error; @@ -16,25 +16,25 @@ pub enum DkgError { StatePersistenceFailure { path: PathBuf, #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the current DKG epoch state: {source}")] EpochQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the epoch state status: {source}")] StateStatusQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query the CW4 group contract for the membership status: {source}")] GroupQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("this API is currently not member of the DKG group and thus can't participate in the process")] @@ -73,6 +73,6 @@ pub enum DkgError { #[error("failed to advance the DKG state: {source}")] StateAdvancementFailure { #[source] - source: CoconutError, + source: EcashError, }, } diff --git a/nym-api/src/coconut/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/keys.rs rename to nym-api/src/ecash/dkg/controller/keys.rs index ce0d16930a..6b806bf792 100644 --- a/nym-api/src/coconut/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::client::Client; +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; use crate::support::{config, nyxd}; use anyhow::{anyhow, bail, Context}; use nym_coconut_dkg_common::types::{EpochId, EpochState}; @@ -35,15 +35,32 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result .context("bte keypair load failure") } -pub(crate) fn load_coconut_keypair_if_exists( +pub(crate) fn load_ecash_keypair_if_exists( config: &config::CoconutSigner, ) -> anyhow::Result> { if !config.storage_paths.coconut_key_path.exists() { return Ok(None); } - nym_pemstore::load_key(&config.storage_paths.coconut_key_path) - .context("coconut key load failure") - .map(Some) + + // first attempt to load ecash keys directly, + // if that fails fallback to coconut keys and perform migration + if let Ok(ecash_key) = + nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + { + return Ok(Some(ecash_key)); + } + + if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( + &config.storage_paths.coconut_key_path, + ) { + let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); + nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + .context("migrated key storage failure")?; + + return Ok(Some(migrated_key)); + } + + bail!("ecash key load failure") } // the keys can be considered valid if they were generated for the current dkg epoch diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/controller/mod.rs rename to nym-api/src/ecash/dkg/controller/mod.rs index db9ce0a82c..6dd454f10d 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -1,10 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::error::DkgError; -use crate::coconut::dkg::state::{PersistentState, State}; -use crate::coconut::keys::KeyPair as CoconutKeyPair; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::error::DkgError; +use crate::ecash::dkg::state::{PersistentState, State}; +use crate::ecash::keys::KeyPair as CoconutKeyPair; use crate::nyxd; use crate::support::config; use anyhow::{bail, Result}; @@ -340,7 +340,7 @@ impl DkgController { dkg_client, coconut_key_path: Default::default(), state, - rng: crate::coconut::tests::fixtures::test_rng([1u8; 32]), + rng: crate::ecash::tests::fixtures::test_rng([1u8; 32]), polling_rate: Default::default(), } } diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs similarity index 97% rename from nym-api/src/coconut/dkg/dealing.rs rename to nym-api/src/ecash/dkg/dealing.rs index 21b5803854..bb8c17d92b 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -1,11 +1,11 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::archive_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::archive_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; @@ -39,7 +39,7 @@ impl Debug for DealingGeneration { #[derive(Debug, Error)] pub enum DealingGenerationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete dealing exchange without registering public keys")] IncompletePublicKeyRegistration, @@ -70,7 +70,7 @@ impl DkgController { spec: DealingGeneration, ) -> Result, DealingGenerationError> { let threshold = self.dkg_client.get_current_epoch_threshold().await?.ok_or( - CoconutError::UnrecoverableState { + EcashError::UnrecoverableState { reason: String::from("Threshold should have been set"), }, )?; @@ -79,7 +79,7 @@ impl DkgController { .state .registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnrecoverableState { + .ok_or(EcashError::UnrecoverableState { reason: String::from("Node index should have been set"), })?; @@ -469,14 +469,12 @@ impl DkgController { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::coconut::dkg::state::registration::KeyRejectionReason; - use crate::coconut::keys::KeyPair; - use crate::coconut::tests::fixtures::{ - dealers_fixtures, test_rng, TestingDkgControllerBuilder, - }; - use crate::coconut::tests::helpers::unchecked_decode_bte_key; - use nym_coconut::{ttp_keygen, Parameters}; + use crate::ecash::dkg::state::registration::KeyRejectionReason; + use crate::ecash::keys::KeyPair; + use crate::ecash::tests::fixtures::{dealers_fixtures, test_rng, TestingDkgControllerBuilder}; + use crate::ecash::tests::helpers::unchecked_decode_bte_key; use nym_coconut_dkg_common::types::DealerRegistrationDetails; + use nym_compact_ecash::ttp_keygen; use nym_dkg::bte::PublicKeyWithProof; #[tokio::test] @@ -619,7 +617,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch)) @@ -676,7 +674,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch - 1)) diff --git a/nym-api/src/coconut/dkg/helpers.rs b/nym-api/src/ecash/dkg/helpers.rs similarity index 94% rename from nym-api/src/coconut/dkg/helpers.rs rename to nym-api/src/ecash/dkg/helpers.rs index e36c1c04fc..d57e7962b0 100644 --- a/nym-api/src/coconut/dkg/helpers.rs +++ b/nym-api/src/ecash/dkg/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_validator_client::nyxd::AccountId; @@ -29,7 +29,7 @@ impl DkgController { pub(crate) async fn get_validation_proposals( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let dkg_contract = self.dkg_client.dkg_contract_address().await?; // FUTURE OPTIMIZATION: don't query for ALL proposals. say if we're in epoch 50, diff --git a/nym-api/src/coconut/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_derivation.rs rename to nym-api/src/ecash/dkg/key_derivation.rs index eb6c5b7214..e2ad2a7cb2 100644 --- a/nym-api/src/coconut/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -1,19 +1,19 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; use log::debug; -use nym_coconut::KeyPair as CoconutKeyPair; -use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::utils::check_vk_pairing; +use nym_compact_ecash::{ecash_group_parameters, Base58, KeyPairAuth, VerificationKeyAuth}; use nym_dkg::{ bte::{self, decrypt_share}, combine_shares, try_recover_verification_keys, Dealing, @@ -28,7 +28,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyDerivationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key derivation without dealing exchange")] IncompleteDealingExchange, @@ -75,7 +75,7 @@ impl DkgController { dealer: &Addr, epoch_receivers: &BTreeMap, raw_dealings: HashMap>, - prior_public_key: Option, + prior_public_key: Option, ) -> Result, DealerRejectionReason>, KeyDerivationError> { let threshold = self.state.threshold(epoch_id)?; @@ -155,7 +155,7 @@ impl DkgController { &self, epoch_id: EpochId, dealer: &Addr, - ) -> Result, KeyDerivationError> { + ) -> Result, KeyDerivationError> { let Some(previous_epoch) = epoch_id.checked_sub(1) else { return Err(KeyDerivationError::ZerothEpochResharing); }; @@ -176,7 +176,7 @@ impl DkgController { // since this share appears as 'verified' on the chain, it means the consensus of dealers confirmed its validity // and thus they must have been able to parse it, so the unwrap/expect here is fine Ok(Some( - VerificationKey::try_from_bs58(&share.share) + VerificationKeyAuth::try_from_bs58(&share.share) .expect("failed to deserialize VERIFIED key"), )) } @@ -424,8 +424,8 @@ impl DkgController { // SAFETY: // we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret // (i.e. the x-element) - let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets); - let derived_vk = sk.verification_key(bandwidth_credential_params()); + let sk = SecretKeyAuth::create_from_raw(derived_x.unwrap(), derived_secrets); + let derived_vk = sk.verification_key(); // make the key we derived out of the decrypted shares matches the partial key // (cryptographically there shouldn't be any reason for the mismatch, @@ -436,21 +436,21 @@ impl DkgController { .derived_partials_for(receiver_index) .ok_or(KeyDerivationError::NoSelfPartialKey { receiver_index })?; - if !check_vk_pairing(bandwidth_credential_params(), &derived_partial, &derived_vk) { + if !check_vk_pairing(ecash_group_parameters(), &derived_partial, &derived_vk) { // can't do anything, we got all dealings, we derived all keys, but somehow they don't match error!("our derived key does not match the expected partials!"); return Ok(Err(DerivationFailure::MismatchedPartialKey)); } Ok(Ok(KeyPairWithEpoch::new( - CoconutKeyPair::from_keys(sk, derived_vk), + KeyPairAuth::from_keys(sk, derived_vk), epoch_id, ))) } async fn submit_partial_verification_key( &self, - key: &VerificationKey, + key: &VerificationKeyAuth, resharing: bool, ) -> Result { fn extract_proposal_id_from_logs( @@ -558,7 +558,7 @@ impl DkgController { debug!("we have already generated keys for this epoch but failed to send them to the contract"); let proposal_id = self - .submit_partial_verification_key(keys.keys.verification_key(), resharing) + .submit_partial_verification_key(&keys.keys.verification_key(), resharing) .await?; Ok(Some(proposal_id)) } else { @@ -657,7 +657,7 @@ impl DkgController { } let proposal_id = self - .submit_partial_verification_key(coconut_keypair.keys.verification_key(), resharing) + .submit_partial_verification_key(&coconut_keypair.keys.verification_key(), resharing) .await?; self.state.set_coconut_keypair(coconut_keypair).await; @@ -669,8 +669,8 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::dkg::state::key_derivation::DealerRejectionReason; - use crate::coconut::tests::helpers::{ + use crate::ecash::dkg::state::key_derivation::DealerRejectionReason; + use crate::ecash::tests::helpers::{ exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_finalization.rs rename to nym-api/src/ecash/dkg/key_finalization.rs index 628f04daa3..9ba5ff958a 100644 --- a/nym-api/src/coconut/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyFinalizationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("our proposal for key verification is still open (or is pending) (proposal id: {proposal_id}) ")] UnresolvedProposal { proposal_id: u64 }, @@ -90,7 +90,7 @@ impl DkgController { #[cfg(test)] mod tests { use super::*; - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, validate_keys, }; diff --git a/nym-api/src/coconut/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_validation.rs rename to nym-api/src/ecash/dkg/key_validation.rs index ada4f72898..b99bacd962 100644 --- a/nym-api/src/coconut/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -1,14 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cosmwasm_std::Addr; use cw3::Vote; -use nym_coconut::{check_vk_pairing, Base58, VerificationKey}; use nym_coconut_dkg_common::types::EpochId; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::{ + ecash_group_parameters, utils::check_vk_pairing, Base58, VerificationKeyAuth, +}; use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; @@ -24,7 +25,7 @@ fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { #[derive(Debug, Error)] pub enum KeyValidationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key validation without key derivation")] IncompleteKeyDerivation, @@ -45,7 +46,7 @@ pub enum ShareRejectionReason { epoch_id: EpochId, owner: Addr, #[source] - source: nym_coconut::CoconutError, + source: nym_compact_ecash::error::CompactEcashError, }, #[error("did not derive partial keys for {owner} at index {receiver_index} for epoch {epoch_id} during the dealings exchange")] @@ -95,7 +96,7 @@ impl DkgController { }; // attempt to recover the underlying key from its bs58 representation - let recovered_key = match VerificationKey::try_from_bs58(share.share) { + let recovered_key = match VerificationKeyAuth::try_from_bs58(share.share) { Ok(key) => key, Err(source) => { return reject(ShareRejectionReason::MalformedKeyEncoding { @@ -119,7 +120,7 @@ impl DkgController { }); }; - if !check_vk_pairing(bandwidth_credential_params(), &self_derived, &recovered_key) { + if !check_vk_pairing(ecash_group_parameters(), &self_derived, &recovered_key) { return reject(ShareRejectionReason::InconsistentKeys { epoch_id, owner, @@ -258,7 +259,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/ecash/dkg/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/mod.rs rename to nym-api/src/ecash/dkg/mod.rs index 46083a2d1a..f7272cd8d2 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/ecash/dkg/mod.rs @@ -20,11 +20,11 @@ pub(crate) mod state; #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, finalize, init_chain, initialise_controller, initialise_dkg, submit_public_keys, validate_keys, }; - use nym_coconut::aggregate_verification_keys; + use nym_compact_ecash::aggregate_verification_keys; #[tokio::test] #[ignore] // expensive test diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs similarity index 96% rename from nym-api/src/coconut/dkg/public_key.rs rename to nym-api/src/ecash/dkg/public_key.rs index b2b7689923..b7f7d3e6ac 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), } impl DkgController { @@ -83,7 +83,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::tests::fixtures; + use crate::ecash::tests::fixtures; #[tokio::test] async fn submit_public_key() -> anyhow::Result<()> { diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/ecash/dkg/state/dealing_exchange.rs similarity index 94% rename from nym-api/src/coconut/dkg/state/dealing_exchange.rs rename to nym-api/src/ecash/dkg/state/dealing_exchange.rs index 02a0ff1dd2..d75b7aa808 100644 --- a/nym-api/src/coconut/dkg/state/dealing_exchange.rs +++ b/nym-api/src/ecash/dkg/state/dealing_exchange.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use super::serde_helpers::generated_dealings; -use crate::coconut::dkg::state::DkgParticipant; +use crate::ecash::dkg::state::DkgParticipant; use nym_coconut_dkg_common::types::DealingIndex; use nym_dkg::{Dealing, NodeIndex}; use serde::{Deserialize, Serialize}; diff --git a/nym-api/src/coconut/dkg/state/in_progress.rs b/nym-api/src/ecash/dkg/state/in_progress.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/in_progress.rs rename to nym-api/src/ecash/dkg/state/in_progress.rs diff --git a/nym-api/src/coconut/dkg/state/key_derivation.rs b/nym-api/src/ecash/dkg/state/key_derivation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_derivation.rs rename to nym-api/src/ecash/dkg/state/key_derivation.rs diff --git a/nym-api/src/coconut/dkg/state/key_finalization.rs b/nym-api/src/ecash/dkg/state/key_finalization.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_finalization.rs rename to nym-api/src/ecash/dkg/state/key_finalization.rs diff --git a/nym-api/src/coconut/dkg/state/key_validation.rs b/nym-api/src/ecash/dkg/state/key_validation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_validation.rs rename to nym-api/src/ecash/dkg/state/key_validation.rs diff --git a/nym-api/src/coconut/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs similarity index 76% rename from nym-api/src/coconut/dkg/state/mod.rs rename to nym-api/src/ecash/dkg/state/mod.rs index 5ef0fae275..eb1cca637e 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -1,16 +1,14 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState; -use crate::coconut::dkg::state::in_progress::InProgressState; -use crate::coconut::dkg::state::key_derivation::KeyDerivationState; -use crate::coconut::dkg::state::key_finalization::FinalizationState; -use crate::coconut::dkg::state::key_validation::ValidationState; -use crate::coconut::dkg::state::registration::{ - DkgParticipant, ParticipantState, RegistrationState, -}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; +use crate::ecash::dkg::state::dealing_exchange::DealingExchangeState; +use crate::ecash::dkg::state::in_progress::InProgressState; +use crate::ecash::dkg::state::key_derivation::KeyDerivationState; +use crate::ecash::dkg::state::key_finalization::FinalizationState; +use crate::ecash::dkg::state::key_validation::ValidationState; +use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, RegistrationState}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; @@ -60,13 +58,13 @@ impl From<&State> for PersistentState { } impl PersistentState { - pub fn save_to_file>(&self, path: P) -> Result<(), CoconutError> { + pub fn save_to_file>(&self, path: P) -> Result<(), EcashError> { debug!("persisting the dkg state"); std::fs::write(path, serde_json::to_string(self)?)?; Ok(()) } - pub fn load_from_file>(path: P) -> Result { + pub fn load_from_file>(path: P) -> Result { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } } @@ -136,7 +134,7 @@ impl State { } } - pub fn persist(&self) -> Result<(), CoconutError> { + pub fn persist(&self) -> Result<(), EcashError> { PersistentState::from(self).save_to_file(self.persistent_state_path()) } @@ -158,7 +156,7 @@ impl State { pub fn valid_epoch_receivers( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -177,7 +175,7 @@ impl State { pub fn valid_epoch_receivers_keys( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -186,157 +184,151 @@ impl State { .collect()) } - pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> { + pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, EcashError> { self.dkg_instances .get(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> { + pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, EcashError> { self.dkg_instances .get_mut(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn registration_state( - &self, - epoch_id: EpochId, - ) -> Result<&RegistrationState, CoconutError> { + pub fn registration_state(&self, epoch_id: EpochId) -> Result<&RegistrationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn registration_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut RegistrationState, CoconutError> { + ) -> Result<&mut RegistrationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, CoconutError> { + pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn in_progress_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut InProgressState, CoconutError> { + ) -> Result<&mut InProgressState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state( &self, epoch_id: EpochId, - ) -> Result<&DealingExchangeState, CoconutError> { + ) -> Result<&DealingExchangeState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut DealingExchangeState, CoconutError> { + ) -> Result<&mut DealingExchangeState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state( &self, epoch_id: EpochId, - ) -> Result<&KeyDerivationState, CoconutError> { + ) -> Result<&KeyDerivationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut KeyDerivationState, CoconutError> { + ) -> Result<&mut KeyDerivationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn key_validation_state( - &self, - epoch_id: EpochId, - ) -> Result<&ValidationState, CoconutError> { + pub fn key_validation_state(&self, epoch_id: EpochId) -> Result<&ValidationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_validation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut ValidationState, CoconutError> { + ) -> Result<&mut ValidationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state( &self, epoch_id: EpochId, - ) -> Result<&FinalizationState, CoconutError> { + ) -> Result<&FinalizationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut FinalizationState, CoconutError> { + ) -> Result<&mut FinalizationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn threshold(&self, epoch_id: EpochId) -> Result { + pub fn threshold(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .expected_threshold - .ok_or(CoconutError::UnavailableThreshold { epoch_id }) + .ok_or(EcashError::UnavailableThreshold { epoch_id }) } - pub fn assigned_index(&self, epoch_id: EpochId) -> Result { + pub fn assigned_index(&self, epoch_id: EpochId) -> Result { self.registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnavailableAssignedIndex { epoch_id }) + .ok_or(EcashError::UnavailableAssignedIndex { epoch_id }) } - pub fn receiver_index(&self, epoch_id: EpochId) -> Result { + pub fn receiver_index(&self, epoch_id: EpochId) -> Result { self.dealing_exchange_state(epoch_id)? .receiver_index - .ok_or(CoconutError::UnavailableReceiverIndex { epoch_id }) + .ok_or(EcashError::UnavailableReceiverIndex { epoch_id }) } - pub fn proposal_id(&self, epoch_id: EpochId) -> Result { + pub fn proposal_id(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .proposal_id - .ok_or(CoconutError::UnavailableProposalId { epoch_id }) + .ok_or(EcashError::UnavailableProposalId { epoch_id }) } pub fn persistent_state_path(&self) -> &Path { diff --git a/nym-api/src/coconut/dkg/state/registration.rs b/nym-api/src/ecash/dkg/state/registration.rs similarity index 98% rename from nym-api/src/coconut/dkg/state/registration.rs rename to nym-api/src/ecash/dkg/state/registration.rs index 8391cebeed..c5a7271aab 100644 --- a/nym-api/src/coconut/dkg/state/registration.rs +++ b/nym-api/src/ecash/dkg/state/registration.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::serde_helpers::bte_pk_serde; +use crate::ecash::dkg::state::serde_helpers::bte_pk_serde; use cosmwasm_std::Addr; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; diff --git a/nym-api/src/coconut/dkg/state/serde_helpers.rs b/nym-api/src/ecash/dkg/state/serde_helpers.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/serde_helpers.rs rename to nym-api/src/ecash/dkg/state/serde_helpers.rs diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/ecash/error.rs similarity index 50% rename from nym-api/src/coconut/error.rs rename to nym-api/src/ecash/error.rs index 3fa75c443f..0464c5baa4 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/ecash/error.rs @@ -3,28 +3,34 @@ use crate::node_status_api::models::NymApiStorageError; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; -use nym_credentials::coconut::bandwidth::{CredentialType, UnknownCredentialType}; use nym_crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, }; use nym_dkg::error::DkgError; -use nym_validator_client::coconut::CoconutApiError; -use nym_validator_client::nyxd::error::{NyxdError, TendermintError}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; +use okapi::openapi3::Responses; use rocket::http::{ContentType, Status}; use rocket::response::Responder; use rocket::{response, Request, Response}; +use rocket_okapi::gen::OpenApiGenerator; +use rocket_okapi::response::OpenApiResponderInner; +use rocket_okapi::util::ensure_status_code_exists; use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; use time::OffsetDateTime; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Error)] -pub enum CoconutError { +pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), @@ -37,49 +43,24 @@ pub enum CoconutError { #[error("failed to derive the admin account from the provided public key: {formatted_source}")] AdminAccountDerivationFailure { formatted_source: String }, - #[error("failed to query for the authorised freepass requester address: {source}")] - FreepassAuthorisedFreepassRequesterQueryFailure { - #[from] - source: reqwest::Error, - }, - - #[error("the provided authorised freepass requester address ({address}) is not a valid cosmos address")] - MalformedAuthorisedFreepassRequesterAddress { address: String }, - - #[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {explicit_admin:?} or {bandwidth_contract_admin:?}.")] - UnauthorisedFreePassAccount { - requester: AccountId, - explicit_admin: Option, - bandwidth_contract_admin: Option, - }, - - #[error("failed to verify signature on the provided free pass request")] - FreePassSignatureVerificationFailure, - - #[error("the provided signing nonce is invalid. the current value is: {current:?}. got {received:?} instead")] - InvalidNonce { - current: [u8; 16], - received: [u8; 16], - }, - #[error("only secp256k1 keys are supported for free pass issuance")] UnsupportedNonSecp256k1Key, - #[error("received credential request for an unknown type: {0}")] - UnknownCredentialType(#[from] UnknownCredentialType), - - #[error("the provided free pass request had an unexpected number of public attributes. got {got} but expected {expected}")] - InvalidFreePassAttributes { got: usize, expected: usize }, - - #[error("the provided free pass request had an invalid type attribute (got: '{got}')")] - InvalidFreePassTypeAttribute { got: CredentialType }, - #[error("failed to parse the free pass expiry date: {source}")] ExpiryDateParsingFailure { #[source] source: ParseIntError, }, + #[error("the provided expiration date is too late")] + ExpirationDateTooLate, + + #[error("the provided expiration date is too early")] + ExpirationDateTooEarly, + + #[error("the provided expiration date is malformed")] + MalformedExpirationDate { raw: String }, + #[error("failed to parse expiry timestamp into proper datetime: {source}")] InvalidExpiryDate { unix_timestamp: i64, @@ -87,22 +68,9 @@ pub enum CoconutError { source: ComponentRange, }, - #[error( - "the provided free pass request has too long expiry (expiry is set to on {expiry_date})" - )] - TooLongFreePass { expiry_date: OffsetDateTime }, - - #[error("the provided free pass expiry is set in the past!")] - FreePassExpiryInThePast { expiry_date: OffsetDateTime }, - #[error("the received bandwidth voucher did not contain deposit value")] MissingBandwidthValue, - #[error( - "the received bandwidth credential is not a bandwidth voucher. the encoded type is: {typ}" - )] - NotABandwidthVoucher { typ: CredentialType }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -110,7 +78,7 @@ pub enum CoconutError { }, #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -121,12 +89,6 @@ pub enum CoconutError { #[error("could not parse X25519 data: {0}")] X25519ParseError(#[from] KeyRecoveryError), - #[error("could not parse tx hash in request body: {source}")] - TxHashParseError { - #[source] - source: TendermintError, - }, - #[error("could not get transaction details for '{tx_hash}': {source}")] TxRetrievalFailure { tx_hash: String, @@ -140,39 +102,21 @@ pub enum CoconutError { #[error("validator client error: {0}")] ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), - #[error("coconut internal error: {0}")] - CoconutInternalError(#[from] nym_coconut::CoconutError), + #[error("Compact ecash internal error - {0}")] + CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), + + #[error("Account linked to this public key has been blacklisted")] + BlacklistedAccount, #[error("could not find a deposit event in the transaction provided")] DepositEventNotFound, - #[error("could not find the deposit value in the event")] - DepositValueNotFound, - #[error("could not find the deposit info in the event")] DepositInfoNotFound, - #[error("could not find the verification key in the event")] - DepositVerifKeyNotFound, - - #[error("could not find the encryption key in the event")] - DepositEncrKeyNotFound, - #[error("signature didn't verify correctly")] SignatureVerificationError(#[from] SignatureError), - #[error("inconsistent public attributes")] - InconsistentPublicAttributes, - - #[error("the provided deposit value is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositValue { request: String, on_chain: String }, - - #[error("the provided deposit info is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositInfo { request: String, on_chain: String }, - - #[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")] - DifferentPublicAttributes(String, String), - #[error("storage error: {0}")] StorageError(#[from] NymApiStorageError), @@ -182,9 +126,6 @@ pub enum CoconutError { #[error("incorrect credential proposal description: {reason}")] IncorrectProposal { reason: String }, - #[error("invalid status of credential: {status}")] - InvalidCredentialStatus { status: String }, - #[error("DKG error: {0}")] DkgError(#[from] DkgError), @@ -210,6 +151,9 @@ pub enum CoconutError { #[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")] MissingDkgState { epoch_id: EpochId }, + #[error("a new iteration of DKG is currently in progress. all ticket issuance is halted until that's completed")] + DkgInProgress, + #[error( "the node index value for epoch {epoch_id} is not available - are you sure we are a dealer?" )] @@ -231,9 +175,47 @@ pub enum CoconutError { dealing_index: DealingIndex, chunk_index: ChunkIndex, }, + + #[error("could not find ecash deposit associated with id {deposit_id}")] + NonExistentDeposit { deposit_id: DepositId }, + + #[error("the provided request digest does not match the hash of attached serial numbers")] + MismatchedRequestDigest, + + #[error("the on chain proposal digest does not match the attached request digest")] + MismatchedOnChainDigest, + + #[error("one of the attached tickets {serial_number_bs58} has not been verified before")] + TicketNotVerified { serial_number_bs58: String }, + + #[error("the provided ticket(s) redemption proposal is invalid: {source}")] + RedemptionProposalFailure { + #[from] + source: RedemptionError, + }, + + #[error("this gateway hasn't submitted any tickets for verification")] + NotTicketsProvided, + + #[error("this gateway is attempting to redeem its tickets too often. last redemption happened on {last_redemption}. the earliest next permitted redemption will be on {next_allowed}")] + TooFrequentRedemption { + last_redemption: OffsetDateTime, + next_allowed: OffsetDateTime, + }, + + #[error( + "could not sign the data for epoch {requested}. our current key is for epoch {available}" + )] + InvalidSigningKeyEpoch { + requested: EpochId, + available: EpochId, + }, + + #[error("could not obtain enough shares for aggregation. got {shares} shares whilst the threshold is {threshold}")] + InsufficientNumberOfShares { threshold: Threshold, shares: usize }, } -impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { +impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { let err_msg = self.to_string(); Response::build() @@ -243,3 +225,74 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { .ok() } } + +impl OpenApiResponderInner for EcashError { + fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + let mut responses = Responses::default(); + ensure_status_code_exists(&mut responses, 400); + Ok(responses) + } +} + +#[derive(Debug, Error)] +pub enum RedemptionError { + #[error("failed to retrieve proposal {proposal_id} from the chain")] + ProposalRetrievalFailure { proposal_id: u64 }, + + #[error( + "the proposal {proposal_id} has invalid title. got {received} but expected {}", + BATCH_REDEMPTION_PROPOSAL_TITLE + )] + InvalidProposalTitle { proposal_id: u64, received: String }, + + #[error("the proposal {proposal_id} has invalid description. got {received} but expected {expected}")] + InvalidProposalDescription { + proposal_id: u64, + received: String, + expected: String, + }, + + #[error("the proposal {proposal_id} is still pending")] + StillPending { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been executed")] + AlreadyExecuted { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been rejected")] + AlreadyRejected { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been passed")] + AlreadyPassed { proposal_id: u64 }, + + #[error("the proposal {proposal_id} was proposed by an unexpected address {received}. expected the ecash contract at {expected}")] + InvalidProposer { + proposal_id: u64, + received: String, + expected: AccountId, + }, + + #[error( + "the proposal {proposal_id} did not contain exactly a single contract execution message" + )] + TooManyMessages { proposal_id: u64 }, + + #[error("the proposal {proposal_id} did not contain the correct redemption execution message")] + InvalidMessage { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has not been made against the expected e-cash contract")] + InvalidContract { proposal_id: u64 }, + + #[error("the proposal {proposal_id} proposes redemption of tickets for gateway {proposed}, but the request has been sent by {received}")] + InvalidRedemptionTarget { + proposal_id: u64, + proposed: String, + received: String, + }, + + #[error("the proposal {proposal_id} proposes redemption of {proposed} tickets, but the request has been sent for {received} instead")] + InvalidRedemptionTicketCount { + proposal_id: u64, + proposed: u16, + received: u16, + }, +} diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs new file mode 100644 index 0000000000..6a047d63c2 --- /dev/null +++ b/nym-api/src/ecash/helpers.rs @@ -0,0 +1,134 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; +use nym_ecash_time::EcashTime; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::ops::Deref; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedExpirationDateSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedCoinIndicesSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +pub(crate) trait CredentialRequest { + fn withdrawal_request(&self) -> &WithdrawalRequest; + fn expiration_date_timestamp(&self) -> u64; + fn ecash_pubkey(&self) -> PublicKeyUser; +} + +impl CredentialRequest for BlindSignRequestBody { + fn withdrawal_request(&self) -> &WithdrawalRequest { + &self.inner_sign_request + } + + fn expiration_date_timestamp(&self) -> u64 { + self.expiration_date.ecash_unix_timestamp() + } + + fn ecash_pubkey(&self) -> PublicKeyUser { + self.ecash_pubkey.clone() + } +} + +pub(crate) fn blind_sign( + request: &C, + signing_key: &SecretKeyAuth, +) -> Result { + Ok(nym_compact_ecash::scheme::withdrawal::issue( + signing_key, + request.ecash_pubkey().clone(), + request.withdrawal_request(), + request.expiration_date_timestamp(), + )?) +} + +// a map of items that never change for given key +pub(crate) struct CachedImmutableItems { + // I wonder if there's a more efficient structure with OnceLock or OnceCell or something + inner: RwLock>, +} + +// an item that stays constant throughout given epoch +pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; + +impl Default for CachedImmutableItems { + fn default() -> Self { + CachedImmutableItems { + inner: RwLock::new(HashMap::new()), + } + } +} + +impl Deref for CachedImmutableItems { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CachedImmutableItems +where + K: Eq + Hash, +{ + pub(crate) async fn get_or_init( + &self, + key: K, + f: F, + ) -> Result, EcashError> + where + F: FnOnce() -> U, + U: Future>, + K: Clone, + { + // 1. see if we already have the item cached + let guard = self.inner.read().await; + if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) { + return Ok(item); + } + + // 2. attempt to retrieve (and cache) it + let mut write_guard = self.inner.write().await; + + // see if another task has already set the item whilst we were waiting for the lock + if write_guard.get(&key).is_some() { + let read_guard = write_guard.downgrade(); + + // SAFETY: we just checked the entry exists and we never dropped the guard + #[allow(clippy::unwrap_used)] + return Ok(RwLockReadGuard::map(read_guard, |map| { + map.get(&key).unwrap() + })); + } + + let init = f().await?; + write_guard.insert(key.clone(), init); + + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap())) + } +} diff --git a/nym-api/src/coconut/keys/mod.rs b/nym-api/src/ecash/keys/mod.rs similarity index 60% rename from nym-api/src/coconut/keys/mod.rs rename to nym-api/src/ecash/keys/mod.rs index 1902d9e180..87d01865e2 100644 --- a/nym-api/src/coconut/keys/mod.rs +++ b/nym-api/src/ecash/keys/mod.rs @@ -1,8 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_coconut::VerificationKey; +use crate::ecash::error::EcashError; use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{SecretKeyAuth, VerificationKeyAuth}; use nym_dkg::Scalar; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -19,12 +20,29 @@ pub struct KeyPair { #[derive(Debug)] pub struct KeyPairWithEpoch { - pub(crate) keys: nym_coconut::KeyPair, + pub(crate) keys: nym_compact_ecash::KeyPairAuth, + pub(crate) issued_for_epoch: EpochId, +} + +impl From for KeyPairWithEpoch { + fn from(value: LegacyCoconutKeyWithEpoch) -> Self { + let (x, ys) = value.secret_key.hazmat_to_raw(); + let sk = nym_compact_ecash::SecretKeyAuth::create_from_raw(x, ys); + + KeyPairWithEpoch { + keys: sk.into(), + issued_for_epoch: value.issued_for_epoch, + } + } +} + +pub struct LegacyCoconutKeyWithEpoch { + pub(crate) secret_key: nym_coconut::SecretKey, pub(crate) issued_for_epoch: EpochId, } impl KeyPairWithEpoch { - pub(crate) fn new(keys: nym_coconut::KeyPair, issued_for_epoch: EpochId) -> Self { + pub(crate) fn new(keys: nym_compact_ecash::KeyPairAuth, issued_for_epoch: EpochId) -> Self { KeyPairWithEpoch { keys, issued_for_epoch, @@ -64,9 +82,24 @@ impl KeyPair { } } - pub async fn verification_key(&self) -> Option> { - RwLockReadGuard::try_map(self.get().await?, |maybe_keypair| { - maybe_keypair.as_ref().map(|k| k.keys.verification_key()) + pub async fn keys(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + RwLockReadGuard::try_map(keypair_guard, |keypair| keypair.as_ref()) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn signing_key(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + + RwLockReadGuard::try_map(keypair_guard, |keypair| { + keypair.as_ref().map(|k| k.keys.secret_key()) + }) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn verification_key(&self) -> Option> { + RwLockReadGuard::try_map(self.get().await?, |maybe_keys| { + maybe_keys.as_ref().map(|k| k.keys.verification_key_ref()) }) .ok() } diff --git a/nym-api/src/ecash/keys/persistence.rs b/nym-api/src/ecash/keys/persistence.rs new file mode 100644 index 0000000000..976be0dfd6 --- /dev/null +++ b/nym-api/src/ecash/keys/persistence.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{error::CompactEcashError, scheme::keygen::SecretKeyAuth, KeyPairAuth}; +use nym_pemstore::traits::PemStorableKey; +use std::mem; + +impl PemStorableKey for KeyPairWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.keys.secret_key().to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(CompactEcashError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = SecretKeyAuth::from_bytes(&bytes[mem::size_of::()..])?; + let vk = sk.verification_key(); + + Ok(KeyPairWithEpoch { + keys: KeyPairAuth::from_keys(sk, vk), + issued_for_epoch: epoch_id, + }) + } +} + +impl PemStorableKey for LegacyCoconutKeyWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = nym_coconut::CoconutError; + + fn pem_type() -> &'static str { + "COCONUT KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.secret_key.to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(nym_coconut::CoconutError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = nym_coconut::SecretKey::from_bytes(&bytes[mem::size_of::()..])?; + + Ok(LegacyCoconutKeyWithEpoch { + secret_key: sk, + issued_for_epoch: epoch_id, + }) + } +} diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs new file mode 100644 index 0000000000..662822114f --- /dev/null +++ b/nym-api/src/ecash/mod.rs @@ -0,0 +1,47 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use okapi::openapi3::OpenApi; +use rocket::Route; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; + +pub(crate) mod api_routes; +pub(crate) mod client; +pub(crate) mod comm; +mod deposit; +pub(crate) mod dkg; +pub(crate) mod error; +pub(crate) mod helpers; +pub(crate) mod keys; +pub(crate) mod state; +pub(crate) mod storage; +#[cfg(test)] +pub(crate) mod tests; + +// equivalent of 100nym +pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; + +pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { + if enabled { + openapi_get_routes_spec![ + settings: + api_routes::partial_signing::post_blind_sign, + api_routes::partial_signing::partial_expiration_date_signatures, + api_routes::partial_signing::partial_coin_indices_signatures, + api_routes::spending::verify_ticket, + api_routes::spending::batch_redeem_tickets, + api_routes::spending::double_spending_filter_v1, + api_routes::issued::epoch_credentials, + api_routes::issued::issued_credential, + api_routes::issued::issued_credentials, + api_routes::aggregation::master_verification_key, + api_routes::aggregation::coin_indices_signatures, + api_routes::aggregation::expiration_date_signatures + ] + } else { + openapi_get_routes_spec![ + settings: + ] + } +} diff --git a/nym-api/src/ecash/state/auxiliary.rs b/nym-api/src/ecash/state/auxiliary.rs new file mode 100644 index 0000000000..d96713a336 --- /dev/null +++ b/nym-api/src/ecash/state/auxiliary.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::error::{EcashError, Result}; +use crate::support::storage::NymApiStorage; +use nym_coconut_dkg_common::types::EpochId; + +pub(crate) struct AuxiliaryEcashState { + pub(crate) client: Box, + pub(crate) comm_channel: Box, + pub(crate) storage: NymApiStorage, +} + +impl AuxiliaryEcashState { + pub(crate) fn new(client: C, comm_channel: D, storage: NymApiStorage) -> Self + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + AuxiliaryEcashState { + client: Box::new(client), + comm_channel: Box::new(comm_channel), + storage, + } + } + + pub(crate) async fn current_epoch(&self) -> Result { + self.comm_channel.current_epoch().await + } + + pub(crate) async fn ensure_not_blacklisted(&self, encoded_pubkey_bs58: &str) -> Result<()> { + let res = self + .client + .get_blacklisted_account(encoded_pubkey_bs58.to_string()) + .await?; + + if res.account.is_some() { + return Err(EcashError::BlacklistedAccount); + } + + Ok(()) + } +} diff --git a/nym-api/src/ecash/state/bloom.rs b/nym-api/src/ecash/state/bloom.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/ecash/state/bloom.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/ecash/state/global.rs b/nym-api/src/ecash/state/global.rs new file mode 100644 index 0000000000..24d1ff00ff --- /dev/null +++ b/nym-api/src/ecash/state/global.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use nym_compact_ecash::VerificationKeyAuth; +use nym_validator_client::nyxd::AccountId; +use time::Date; + +pub(crate) struct GlobalEcachState { + pub(crate) contract_address: AccountId, + + pub(crate) master_verification_key: CachedImmutableEpochItem, + + // maybe we should use arrays here instead? + pub(crate) coin_index_signatures: CachedImmutableEpochItem, + + pub(crate) expiration_date_signatures: + CachedImmutableItems, +} + +impl GlobalEcachState { + pub(crate) fn new(contract_address: AccountId) -> Self { + GlobalEcachState { + contract_address, + master_verification_key: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + } + } +} diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs new file mode 100644 index 0000000000..ffcab88644 --- /dev/null +++ b/nym-api/src/ecash/state/helpers.rs @@ -0,0 +1,160 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::local::TicketDoubleSpendingFilter; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use futures::{stream, StreamExt}; +use nym_compact_ecash::constants; +use nym_config::defaults::BloomfilterParameters; +use nym_dkg::Threshold; +use nym_ecash_double_spending::{DoubleSpendingFilter, DoubleSpendingFilterBuilder}; +use nym_ecash_time::{cred_exp_date, ecash_today}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use time::ext::NumericalDuration; +use time::Date; +use tokio::sync::Mutex; + +// attempt to completely rebuild the bloomfilter data for given day +async fn try_rebuild_today_bloomfilter( + today: Date, + params: BloomfilterParameters, + storage: &NymApiStorage, +) -> Result { + log::info!("rebuilding bloomfilter for {today}"); + + let tickets = storage.get_all_spent_tickets_on(today).await?; + log::debug!( + "there are {} tickets to insert into the filter", + tickets.len() + ); + + let mut filter = DoubleSpendingFilter::new_empty(params); + for ticket in tickets { + filter.set(&ticket.serial_number) + } + Ok(filter) +} + +pub(crate) async fn prepare_partial_bloomfilter_builder( + storage: &NymApiStorage, + params: BloomfilterParameters, + params_id: i64, + start: Date, + days: i64, +) -> Result { + log::info!( + "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" + ); + + let mut filter_builder = DoubleSpendingFilter::builder(params); + for i in 0..days { + let date = start - i.days(); + let Some(bitmap) = storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + else { + log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least 30 days since 'ecash'-based zk-nyms were introduced this is expected)"); + continue; + }; + if !filter_builder.add_bytes(&bitmap) { + log::error!( + "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" + ); + } + } + Ok(filter_builder) +} + +pub(super) async fn try_rebuild_bloomfilter( + storage: &NymApiStorage, +) -> Result { + log::info!("attempting to rebuild the double spending bloomfilter..."); + let today = ecash_today().date(); + + let (params_id, params) = storage.get_double_spending_filter_params().await?; + log::info!("will use the following parameters: {params:?}"); + + // we're never going to have persisted data for 'today'. we need to rebuild it from scratch + let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; + + log::info!("attempting to rebuild the global filter"); + let mut global_filter = prepare_partial_bloomfilter_builder( + storage, + params, + params_id, + today.previous_day().unwrap(), + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await?; + + if !global_filter.add_bytes(&today_filter.dump_bitmap()) { + log::error!( + "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" + ); + } + + Ok(TicketDoubleSpendingFilter::new( + today, + params_id, + global_filter.build(), + today_filter, + )) +} + +pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), EcashError> { + let today = ecash_today(); + + if expiration_date < today.date() { + // what's the point of signatures with expiration in the past? + return Err(EcashError::ExpirationDateTooEarly); + } + + // SAFETY: we're nowhere near MAX date + #[allow(clippy::unwrap_used)] + if expiration_date > cred_exp_date().date().next_day().unwrap() { + // don't allow issuing signatures too far in advance (1 day beyond current value is fine) + return Err(EcashError::ExpirationDateTooLate); + } + + Ok(()) +} + +pub(crate) async fn query_all_threshold_apis( + all_apis: Vec, + threshold: Threshold, + f: F, +) -> Result, EcashError> +where + F: Fn(EcashApiClient) -> U, + U: Future>, +{ + let shares = Mutex::new(Vec::with_capacity(all_apis.len())); + + stream::iter(all_apis) + .for_each_concurrent(8, |api| async { + // can't be bothered to restructure the code to appease the borrow checker properly, + // so just assign this to a variable + let disp = api.to_string(); + match f(api).await { + Ok(partial_share) => shares.lock().await.push(partial_share), + Err(err) => { + log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + } + } + }) + .await; + + let shares = shares.into_inner(); + + if shares.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: shares.len(), + }); + } + + Ok(shares) +} diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs new file mode 100644 index 0000000000..3f97c98a53 --- /dev/null +++ b/nym-api/src/ecash/state/local.rs @@ -0,0 +1,107 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use crate::ecash::keys::KeyPair; +use nym_config::defaults::BloomfilterParameters; +use nym_crypto::asymmetric::identity; +use nym_ecash_double_spending::DoubleSpendingFilter; +use std::sync::Arc; +use time::Date; +use tokio::sync::RwLock; + +pub(crate) struct TicketDoubleSpendingFilter { + built_on: Date, + params_id: i64, + + today_filter: DoubleSpendingFilter, + global_filter: DoubleSpendingFilter, +} + +impl TicketDoubleSpendingFilter { + pub(crate) fn new( + built_on: Date, + params_id: i64, + global_filter: DoubleSpendingFilter, + today_filter: DoubleSpendingFilter, + ) -> TicketDoubleSpendingFilter { + TicketDoubleSpendingFilter { + built_on, + params_id, + today_filter, + global_filter, + } + } + + pub(crate) fn built_on(&self) -> Date { + self.built_on + } + + pub(crate) fn params(&self) -> BloomfilterParameters { + self.today_filter.params() + } + + pub(crate) fn params_id(&self) -> i64 { + self.params_id + } + + pub(crate) fn check(&self, sn: &Vec) -> bool { + self.global_filter.check(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_both(&mut self, sn: &Vec) -> bool { + self.today_filter.set(sn); + self.insert_global_only(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_global_only(&mut self, sn: &Vec) -> bool { + let existed = self.global_filter.check(sn); + self.global_filter.set(sn); + existed + } + + pub(crate) fn export_today_bitmap(&self) -> Vec { + self.today_filter.dump_bitmap() + } + + pub(crate) fn export_global_bitmap(&self) -> Vec { + self.global_filter.dump_bitmap() + } + + pub(crate) fn advance_day(&mut self, date: Date, new_global: DoubleSpendingFilter) { + self.built_on = date; + self.global_filter = new_global; + self.today_filter.reset(); + } +} + +pub(crate) struct LocalEcashState { + pub(crate) ecash_keypair: KeyPair, + pub(crate) identity_keypair: identity::KeyPair, + + pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, + pub(crate) partial_expiration_date_signatures: + CachedImmutableItems, + pub(crate) double_spending_filter: Arc>, +} + +impl LocalEcashState { + pub(crate) fn new( + ecash_keypair: KeyPair, + identity_keypair: identity::KeyPair, + double_spending_filter: TicketDoubleSpendingFilter, + ) -> Self { + LocalEcashState { + ecash_keypair, + identity_keypair, + partial_coin_index_signatures: Default::default(), + partial_expiration_date_signatures: Default::default(), + double_spending_filter: Arc::new(RwLock::new(double_spending_filter)), + } + } +} diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs new file mode 100644 index 0000000000..b790e9a84c --- /dev/null +++ b/nym-api/src/ecash/state/mod.rs @@ -0,0 +1,863 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::deposit::validate_deposit; +use crate::ecash::error::{EcashError, RedemptionError, Result}; +use crate::ecash::helpers::{IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures}; +use crate::ecash::keys::KeyPair; +use crate::ecash::state::auxiliary::AuxiliaryEcashState; +use crate::ecash::state::global::GlobalEcachState; +use crate::ecash::state::helpers::{ + ensure_sane_expiration_date, prepare_partial_bloomfilter_builder, query_all_threshold_apis, + try_rebuild_bloomfilter, +}; +use crate::ecash::state::local::LocalEcashState; +use crate::ecash::storage::models::{SerialNumberWrapper, TicketProvider}; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use cw3::Status; +use nym_api_requests::ecash::helpers::issued_credential_plaintext; +use nym_api_requests::ecash::models::BatchRedeemTicketsBody; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, sign_coin_indices, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::{ + constants, scheme::expiration_date_signatures::sign_expiration_date, BlindedSignature, + SecretKeyAuth, VerificationKeyAuth, +}; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::{aggregate_verification_keys, CredentialSpendingData}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::{Deposit, DepositId}; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_ecash_time::cred_exp_date; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use time::ext::NumericalDuration; +use time::{Date, OffsetDateTime}; +use tokio::sync::RwLockReadGuard; +use tokio::time::Instant; + +pub(crate) mod auxiliary; +pub(crate) mod bloom; +pub(crate) mod global; +mod helpers; +pub(crate) mod local; + +pub struct EcashState { + // state global to the system, like aggregated keys, addresses, etc. + pub(crate) global: GlobalEcachState, + + // state local to the api instance, like partial signatures, keys, etc. + pub(crate) local: LocalEcashState, + + // auxiliary data used for resolving requests like clients, storage, etc. + pub(crate) aux: AuxiliaryEcashState, +} + +impl EcashState { + pub(crate) async fn new( + contract_address: AccountId, + client: C, + identity_keypair: identity::KeyPair, + key_pair: KeyPair, + comm_channel: D, + storage: NymApiStorage, + ) -> Result + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + let double_spending_filter = try_rebuild_bloomfilter(&storage).await?; + + Ok(Self { + global: GlobalEcachState::new(contract_address), + local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter), + aux: AuxiliaryEcashState::new(client, comm_channel, storage), + }) + } + + pub(crate) async fn ecash_signing_key(&self) -> Result> { + self.local.ecash_keypair.signing_key().await + } + + #[allow(dead_code)] + pub(crate) async fn current_master_verification_key( + &self, + ) -> Result> { + self.master_verification_key(None).await + } + + pub(crate) async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .aux + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored); + } + + // 2. perform actual aggregation + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: all_apis.len(), + }); + } + + let master_key = aggregate_verification_keys(&all_apis)?; + + // 3. save the key in the storage for when we reboot + self.aux + .storage + .insert_master_verification_key(epoch_id, &master_key) + .await?; + + Ok(master_key) + }) + .await + } + + pub(crate) async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: master_sigs, + }); + } + + log::info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + // let mut shares = Mutex::new(Vec::with_capacity(all_apis.len())); + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_coin_index_signatures(Some(epoch_id)) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures + }; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_coin_index_signatures(epoch_id, &aggregated) + .await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }) + }) + .await + } + + pub(crate) async fn partial_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.local + .partial_coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: partial_sigs, + }); + } + + + // 2. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + if signing_keys.issued_for_epoch != epoch_id { + // TODO: this should get handled at some point, + // because if it was a past epoch we **do** have those keys. + // they're just archived + + log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + return Err(EcashError::InvalidSigningKeyEpoch { + requested: epoch_id, + available: signing_keys.issued_for_epoch, + }) + } + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let signatures = sign_coin_indices( + nym_compact_ecash::ecash_parameters(), + &master_vk, + signing_keys.keys.secret_key(), + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux.storage.insert_partial_coin_index_signatures(epoch_id, &signatures).await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures, + }) + }) + .await + } + + pub(crate) async fn master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.global + .expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_expiration_date_signatures(expiration_date) + .await? + { + return Ok(master_sigs); + } + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.aux.comm_channel.current_epoch().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_expiration_date_signatures(expiration_date) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_expiration_date_signatures(Some(expiration_date)) + .await? + .signatures + }; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.local + .partial_expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_expiration_date_signatures(expiration_date) + .await? + { + return Ok(partial_sigs); + } + + // 3. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + + let signatures = sign_expiration_date( + signing_keys.keys.secret_key(), + expiration_date.ecash_unix_timestamp(), + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id: signing_keys.issued_for_epoch, + signatures, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_partial_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn ensure_dkg_not_in_progress(&self) -> Result<()> { + if self.aux.comm_channel.dkg_in_progress().await? { + return Err(EcashError::DkgInProgress); + } + Ok(()) + } + + /// Check if this nym-api has already issued a credential for the provided deposit id. + /// If so, return it. + pub async fn already_issued(&self, deposit_id: DepositId) -> Result> { + self.aux + .storage + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await? + .map(|cred| cred.try_into()) + .transpose() + } + + pub async fn get_deposit(&self, deposit_id: DepositId) -> Result { + self.aux + .client + .get_deposit(deposit_id) + .await? + .deposit + .ok_or(EcashError::NonExistentDeposit { deposit_id }) + } + + pub async fn validate_request( + &self, + request: &BlindSignRequestBody, + deposit: Deposit, + ) -> Result<()> { + validate_deposit(request, deposit).await + } + + pub(crate) async fn validate_redemption_proposal( + &self, + request: &BatchRedeemTicketsBody, + ) -> std::result::Result<(), RedemptionError> { + let proposal_id = request.proposal_id; + + // retrieve the proposal itself + let mut proposal = self + .aux + .client + .get_proposal(proposal_id) + .await + .map_err(|_| RedemptionError::ProposalRetrievalFailure { proposal_id })?; + + if proposal.title != BATCH_REDEMPTION_PROPOSAL_TITLE { + return Err(RedemptionError::InvalidProposalTitle { + proposal_id, + received: proposal.title, + }); + } + + // make sure you can still vote on it + match proposal.status { + Status::Pending => return Err(RedemptionError::StillPending { proposal_id }), + Status::Open => {} + Status::Rejected => return Err(RedemptionError::AlreadyRejected { proposal_id }), + + // TODO: need to double check with the multisig whether it wouldn't always be thrown on threshold + // i.e. whether after the 2+/3 vote, the remaining 1-/3 would return this error + Status::Passed => return Err(RedemptionError::AlreadyPassed { proposal_id }), + Status::Executed => return Err(RedemptionError::AlreadyExecuted { proposal_id }), + } + + let encoded_digest = bs58::encode(&request.digest).into_string(); + + // check if the description matches the expected digest + if encoded_digest != proposal.description { + return Err(RedemptionError::InvalidProposalDescription { + proposal_id, + received: proposal.description, + expected: encoded_digest, + }); + } + + // check if it was actually created by the ecash contract + if proposal.proposer != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidProposer { + proposal_id, + received: proposal.proposer.into_string(), + expected: self.global.contract_address.clone(), + }); + } + + // check if contains exactly the content we expect, + // i.e. single `RedeemTickets` message with no funds, etc. + if proposal.msgs.len() != 1 { + return Err(RedemptionError::TooManyMessages { proposal_id }); + } + + // SAFETY: we just checked we have exactly one message + #[allow(clippy::unwrap_used)] + let msg = proposal.msgs.pop().unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = msg + else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if !funds.is_empty() { + return Err(RedemptionError::InvalidMessage { proposal_id }); + } + + if contract_addr != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidContract { proposal_id }); + } + + let Ok(ExecuteMsg::RedeemTickets { n, gw }) = from_binary(&msg) else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if gw != request.gateway_cosmos_addr.as_ref() { + return Err(RedemptionError::InvalidRedemptionTarget { + proposal_id, + proposed: gw, + received: request.gateway_cosmos_addr.to_string(), + }); + } + + if n as usize != request.included_serial_numbers.len() { + return Err(RedemptionError::InvalidRedemptionTicketCount { + proposal_id, + proposed: n, + received: request.included_serial_numbers.len() as u16, + }); + } + + Ok(()) + } + + pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { + //SW NOTE: What to do if this fails + if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { + log::debug!("failed to vote on proposal {proposal_id}: {err}"); + } + + Ok(()) + } + + // pub(crate) async fn blacklist(&self, public_key: String) { + // let client = self.aux.client.clone(); + // tokio::spawn(async move { + // //SW TODO error handling with one log at the end + // let response = client.propose_for_blacklist(public_key.clone()).await?; + // let proposal_id = find_proposal_id(&response.logs)?; + // + // let proposal = client.get_proposal(proposal_id).await?; + // if proposal.status == Status::Open { + // if public_key != proposal.description { + // return Err(EcashError::IncorrectProposal { + // reason: String::from("incorrect publickey in description"), + // }); + // } + // let ret = client.vote_proposal(proposal_id, true, None).await; + // + // accepted_vote_err(ret)?; + // + // if let Ok(proposal) = client.get_proposal(proposal_id).await { + // if proposal.status == Status::Passed { + // client.execute_proposal(proposal_id).await? + // } + // } + // } + // Ok(()) + // }); + // } + + pub(crate) async fn sign_and_store_credential( + &self, + current_epoch: EpochId, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result { + let encoded_commitments = request_body.encode_commitments(); + + let plaintext = issued_credential_plaintext( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + &encoded_commitments, + request_body.expiration_date, + ); + + let signature = self.local.identity_keypair.private_key().sign(plaintext); + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .aux + .storage + .store_issued_credential( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + signature, + encoded_commitments, + request_body.expiration_date, + ) + .await?; + + Ok(credential_id) + } + + pub async fn store_issued_credential( + &self, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result<()> { + let current_epoch = self.aux.current_epoch().await?; + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .sign_and_store_credential(current_epoch, request_body, blinded_signature) + .await?; + self.aux + .storage + .update_epoch_credentials_entry(current_epoch, credential_id) + .await?; + debug!("the stored credential has id {credential_id}"); + + Ok(()) + } + + pub async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<()> { + self.aux + .storage + .store_verified_ticket(ticket_data, gateway_addr) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result> { + self.aux + .storage + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + pub async fn get_redeemable_tickets( + &self, + provider_info: TicketProvider, + ) -> Result> { + let since = provider_info + .last_batch_verification + .unwrap_or(OffsetDateTime::UNIX_EPOCH); + + self.aux + .storage + .get_verified_tickets_since(provider_info.id, since) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_data_by_serial_number( + &self, + serial_number: &[u8], + ) -> Result> { + self.aux + .storage + .get_credential_data(serial_number) + .await + .map_err(Into::into) + } + + pub async fn check_bloomfilter(&self, serial_number: &Vec) -> bool { + self.local + .double_spending_filter + .read() + .await + .check(serial_number) + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + params: BloomfilterParameters, + sn: &Vec, + ) -> Result<(), EcashError> { + let mut filter = match self + .aux + .storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + { + Some(bitmap) => DoubleSpendingFilter::from_bytes(params, &bitmap), + None => { + warn!("no existing partial bloomfilter for {date}"); + DoubleSpendingFilter::new_empty(params) + } + }; + filter.set(sn); + let updated_bitmap = filter.dump_bitmap(); + self.aux + .storage + .update_archived_partial_bloomfilter(date, &updated_bitmap) + .await?; + + Ok(()) + } + + /// Attempt to insert the provided serial number into the bloomfilter. + /// Furthermore, attempt to rotate the filter if we have advanced into a next day. + pub async fn update_bloomfilter( + &self, + serial_number: &Vec, + spending_date: Date, + today: Date, + ) -> Result { + let mut guard = self.local.double_spending_filter.write().await; + + let filter_date = guard.built_on(); + let yesterday = today.previous_day().unwrap(); + + let params_id = guard.params_id(); + let params = guard.params(); + + // if the filter is up-to-date, we just insert the entry and call it a day + if filter_date == today { + if spending_date == today { + return Ok(guard.insert_both(serial_number)); + } + // sanity check because this should NEVER happen, + // but when it inevitably does, we don't want to crash + if spending_date != yesterday { + log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + } + + // this shouldn't be happening too often, so it's fine to interact with the storage + warn!("updating archived partial bloomfilter for {spending_date}. those logs have to be closely controlled to make sure they're not too frequent"); + self.update_archived_partial_bloomfilter( + spending_date, + params_id, + params, + serial_number, + ) + .await?; + + return Ok(guard.insert_global_only(serial_number)); + } + + log::info!("we need to advance our bloomfilter"); + let previous_bitmap = guard.export_today_bitmap(); + + // archive the BF for today's date + self.aux + .storage + .insert_partial_bloomfilter(filter_date, params_id, &previous_bitmap) + .await?; + + let new_global_filter = if filter_date == yesterday { + // normal case when we update filter daily + let two_days_ago = yesterday.previous_day().unwrap(); + let mut filter_builder = prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + two_days_ago, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 2, + ) + .await?; + // add the bitmap from 'old today', i.e. yesterday + // (we have it on hand so no point in retrieving it from storage) + filter_builder.add_bytes(&previous_bitmap); + filter_builder.build() + } else { + // initial deployment case when we don't even get tickets daily + prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + yesterday, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await? + .build() + }; + + guard.advance_day(today, new_global_filter); + + // drop guard so other tasks could read the filter already whilst we clean-up the storage + let res = if spending_date == today { + Ok(guard.insert_both(serial_number)) + } else { + Ok(guard.insert_global_only(serial_number)) + }; + drop(guard); + + let cutoff = cred_exp_date().ecash_date(); + + // sanity check: + assert_eq!( + cutoff, + today + (constants::CRED_VALIDITY_PERIOD_DAYS as i64).days() + ); + + // remove the data we no longer need to hold, i.e. partial bloomfilters beyond max credential validity + // and the ticket data for those + self.aux + .storage + .remove_old_partial_bloomfilters(cutoff) + .await?; + self.aux + .storage + .remove_expired_verified_tickets(cutoff) + .await?; + + res + } + + pub async fn export_bloomfilter(&self) -> Vec { + let export_start = Instant::now(); + let bytes = self + .local + .double_spending_filter + .read() + .await + .export_global_bitmap(); + + let taken = export_start.elapsed(); + match taken.as_millis() { + // at that point it should somehow be cached; especially **IF** we have more reads than writes + ms if ms > 100 => error!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 50 => warn!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 10 => info!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 2 => debug!("exporting the bloomfilter took {ms}ms to complete"), + ms => trace!("exporting the bloomfilter took {ms}ms to complete"), + } + + bytes + } +} diff --git a/nym-api/src/ecash/storage/helpers.rs b/nym-api/src/ecash/storage/helpers.rs new file mode 100644 index 0000000000..c0be8a9183 --- /dev/null +++ b/nym-api/src/ecash/storage/helpers.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::NymApiStorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +pub(crate) fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs new file mode 100644 index 0000000000..4689ffa10a --- /dev/null +++ b/nym-api/src/ecash/storage/manager.rs @@ -0,0 +1,809 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::storage::models::{ + EpochCredentials, IssuedCredential, RawExpirationDateSignatures, SerialNumberWrapper, + StoredBloomfilterParams, TicketProvider, VerifiedTicket, +}; +use crate::support::storage::manager::StorageManager; +use nym_coconut_dkg_common::types::EpochId; +use nym_ecash_contract_common::deposit::DepositId; +use time::{Date, OffsetDateTime}; + +#[async_trait] +pub trait EcashStorageManagerExt { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error>; + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; + + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error>; + + /// Store the provided issued credential information and return its (database) id. + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + expiration_date: Date, + ) -> Result; + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error>; + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error>; + + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error>; + + async fn get_ticket(&self, serial_number: &[u8]) + -> Result, sqlx::Error>; + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error>; + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error>; + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result; + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error>; +} + +#[async_trait] +impl EcashStorageManagerExt for StorageManager { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + -1, + 0 + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + // the logic in this function can be summarised with: + // 1. get the current EpochCredentials for this epoch + // 2. if it exists -> increment `total_issued` + // 3. it has invalid (i.e. -1) `start_id` set it to the provided value + // 4. if it didn't exist, create new entry + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + // make the atomic transaction in case other tasks are attempting to use the pool + let mut tx = self.connection_pool.begin().await?; + + if let Some(existing) = sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&mut tx) + .await? + { + // the entry has existed before -> update it + if existing.total_issued == 0 { + // no credentials has been issued -> we have to set the `start_id` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = 1, start_id = ? + WHERE epoch_id = ? + "#, + credential_id, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } else { + // we have issued credentials in this epoch before -> just increment `total_issued` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = total_issued + 1 + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } + } else { + // the entry has never been created -> probably some race condition; create it instead + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + credential_id, + 1 + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE id = ? + "#, + credential_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE deposit_id = ? + "#, + deposit_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Store the provided issued credential information and return its (database) id. + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + expiration_date: Date, + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO issued_credential + (epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date) + VALUES + (?, ?, ?, ?, ?, ?) + "#, + epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date + ).execute(&self.connection_pool).await?.last_insert_rowid(); + + Ok(row_id) + } + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error> { + // that sucks : ( + // https://stackoverflow.com/a/70032524 + let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); + let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); + let mut query = sqlx::query_as(&query_str); + for id in credential_ids { + query = query.bind(id) + } + + query.fetch_all(&self.connection_pool).await + } + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE id > ? + ORDER BY id + LIMIT ? + "#, + start_after, + limit + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result { + let id = sqlx::query!( + "INSERT INTO ticket_providers(gateway_address) VALUES (?)", + gateway_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM ticket_providers WHERE gateway_address = ?") + .bind(gateway_address) + .fetch_optional(&self.connection_pool) + .await + } + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO verified_tickets(ticket_data, serial_number, spending_date, verified_at, gateway_id) + VALUES (?, ?, ?, ?, ?) + "#, + ticket_data, + serial_number, + spending_date, + verified_at, + provider_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + async fn get_ticket( + &self, + serial_number: &[u8], + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM verified_tickets WHERE serial_number = ?") + .bind(serial_number) + .fetch_optional(&self.connection_pool) + .await + } + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE gateway_id = ? + AND verified_at > ? + ORDER BY verified_at ASC + LIMIT 65535 + "#, + provider_id, + since + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE spending_date = ? + "#, + date + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM partial_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM global_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM partial_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM global_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO bloomfilter_parameters(num_hashes, bitmap_size,sip0_key0, sip0_key1, sip1_key0, sip1_key1) + VALUES (?, ?, ?, ?, ?, ?) + "#, + num_hashes, + bitmap_size, + sip0_key0, + sip0_key1, + sip1_key0, + sip1_key1 + ).execute(&self.connection_pool).await?.last_insert_rowid(); + Ok(row_id) + } + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM bloomfilter_parameters ORDER BY id DESC LIMIT 1") + .fetch_optional(&self.connection_pool) + .await + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE partial_bloomfilter SET bitmap = ? WHERE date = ?", + new_bitmap, + date + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT bitmap FROM partial_bloomfilter WHERE date = ? AND parameters = ?", + date, + params_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.bitmap)) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_bloomfilter(date, parameters, bitmap) VALUES (?, ?, ?)", + date, + params_id, + bitmap + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM partial_bloomfilter WHERE date > ?", cutoff) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE spending_date > ?", + cutoff + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs new file mode 100644 index 0000000000..cc7621b1ae --- /dev/null +++ b/nym-api/src/ecash/storage/mod.rs @@ -0,0 +1,635 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::IssuedExpirationDateSignatures; +use crate::ecash::storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::ecash::storage::manager::EcashStorageManagerExt; +use crate::ecash::storage::models::{ + join_attributes, EpochCredentials, IssuedCredential, SerialNumberWrapper, TicketProvider, +}; +use crate::node_status_api::models::NymApiStorageError; +use crate::support::storage::NymApiStorage; +use nym_api_requests::ecash::models::Pagination; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::CredentialSpendingData; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::AccountId; +use time::{Date, OffsetDateTime}; + +mod helpers; +pub(crate) mod manager; +pub(crate) mod models; + +const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; + +#[async_trait] +pub trait EcashStorageExt { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + + #[allow(dead_code)] + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError>; + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError>; + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError>; + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError>; + + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + expiration_date: Date, + ) -> Result; + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError>; + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError>; + // + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError>; + // + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError>; + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError>; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError>; + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError>; + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError>; + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; +} + +#[async_trait] +impl EcashStorageExt for NymApiStorage { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError> { + match self + .manager + .get_latest_double_spending_filter_params() + .await? + { + Some(raw) => Ok((raw.id, (&raw).try_into()?)), + None => { + let default = BloomfilterParameters::default_ecash(); + info!("using default bloomfilter parameters: {default:?}"); + let id = self + .manager + .insert_double_spending_filter_params( + default.num_hashes, + default.bitmap_size as u32, + &default.sip_keys[0].0.to_be_bytes(), + &default.sip_keys[0].1.to_be_bytes(), + &default.sip_keys[1].0.to_be_bytes(), + &default.sip_keys[1].1.to_be_bytes(), + ) + .await?; + Ok((id, default)) + } + } + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_archived_partial_bloomfilter(date, new_bitmap) + .await?) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError> { + Ok(self + .manager + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await?) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_partial_bloomfilter(date, params_id, bitmap) + .await?) + } + + async fn remove_old_partial_bloomfilters( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_old_partial_bloomfilters(cutoff).await?) + } + + async fn remove_expired_verified_tickets( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_expired_verified_tickets(cutoff).await?) + } + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_epoch_credentials(epoch_id).await?) + } + + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .create_epoch_credentials_entry(epoch_id) + .await?) + } + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_epoch_credentials_entry(epoch_id, credential_id) + .await?) + } + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credential(credential_id).await?) + } + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await?) + } + + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + expiration_date: Date, + ) -> Result { + Ok(self + .manager + .store_issued_credential( + epoch_id, + deposit_id, + partial_credential.to_bs58(), + signature.to_base58_string(), + join_attributes(private_commitments), + expiration_date, + ) + .await?) + } + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credentials(credential_ids).await?) + } + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError> { + // rows start at 1 + let start_after = pagination.last_key.unwrap_or(0); + let limit = match pagination.limit { + Some(v) => { + if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { + DEFAULT_CREDENTIALS_PAGE_LIMIT + } else { + v + } + } + None => DEFAULT_CREDENTIALS_PAGE_LIMIT, + }; + + Ok(self + .manager + .get_issued_credentials_paged(start_after, limit) + .await?) + } + + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError> { + // self.manager + // .insert_credential( + // credential.to_bs58(), + // serial_number_bs58, + // gateway_addr.to_string(), + // proposal_id as i64, + // ) + // .await + // .map_err(|err| err.into()) + // } + + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError> { + let ticket = self.manager.get_ticket(serial_number).await?; + ticket + .map(|ticket| { + CredentialSpendingData::try_from_bytes(&ticket.ticket_data).map_err(|_| { + NymApiStorageError::DatabaseInconsistency { + reason: "impossible to deserialize verified ticket".to_string(), + } + }) + }) + .transpose() + } + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError> { + let provider_id = self + .get_or_create_ticket_provider_with_id(gateway_addr.as_ref()) + .await?; + + let now = OffsetDateTime::now_utc(); + + let ticket_bytes = ticket_data.to_bytes(); + let encoded_serial_number = ticket_data.encoded_serial_number(); + self.manager + .insert_verified_ticket( + provider_id, + ticket_data.spend_date, + now, + ticket_bytes, + encoded_serial_number, + ) + .await + .map_err(Into::into) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError> { + self.manager + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError> { + self.manager + .get_provider_ticket_serial_numbers(provider_id, since) + .await + .map_err(Into::into) + } + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError> { + self.manager + .get_spent_tickets_on(date) + .await + .map_err(Into::into) + } + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result { + if let Some(provider) = self.get_ticket_provider(gateway_address).await? { + Ok(provider.id) + } else { + Ok(self.manager.insert_ticket_provider(gateway_address).await?) + } + } + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/models.rs b/nym-api/src/ecash/storage/models.rs new file mode 100644 index 0000000000..c73d759b4e --- /dev/null +++ b/nym-api/src/ecash/storage/models.rs @@ -0,0 +1,198 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::node_status_api::models::NymApiStorageError; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, + IssuedCredentialBody as ApiIssuedCredentialInner, +}; +use nym_api_requests::ecash::BlindedSignatureResponse; +use nym_compact_ecash::{Base58, BlindedSignature}; +use nym_config::defaults::BloomfilterParameters; +use nym_ecash_contract_common::deposit::DepositId; +use sqlx::FromRow; +use std::fmt::Display; +use std::ops::Deref; +use time::{Date, OffsetDateTime}; + +pub struct EpochCredentials { + pub epoch_id: u32, + pub start_id: i64, + pub total_issued: u32, +} + +impl From for EpochCredentialsResponse { + fn from(value: EpochCredentials) -> Self { + let first_epoch_credential_id = if value.start_id == -1 { + None + } else { + Some(value.start_id) + }; + + EpochCredentialsResponse { + epoch_id: value.epoch_id as u64, + first_epoch_credential_id, + total_issued: value.total_issued, + } + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct TicketProvider { + pub(crate) id: i64, + pub(crate) gateway_address: String, + pub(crate) last_batch_verification: Option, +} + +#[derive(FromRow)] +pub struct SerialNumberWrapper { + pub serial_number: Vec, +} + +impl Deref for SerialNumberWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.serial_number + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct VerifiedTicket { + pub(crate) id: i64, + pub(crate) ticket_data: Vec, + pub(crate) serial_number: Vec, + pub(crate) spending_date: Date, + pub(crate) verified_at: OffsetDateTime, + pub(crate) gateway_id: i64, +} + +#[derive(FromRow)] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: DepositId, + + /// base58-encoded issued credential + pub bs58_partial_credential: String, + + /// base58-encoded signature on the issued credential (and the attributes) + pub bs58_signature: String, + + // i.e. "'attr1','attr2',..." + pub joined_private_commitments: String, + + pub expiration_date: Date, +} + +#[derive(FromRow)] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, +} + +#[derive(FromRow)] +pub(crate) struct StoredBloomfilterParams { + pub(crate) id: i64, + pub(crate) num_hashes: u32, + pub(crate) bitmap_size: u32, + + pub(crate) sip0_key0: Vec, + pub(crate) sip0_key1: Vec, + + pub(crate) sip1_key0: Vec, + pub(crate) sip1_key1: Vec, +} + +impl<'a> TryFrom<&'a StoredBloomfilterParams> for BloomfilterParameters { + type Error = NymApiStorageError; + fn try_from(value: &'a StoredBloomfilterParams) -> Result { + let Ok(sip0_key0) = <[u8; 8]>::try_from(value.sip0_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key0", + )); + }; + let Ok(sip0_key1) = <[u8; 8]>::try_from(value.sip0_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key1", + )); + }; + let Ok(sip1_key0) = <[u8; 8]>::try_from(value.sip1_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key0", + )); + }; + let Ok(sip1_key1) = <[u8; 8]>::try_from(value.sip1_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key1", + )); + }; + Ok(BloomfilterParameters { + num_hashes: value.num_hashes, + bitmap_size: value.bitmap_size as u64, + sip_keys: [ + (u64::from_be_bytes(sip0_key0), u64::from_be_bytes(sip0_key1)), + (u64::from_be_bytes(sip1_key0), u64::from_be_bytes(sip1_key1)), + ], + }) + } +} + +impl TryFrom for ApiIssuedCredentialInner { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(ApiIssuedCredentialInner { + credential: ApiIssuedCredential { + id: value.id, + epoch_id: value.epoch_id, + deposit_id: value.deposit_id, + blinded_partial_credential: BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?, + bs58_encoded_private_attributes_commitments: split_attributes( + &value.joined_private_commitments, + ), + expiration_date: value.expiration_date, + }, + signature: value.bs58_signature.parse()?, + }) + } +} + +impl TryFrom for BlindedSignatureResponse { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, + }) + } +} + +impl TryFrom for BlindedSignature { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?) + } +} + +pub fn join_attributes(attrs: I) -> String +where + I: IntoIterator, + M: Display, +{ + // I could have used `attrs.into_iter().join(",")`, + // but my IDE didn't like it (compiler was fine) + itertools::Itertools::join(&mut attrs.into_iter(), ",") +} + +pub fn split_attributes(attrs: &str) -> Vec { + attrs.split(',').map(|s| s.to_string()).collect() +} diff --git a/nym-api/src/coconut/tests/fixtures.rs b/nym-api/src/ecash/tests/fixtures.rs similarity index 93% rename from nym-api/src/coconut/tests/fixtures.rs rename to nym-api/src/ecash/tests/fixtures.rs index d883d122ba..80ee791770 100644 --- a/nym-api/src/coconut/tests/fixtures.rs +++ b/nym-api/src/ecash/tests/fixtures.rs @@ -1,17 +1,17 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::State; -use crate::coconut::keys::KeyPair; -use crate::coconut::tests::{DummyClient, SharedFakeChain}; +use crate::ecash::dkg; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::State; +use crate::ecash::keys::KeyPair; +use crate::ecash::tests::{DummyClient, SharedFakeChain}; use cosmwasm_std::Addr; -use nym_coconut::VerificationKey; use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; use nym_coconut_dkg_common::types::{DealerDetails, EpochId}; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::identity; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use nym_dkg::{NodeIndex, Threshold}; @@ -170,7 +170,11 @@ impl TestingDkgControllerBuilder { state_guard.dkg_contract.epoch.epoch_id = epoch_id; } if let Some(threshold) = self.threshold { - state_guard.dkg_contract.threshold = Some(threshold) + let epoch_id = state_guard.dkg_contract.epoch.epoch_id; + state_guard + .dkg_contract + .threshold + .insert(epoch_id, threshold); } let epoch_id = state_guard.dkg_contract.epoch.epoch_id; @@ -266,7 +270,7 @@ impl TestingDkgController { Addr::unchecked(self.address().await.as_ref()) } - pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKey { + pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKeyAuth { self.state .unchecked_coconut_keypair() .await diff --git a/nym-api/src/coconut/tests/helpers.rs b/nym-api/src/ecash/tests/helpers.rs similarity index 96% rename from nym-api/src/coconut/tests/helpers.rs rename to nym-api/src/ecash/tests/helpers.rs index a92a448502..5cd410056f 100644 --- a/nym-api/src/coconut/tests/helpers.rs +++ b/nym-api/src/ecash/tests/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; -use crate::coconut::tests::SharedFakeChain; +use crate::ecash::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; +use crate::ecash::tests::SharedFakeChain; use nym_coconut_dkg_common::types::EpochState; use nym_dkg::bte::PublicKeyWithProof; @@ -80,7 +80,7 @@ pub(crate) async fn submit_public_keys(controllers: &mut [TestingDkgController], let mut guard = controllers[0].chain_state.lock().unwrap(); guard.dkg_contract.epoch.state = EpochState::DealingExchange { resharing }; - guard.dkg_contract.threshold = Some(threshold) + guard.dkg_contract.threshold.insert(epoch, threshold); } pub(crate) async fn exchange_dealings(controllers: &mut [TestingDkgController], resharing: bool) { diff --git a/nym-api/src/coconut/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs similarity index 81% rename from nym-api/src/coconut/tests/issued_credentials.rs rename to nym-api/src/ecash/tests/issued_credentials.rs index fadfdad4e8..2dfbf1c473 100644 --- a/nym-api/src/coconut/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -1,21 +1,20 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::{voucher_fixture, TestFixture}; -use cosmwasm_std::coin; -use nym_api_requests::coconut::models::{ +use crate::ecash::tests::{voucher_fixture, TestFixture}; +use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, }; -use nym_api_requests::coconut::CredentialsRequestBody; -use nym_validator_client::nym_api::routes::{API_VERSION, BANDWIDTH, COCONUT_ROUTES}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_ROUTES}; use rocket::http::Status; use std::collections::BTreeMap; #[tokio::test] async fn epoch_credentials() { - let route_epoch1 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/1"); - let route_epoch2 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/2"); - let route_epoch42 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/42"); + let route_epoch1 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/1"); + let route_epoch2 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/2"); + let route_epoch42 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/42"); let test_fixture = TestFixture::new().await; @@ -94,27 +93,25 @@ async fn epoch_credentials() { #[tokio::test] async fn issued_credential() { fn route(id: i64) -> String { - format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}") + format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}") } // let test_fixture = TestFixture::new() - let hash1 = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E".to_string(); - let hash2 = "9F4DF28B36189B4410BC23D97FD757FC74B919122E80534CC2CA6F3D646F6518".to_string(); + let deposit_id1 = 123; + let deposit_id2 = 321; - let voucher1 = voucher_fixture(coin(1234, "unym"), Some(hash1.clone())); - let voucher2 = voucher_fixture(coin(1234, "unym"), Some(hash2.clone())); + let voucher1 = voucher_fixture(Some(deposit_id1)); + let voucher2 = voucher_fixture(Some(deposit_id2)); let signing_data1 = voucher1.prepare_for_signing(); - let voucher_data1 = voucher1.get_variant_data().voucher_data().unwrap(); - let request1 = voucher_data1.create_blind_sign_request_body(&signing_data1); + let request1 = voucher1.create_blind_sign_request_body(&signing_data1); let signing_data2 = voucher2.prepare_for_signing(); - let voucher_data2 = voucher2.get_variant_data().voucher_data().unwrap(); - let request2 = voucher_data2.create_blind_sign_request_body(&signing_data2); + let request2 = voucher2.create_blind_sign_request_body(&signing_data2); let test_fixture = TestFixture::new().await; - test_fixture.add_deposit_tx(voucher_data1); - test_fixture.add_deposit_tx(voucher_data2); + test_fixture.add_deposit(&voucher1); + test_fixture.add_deposit(&voucher2); // random credential that was never issued let response = test_fixture.rocket.get(route(42)).dispatch().await; @@ -143,11 +140,12 @@ async fn issued_credential() { // TODO: currently we have no signature checks assert_eq!(1, issued1.credential.id); assert_eq!(1, issued1.credential.epoch_id); - assert_eq!(voucher_data1.tx_hash(), issued1.credential.tx_hash); + assert_eq!(voucher1.deposit_id(), issued1.credential.deposit_id); assert_eq!( cred1.to_bytes(), issued1.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request1.encode_commitments(), issued1 @@ -155,17 +153,18 @@ async fn issued_credential() { .bs58_encoded_private_attributes_commitments ); assert_eq!( - voucher1.get_plain_public_attributes(), - issued1.credential.public_attributes + voucher1.expiration_date(), + issued1.credential.expiration_date ); assert_eq!(2, issued2.credential.id); assert_eq!(3, issued2.credential.epoch_id); - assert_eq!(voucher_data2.tx_hash(), issued2.credential.tx_hash); + assert_eq!(voucher2.deposit_id(), issued2.credential.deposit_id); assert_eq!( cred2.to_bytes(), issued2.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request2.encode_commitments(), issued2 @@ -173,14 +172,14 @@ async fn issued_credential() { .bs58_encoded_private_attributes_commitments ); assert_eq!( - voucher2.get_plain_public_attributes(), - issued2.credential.public_attributes + voucher2.expiration_date(), + issued2.credential.expiration_date ); } #[tokio::test] async fn issued_credentials() { - let route = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credentials"); + let route = format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credentials"); let test_fixture = TestFixture::new().await; diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs similarity index 59% rename from nym-api/src/coconut/tests/mod.rs rename to nym-api/src/ecash/tests/mod.rs index 43f3de399e..6184ac7899 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,26 +1,21 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; +use crate::ecash; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::keys::KeyPairWithEpoch; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; use crate::support::storage::NymApiStorage; use async_trait::async_trait; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ - coin, from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, + from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, }; use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes}; use cw4::{Cw4Contract, MemberResponse}; -use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse}; -use nym_api_requests::coconut::{BlindSignRequestBody, BlindedSignatureResponse}; -use nym_coconut::{BlindedSignature, Parameters, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, - DEPOSIT_VALUE, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use nym_api_requests::ecash::models::{IssuedCredentialBody, IssuedCredentialResponse}; +use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse}; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails, }; @@ -34,21 +29,19 @@ use nym_coconut_dkg_common::types::{ EpochId, EpochState, PartialContractDealingData, State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; use nym_contracts_common::IdentityKey; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::CredentialType; -use nym_credentials::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::{encryption, identity}; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::identity; use nym_dkg::{NodeIndex, Threshold}; -use nym_validator_client::nym_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, -}; +use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; +use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse}; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_BLIND_SIGN, ECASH_ROUTES}; use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::Coin; -use nym_validator_client::nyxd::{ - AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse, -}; +use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse}; +use nym_validator_client::{EcashApiClient, NymApiClient}; use rand::rngs::OsRng; use rand::RngCore; use rocket::http::Status; @@ -59,6 +52,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tempfile::{tempdir, TempDir}; +use tokio::sync::RwLock; pub(crate) mod fixtures; pub(crate) mod helpers; @@ -72,6 +66,9 @@ struct InternalCounters { node_index_counter: NodeIndex, tx_hash_counter: u64, proposal_id_counter: u64, + + #[allow(dead_code)] + deposit_id_counter: u32, } impl InternalCounters { @@ -92,6 +89,12 @@ impl InternalCounters { self.tx_hash_counter += 1; Hash::Sha256(sha2::Sha256::digest(&self.tx_hash_counter.to_be_bytes()).into()) } + + #[allow(dead_code)] + fn next_deposit_id(&mut self) -> DepositId { + self.deposit_id_counter += 1; + self.deposit_id_counter + } } #[derive(Debug)] @@ -151,7 +154,7 @@ pub(crate) struct FakeDkgContractState { pub(crate) epoch: Epoch, pub(crate) contract_state: ContractState, - pub(crate) threshold: Option, + pub(crate) threshold: HashMap, } impl FakeDkgContractState { @@ -168,9 +171,7 @@ impl FakeDkgContractState { // .collect() // } - fn reset_dkg_state(&mut self) { - self.threshold = None; - } + fn reset_dkg_state(&mut self) {} pub(crate) fn reset_epoch_in_reshare_mode(&mut self) { self.reset_dkg_state(); @@ -267,16 +268,17 @@ pub(crate) struct FakeMultisigContractState { } impl FakeMultisigContractState { + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.votes = HashMap::new() } } #[derive(Debug)] -pub(crate) struct FakeBandwidthContractState { +pub(crate) struct FakeEcashContractState { pub(crate) address: Addr, - pub(crate) admin: Option, - pub(crate) spent_credentials: HashMap, + pub(crate) deposits: HashMap, + pub(crate) blacklist: HashMap, } #[derive(Debug, Clone, Default)] @@ -300,7 +302,7 @@ pub(crate) struct FakeChainState { pub(crate) dkg_contract: FakeDkgContractState, pub(crate) group_contract: FakeGroupContractState, pub(crate) multisig_contract: FakeMultisigContractState, - pub(crate) bandwidth_contract: FakeBandwidthContractState, + pub(crate) ecash_contract: FakeEcashContractState, } impl Default for FakeChainState { @@ -311,14 +313,9 @@ impl Default for FakeChainState { Addr::unchecked("n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju"); let dkg_contract = Addr::unchecked("n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"); - let bandwidth_contract = + let ecash_contract = Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l"); - let bandwidth_contract_admin = - "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a" - .parse() - .unwrap(); - FakeChainState { _counters: Default::default(), @@ -339,7 +336,7 @@ impl Default for FakeChainState { }, dealings: HashMap::new(), verification_shares: HashMap::new(), - threshold: None, + threshold: HashMap::new(), }, group_contract: FakeGroupContractState { address: group_contract, @@ -350,10 +347,10 @@ impl Default for FakeChainState { proposals: Default::default(), votes: Default::default(), }, - bandwidth_contract: FakeBandwidthContractState { - address: bandwidth_contract, - admin: Some(bandwidth_contract_admin), - spent_credentials: Default::default(), + ecash_contract: FakeEcashContractState { + address: ecash_contract, + deposits: Default::default(), + blacklist: Default::default(), }, } } @@ -380,6 +377,7 @@ impl FakeChainState { self.group_contract.add_member(address, weight) } + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.multisig_contract.reset_votes() } @@ -429,7 +427,7 @@ impl FakeChainState { if contract == &self.multisig_contract.address { unimplemented!("multisig contract exec") } - if contract == &self.bandwidth_contract.address { + if contract == &self.ecash_contract.address { unimplemented!("bandwidth contract exec") } if contract == self.dkg_contract.address.as_ref() { @@ -504,109 +502,6 @@ impl DummyClient { pub fn chain_state(&self) -> SharedFakeChain { self.state.clone() } - - // pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { - // todo!() - // // self.tx_db = Arc::clone(tx_db); - // // self - // } - // - // pub fn with_proposal_db( - // mut self, - // proposal_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.proposal_db = Arc::clone(proposal_db); - // // self - // } - // - // pub fn with_spent_credential_db( - // mut self, - // spent_credential_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.spent_credential_db = Arc::clone(spent_credential_db); - // // self - // } - // - // pub fn _with_epoch(mut self, epoch: &Arc>) -> Self { - // todo!() - // // self.epoch = Arc::clone(epoch); - // // self - // } - // - // pub fn with_dealer_details( - // mut self, - // dealer_details: &Arc>>, - // ) -> Self { - // todo!() - // // self.dealer_details = Arc::clone(dealer_details); - // // self - // } - // - // pub fn with_threshold(mut self, threshold: &Arc>>) -> Self { - // todo!() - // // self.threshold = Arc::clone(threshold); - // // self - // } - // - // // it's a really bad practice, but I'm not going to be changing it now... - // #[allow(clippy::type_complexity)] - // pub fn with_dealings( - // mut self, - // dealings: &Arc>>>>, - // ) -> Self { - // todo!() - // // self.dealings = Arc::clone(dealings); - // // self - // } - // - // pub fn with_verification_share( - // mut self, - // verification_share: &Arc>>, - // ) -> Self { - // todo!() - // // self.verification_share = Arc::clone(verification_share); - // // self - // } - // - // pub fn _with_group_db( - // mut self, - // group_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.group_db = Arc::clone(group_db); - // // self - // } - // - // pub fn with_initial_dealers_db( - // mut self, - // initial_dealers: &Arc>>, - // ) -> Self { - // todo!() - // // self.initial_dealers_db = Arc::clone(initial_dealers); - // // self - // } - - // async fn get_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } - // - // async fn get_past_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.past_dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } } #[async_trait] @@ -619,19 +514,20 @@ impl super::client::Client for DummyClient { Ok(self.state.lock().unwrap().dkg_contract.address.clone()) } - async fn bandwidth_contract_admin(&self) -> Result> { - Ok(self.state.lock().unwrap().bandwidth_contract.admin.clone()) - } - - async fn get_tx(&self, tx_hash: Hash) -> Result { - Ok(self + async fn get_deposit(&self, deposit_id: DepositId) -> Result { + let deposit = self .state .lock() .unwrap() - .txs - .get(&tx_hash) - .cloned() - .unwrap()) + .ecash_contract + .deposits + .get(&deposit_id) + .cloned(); + + Ok(DepositResponse { + id: deposit_id, + deposit, + }) } async fn get_proposal(&self, proposal_id: u64) -> Result { @@ -641,7 +537,7 @@ impl super::client::Client for DummyClient { .proposals .get(&proposal_id) .cloned() - .ok_or(CoconutError::IncorrectProposal { + .ok_or(EcashError::IncorrectProposal { reason: String::from("proposal not found"), })?; @@ -678,20 +574,19 @@ impl super::client::Client for DummyClient { Ok(VoteResponse { vote }) } - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result { - self.state - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .get(&blinded_serial_number) - .cloned() - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("spent credential not found"), - }) + public_key: String, + ) -> Result { + Ok(BlacklistedAccountResponse::new( + self.state + .lock() + .unwrap() + .ecash_contract + .blacklist + .get(&public_key) + .cloned(), + )) } async fn contract_state(&self) -> Result { @@ -721,7 +616,20 @@ impl super::client::Client for DummyClient { } async fn get_current_epoch_threshold(&self) -> Result> { - Ok(self.state.lock().unwrap().dkg_contract.threshold) + let guard = self.state.lock().unwrap(); + let current_epoch = guard.dkg_contract.epoch.epoch_id; + Ok(guard.dkg_contract.threshold.get(¤t_epoch).cloned()) + } + + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result> { + Ok(self + .state + .lock() + .unwrap() + .dkg_contract + .threshold + .get(&epoch_id) + .cloned()) } async fn get_self_registered_dealer_details(&self) -> Result { @@ -915,6 +823,15 @@ impl super::client::Client for DummyClient { } } + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(|s| s.try_into().unwrap()) + .collect()) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -924,7 +841,7 @@ impl super::client::Client for DummyClient { let voter = self.validator_address.to_string(); let mut chain = self.state.lock().unwrap(); if !chain.multisig_contract.proposals.contains_key(&proposal_id) { - return Err(CoconutError::IncorrectProposal { + return Err(EcashError::IncorrectProposal { reason: String::from("proposal not found"), }); } @@ -970,7 +887,7 @@ impl super::client::Client for DummyClient { let multisig_address: AccountId = chain.multisig_contract.address.as_str().parse().unwrap(); let Some(proposal) = chain.multisig_contract.proposals.get_mut(&proposal_id) else { - return Err(CoconutError::ProposalIdError { + return Err(EcashError::ProposalIdError { reason: String::from("proposal id not found"), }); }; @@ -1034,8 +951,8 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(NODE_INDEX, assigned_index.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1068,8 +985,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1103,8 +1020,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1121,7 +1038,7 @@ impl super::client::Client for DummyClient { let epoch_id = chain.dkg_contract.epoch.epoch_id; let Some(dealer_details) = chain.dkg_contract.get_dealer_details(&address, epoch_id) else { // Just throw some error, not really the correct one - return Err(CoconutError::DepositEncrKeyNotFound); + return Err(EcashError::DepositInfoNotFound); }; let dkg_contract = chain.dkg_contract.address.clone(); @@ -1180,28 +1097,43 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(DKG_PROPOSAL_ID, proposal_id.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct DummyCommunicationChannel { current_epoch: Arc, - aggregated_verification_key: VerificationKey, + ecash_clients: Arc>>>, } impl DummyCommunicationChannel { - pub fn new(aggregated_verification_key: VerificationKey) -> Self { + pub fn new(ecash_clients: Vec) -> Self { + let epoch_id = 1; + let mut ecash_clients_map = HashMap::new(); + ecash_clients_map.insert(epoch_id, ecash_clients); DummyCommunicationChannel { - current_epoch: Arc::new(AtomicU64::new(1)), - aggregated_verification_key, + current_epoch: Arc::new(AtomicU64::new(epoch_id)), + ecash_clients: Arc::new(RwLock::new(ecash_clients_map)), } } + pub fn new_single_dummy(aggregated_verification_key: VerificationKeyAuth) -> Self { + let client = EcashApiClient { + api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), + verification_key: aggregated_verification_key, + node_id: 1, + cosmos_address: "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), + }; + Self::new(vec![client]) + } + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { self.current_epoch = current_epoch; self @@ -1214,11 +1146,37 @@ impl super::comm::APICommunicationChannel for DummyCommunicationChannel { Ok(self.current_epoch.load(Ordering::Relaxed)) } - async fn aggregated_verification_key(&self, _epoch_id: EpochId) -> Result { - Ok(self.aggregated_verification_key.clone()) + async fn dkg_in_progress(&self) -> Result { + // deal with this later lol + Ok(false) + } + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .ecash_clients + .read() + .await + .get(&epoch_id) + .cloned() + .unwrap_or_default()) + } + + async fn ecash_threshold(&self, _epoch_id: EpochId) -> Result { + todo!() } } +#[allow(dead_code)] +pub fn deposit_fixture() -> Deposit { + let mut rng = OsRng; + let identity_keypair = identity::KeyPair::new(&mut rng); + + Deposit { + bs58_encoded_ed25519_pubkey: identity_keypair.public_key().to_base58_string(), + } +} + +#[allow(dead_code)] pub fn tx_entry_fixture(hash: Hash) -> TxResponse { TxResponse { hash, @@ -1239,53 +1197,6 @@ pub fn tx_entry_fixture(hash: Hash) -> TxResponse { } } -pub fn deposit_tx_fixture(voucher_data: &BandwidthVoucherIssuanceData) -> TxResponse { - TxResponse { - hash: voucher_data.tx_hash(), - height: Default::default(), - index: 0, - tx_result: ExecTxResult { - code: Default::default(), - data: Default::default(), - log: "".to_string(), - info: "".to_string(), - gas_wanted: 0, - gas_used: 0, - events: vec![Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![ - EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: voucher_data.value_plain(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: CredentialType::Voucher.to_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: voucher_data.identity_key().public_key().to_base58_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: voucher_data - .encryption_key() - .public_key() - .to_base58_string(), - index: false, - }, - ], - }], - codespace: "".to_string(), - }, - tx: vec![], - proof: None, - } -} - pub fn blinded_signature_fixture() -> BlindedSignature { let gen1_bytes = [ 151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140, @@ -1302,25 +1213,17 @@ pub fn blinded_signature_fixture() -> BlindedSignature { BlindedSignature::from_bytes(&dummy_bytes).unwrap() } -pub fn voucher_fixture>( - amount: C, - tx_hash: Option, -) -> IssuanceBandwidthCredential { +pub fn voucher_fixture(deposit_id: Option) -> IssuanceTicketBook { let mut rng = OsRng; - let tx_hash = if let Some(provided) = &tx_hash { - provided.parse().unwrap() - } else { - Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap() - }; + let deposit_id = deposit_id.unwrap_or(69); let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); + let id_priv = identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); - let enc_priv = - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap(); - - IssuanceBandwidthCredential::new_voucher(amount.into(), tx_hash, id_priv, enc_priv) + let identifier = [44u8; 32]; + // (voucher, request) + IssuanceTicketBook::new(deposit_id, identifier, id_priv) } fn dummy_signature() -> identity::Signature { @@ -1340,13 +1243,12 @@ struct TestFixture { impl TestFixture { async fn new() -> Self { - let mut rng = crate::coconut::tests::fixtures::test_rng([69u8; 32]); - let params = Parameters::new(4).unwrap(); - let coconut_keypair = nym_coconut::ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]); + let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); let identity = identity::KeyPair::new(&mut rng); let epoch = Arc::new(AtomicU64::new(1)); let comm_channel = - DummyCommunicationChannel::new(coconut_keypair.verification_key().clone()) + DummyCommunicationChannel::new_single_dummy(coconut_keypair.verification_key().clone()) .with_epoch(epoch.clone()); // TODO: it's AWFUL to test with actual storage, we should somehow abstract it away @@ -1355,7 +1257,7 @@ impl TestFixture { .await .unwrap(); - let staged_key_pair = crate::coconut::KeyPair::new(); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: coconut_keypair, @@ -1370,14 +1272,33 @@ impl TestFixture { chain_state.clone(), ); - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); + let ecash_contract = chain_state + .lock() + .unwrap() + .ecash_contract + .address + .clone() + .as_str() + .parse() + .unwrap(); + + let rocket = rocket::build() + .manage( + EcashState::new( + ecash_contract, + nyxd_client, + identity, + staged_key_pair, + comm_channel, + storage.clone(), + ) + .await + .unwrap(), + ) + .mount( + "/v1/ecash", + ecash::routes_open_api(&Default::default(), true).0, + ); TestFixture { rocket: Client::tracked(rocket) @@ -1394,39 +1315,43 @@ impl TestFixture { self.epoch.store(epoch, Ordering::Relaxed) } + #[allow(dead_code)] fn add_tx(&self, hash: Hash, tx: TxResponse) { self.chain_state.lock().unwrap().txs.insert(hash, tx); } - fn add_deposit_tx(&self, voucher: &BandwidthVoucherIssuanceData) { - let mut guard = self.chain_state.lock().unwrap(); - let fixture = deposit_tx_fixture(voucher); - - guard.txs.insert(voucher.tx_hash(), fixture); + fn add_deposit(&self, voucher_data: &IssuanceTicketBook) { + let mut chain = self.chain_state.lock().unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher_data + .identity_key() + .public_key() + .to_base58_string(), + }; + let existing = chain + .ecash_contract + .deposits + .insert(voucher_data.deposit_id(), deposit); + assert!(existing.is_none()); } async fn issue_dummy_credential(&self) { let mut rng = OsRng; - let mut tx_hash = [0u8; 32]; - rng.fill_bytes(&mut tx_hash); - let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap(); + let deposit_id = rng.next_u32(); - let voucher = voucher_fixture(coin(1234, "unym"), Some(tx_hash.to_string())); + let voucher = voucher_fixture(Some(deposit_id)); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let req = voucher_data.create_blind_sign_request_body(&signing_data); + let req = voucher.create_blind_sign_request_body(&signing_data); - self.add_deposit_tx(voucher_data); + self.add_deposit(&voucher); self.issue_credential(req).await; } async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self .rocket - .post(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}", - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&req) .dispatch() .await; @@ -1439,7 +1364,7 @@ impl TestFixture { let response = self .rocket .get(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}" + "/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}" )) .dispatch() .await; @@ -1459,41 +1384,38 @@ impl TestFixture { #[cfg(test)] mod credential_tests { use super::*; - use crate::coconut::tests::helpers::init_chain; - use nym_api_requests::coconut::{VerifyCredentialBody, VerifyCredentialResponse}; - use nym_coconut::{blind_sign, hash_to_scalar, ttp_keygen}; - use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredential; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_validator_client::nym_api::routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL; + use nym_compact_ecash::ttp_keygen; #[tokio::test] async fn already_issued() { - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let tx_hash = request_body.tx_hash; - let tx_entry = tx_entry_fixture(tx_hash); + let deposit_id = request_body.deposit_id; let test_fixture = TestFixture::new().await; - test_fixture.add_tx(tx_hash, tx_entry); + test_fixture.add_deposit(&voucher); let sig = blinded_signature_fixture(); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; test_fixture .storage - .store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public) + .store_issued_credential( + 42, + deposit_id, + &sig, + dummy_signature(), + commitments, + expiration_date, + ) .await .unwrap(); let response = test_fixture .rocket - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&request_body) .dispatch() .await; @@ -1524,15 +1446,15 @@ mod credential_tests { AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), Default::default(), ); - let params = Parameters::new(4).unwrap(); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let key_pair = ttp_keygen(1, 1).unwrap().remove(0); let tmp_dir = tempdir().unwrap(); let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) .await .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); + let comm_channel = + DummyCommunicationChannel::new_single_dummy(key_pair.verification_key().clone()); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: key_pair, @@ -1541,43 +1463,44 @@ mod credential_tests { .await; staged_key_pair.validate(); - let state = State::new( + let state = EcashState::new( + "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), nyxd_client, - TEST_COIN_DENOM.to_string(), identity, staged_key_pair, comm_channel, storage.clone(), - ); + ) + .await + .unwrap(); - let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E" - .parse() - .unwrap(); - assert!(state.already_issued(tx_hash).await.unwrap().is_none()); + let deposit_id = 42; + assert!(state.already_issued(deposit_id).await.unwrap().is_none()); - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; let sig = blinded_signature_fixture(); storage .store_issued_credential( 42, - tx_hash, + deposit_id, &sig, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await .unwrap(); assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1599,28 +1522,26 @@ mod credential_tests { let storage_err = storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await; assert!(storage_err.is_err()); - // And use a new hash to store a new signature - let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF" - .parse() - .unwrap(); + // And use a new deposit to store a new signature + let deposit_id = 69; storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await .unwrap(); @@ -1628,7 +1549,7 @@ mod credential_tests { // Check that the same value for tx_hash is returned assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1639,76 +1560,38 @@ mod credential_tests { #[tokio::test] async fn blind_sign_correct() { - let tx_hash = - Hash::from_str("7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B") - .unwrap(); + let deposit_id = 42; - let params = Parameters::new(4).unwrap(); let mut rng = OsRng; - let nym_api_identity = identity::KeyPair::new(&mut rng); - let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); - let voucher = IssuanceBandwidthCredential::new_voucher( - coin(1234, "unym"), - tx_hash, + let identifier = [42u8; 32]; + let voucher = IssuanceTicketBook::new( + deposit_id, + identifier, identity::PrivateKey::from_base58_string( identity_keypair.private_key().to_base58_string(), ) .unwrap(), - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()) - .unwrap(), ); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); - let tmp_dir = tempdir().unwrap(); - let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) - .await - .unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher.identity_key().public_key().to_base58_string(), + }; - let chain = init_chain(); - - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let tx_entry = deposit_tx_fixture(voucher_data); - - chain.lock().unwrap().txs.insert(tx_hash, tx_entry.clone()); - - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - chain.clone(), - ); - - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: 1, - }) - .await; - staged_key_pair.validate(); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - nym_api_identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); + let test = TestFixture::new().await; + test.chain_state + .lock() + .unwrap() + .ecash_contract + .deposits + .insert(voucher.deposit_id(), deposit); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + let response = test + .rocket + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) .json(&request_body) .dispatch() .await; @@ -1722,410 +1605,16 @@ mod credential_tests { assert!(blinded_signature_response.is_ok()); } - #[tokio::test] - async fn verification_of_bandwidth_credential() { - // Setup variables - let chain = init_chain(); - let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - chain - .lock() - .unwrap() - .add_member(validator_address.as_ref(), 100); + #[test] + fn blind_sign_request_body_serde() { + let deposit_id = 123; + let issuance = voucher_fixture(Some(deposit_id)); + let signing_data = issuance.prepare_for_signing(); + let request = issuance.create_blind_sign_request_body(&signing_data); - let nyxd_client = DummyClient::new(validator_address.clone(), chain.clone()); - let db_dir = tempdir().unwrap(); + let json_bytes = serde_json::to_vec(&request).unwrap(); + let recovered: BlindSignRequestBody = serde_json::from_slice(&json_bytes).unwrap(); - // generate all the credential requests - let params = bandwidth_credential_params(); - let key_pair = nym_coconut::keygen(params); - let epoch = 1; - - let voucher_amount = coin(1234, "unym"); - let issuance = voucher_fixture(coin(1234, "unym"), None); - let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - key_pair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, - ) - .unwrap(); - let sig = blind_sig.unblind( - key_pair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - - let issued = issuance.into_issued_credential(sig, epoch); - let spending = issued - .prepare_for_spending(key_pair.verification_key()) - .unwrap(); - - let storage1 = NymApiStorage::init(db_dir.path().join("storage.db")) - .await - .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: epoch, - }) - .await; - staged_key_pair.validate(); - let mut rng = OsRng; - let identity = identity::KeyPair::new(&mut rng); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client.clone(), - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel.clone(), - storage1.clone(), - )); - - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let proposal_id = 42; - // The address is not used, so we can use a duplicate - let gateway_cosmos_addr = validator_address.clone(); - let req = - VerifyCredentialBody::new(spending.clone(), proposal_id, gateway_cosmos_addr.clone()); - - // Test endpoint with not proposal for the proposal id - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "proposal not found".to_string() - } - .to_string() - ); - - let mut proposal = Proposal { - title: String::new(), - description: String::from("25mnnoCcUfeizfC85avvroFg2prpEZBgJbJM2SLtkgyyUkoAU3cqJiqWmg8cMHEPjfFf5sQF92SMAM2vbEoLZvUjenvXhadTLdA4TqMYArJpihyqirW2AhGoNehtcdcK5gnH"), - msgs: vec![], - status: cw3::Status::Open, - expires: cw_utils::Expiration::Never {}, - threshold: cw_utils::Threshold::AbsolutePercentage { - percentage: Decimal::from_ratio(2u32, 3u32), - }, - total_weight: chain.lock().unwrap().total_group_weight(), - votes: Votes::yes(0), - proposer: Addr::unchecked("proposer"), - deposit: None, - start_height: 0 - }; - - // Test the endpoint with a different blinded serial number in the description - - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "incorrect blinded serial number in description".to_string() - } - .to_string() - ); - - // Test the endpoint with no msg in the proposal action - proposal.description = spending - .verify_credential_request - .blinded_serial_number_bs58(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "action is not to release funds".to_string() - } - .to_string() - ); - - // Test the endpoint without any credential recorded in the Coconut Bandwidth Contract - let funds = voucher_amount.clone(); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "spent credential not found".to_string() - } - .to_string() - ); - - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(None), - ); - - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Inexistent".to_string() - } - .to_string() - ); - - // Test the endpoint with a credential that doesn't verify correctly - let mut spent_credential = SpendCredential::new( - funds.clone(), - spending - .verify_credential_request - .blinded_serial_number_bs58(), - Addr::unchecked("unimportant"), - ); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential.clone())), - ); - - // TODO: somehow restore that test - // let bad_credential = Credential::new( - // 4, - // theta.clone(), - // voucher_value, - // String::from("bad voucher info"), - // 0, - // ); - // let bad_req = VerifyCredentialBody::new( - // bad_credential, - // epoch_id, - // proposal_id, - // gateway_cosmos_addr.clone(), - // ); - // let response = client - // .post(format!( - // "/{}/{}/{}/{}", - // API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - // )) - // .json(&bad_req) - // .dispatch() - // .await; - // assert_eq!(response.status(), Status::Ok); - // let verify_credential_response = serde_json::from_str::( - // &response.into_string().await.unwrap(), - // ) - // .unwrap(); - // assert!(!verify_credential_response.verification_result); - // assert_eq!( - // cw3::Status::Rejected, - // chain - // .lock() - // .unwrap() - // .multisig_contract - // .proposals - // .get(&proposal_id) - // .unwrap() - // .status - // ); - - // Test the endpoint with a proposal that has a different value for the funds to be released - // then what's in the credential - let funds = Coin::new(voucher_amount.amount.u128() + 10, TEST_COIN_DENOM); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone().into(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(!verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Rejected, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with every dependency met - let funds = voucher_amount; - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Passed, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract - spent_credential.mark_as_spent(); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential)), - ); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Spent".to_string() - } - .to_string() - ); + assert_eq!(recovered, request) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 5731c1ade3..0bebd493ed 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -4,8 +4,8 @@ #[macro_use] extern crate rocket; -use crate::coconut::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_coconut_keypair_if_exists, +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, }; use crate::epoch_operations::RewardedSetUpdater; use crate::network::models::NetworkDetails; @@ -19,7 +19,7 @@ use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use coconut::dkg::controller::DkgController; +use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_logging; use nym_config::defaults::NymNetworkDetails; @@ -30,7 +30,7 @@ use rand::rngs::OsRng; use support::{http, nyxd}; mod circulating_supply_api; -mod coconut; +mod ecash; mod epoch_operations; pub(crate) mod network; mod network_monitor; @@ -70,10 +70,10 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result let nym_network_details = NymNetworkDetails::new_from_env(); let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - let coconut_keypair_wrapper = coconut::keys::KeyPair::new(); + let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_coconut_keypair_if_exists(&config.coconut_signer)? { + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { let issued_for = loaded_keys.issued_for_epoch; coconut_keypair_wrapper.set(loaded_keys).await; diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 7d13d81522..d0c172d7a3 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -418,3 +418,11 @@ pub enum NymApiStorageError { #[error("failed to perform startup SQL migration - {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), } + +impl NymApiStorageError { + pub fn database_inconsistency>(reason: S) -> NymApiStorageError { + NymApiStorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index c1ea22e825..d200a3445d 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -52,18 +52,18 @@ impl NymContractCacheRefresher { let mixnet = query_guard!(client_guard, mixnet_contract_address()); let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_bandwidth = query_guard!(client_guard, coconut_bandwidth_contract_address()); let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); let group = query_guard!(client_guard, group_contract_address()); let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); for (address, name) in [ (mixnet, "nym-mixnet-contract"), (vesting, "nym-vesting-contract"), - (coconut_bandwidth, "nym-coconut-bandwidth-contract"), (coconut_dkg, "nym-coconut-dkg-contract"), (group, "nym-cw4-group-contract"), (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), ] { let (cw2, build_info) = if let Some(address) = address { let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1a88a9ab33..053cb8ba88 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut; +use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; use okapi::openapi3::OpenApi; @@ -26,7 +26,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: coconut::keys::KeyPair, + pub(crate) coconut_keypair: ecash::keys::KeyPair, } impl ApiStatusState { diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 614785eeba..560ac765d9 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -5,7 +5,7 @@ use crate::node_status_api::models::ErrorResponse; use crate::status::ApiStatusState; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_coconut::Base58; +use nym_compact_ecash::Base58; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 9f965e7815..6521c8f7f4 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::keys::init_bte_keypair; +use crate::ecash::dkg::controller::keys::init_bte_keypair; use crate::support::config; use crate::support::config::{ default_config_directory, default_data_directory, upgrade_helpers, Config, diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 99cd925e80..a0696d4955 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::coconut::client::Client; -use crate::coconut::{self, comm::QueryCommunicationChannel}; +use crate::ecash::client::Client; +use crate::ecash::state::EcashState; +use crate::ecash::{self, comm::QueryCommunicationChannel}; use crate::network::models::NetworkDetails; use crate::network::network_routes; use crate::node_describe_cache::DescribedNodes; @@ -16,7 +17,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use nym_crypto::asymmetric::identity; use nym_validator_client::nyxd::Coin; use rocket::http::Method; @@ -33,7 +34,7 @@ pub(crate) async fn setup_rocket( network_details: NetworkDetails, nyxd_client: nyxd::Client, identity_keypair: identity::KeyPair, - coconut_keypair: coconut::keys::KeyPair, + coconut_keypair: ecash::keys::KeyPair, ) -> anyhow::Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); let mut rocket = rocket::build(); @@ -50,11 +51,11 @@ pub(crate) async fn setup_rocket( "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), "/network" => network_routes(&openapi_settings), "/api-status" => api_status_routes(&openapi_settings), + "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), "" => nym_node_routes(&openapi_settings), // => when we move those routes, we'll need to add a redirection for backwards compatibility "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } let rocket = rocket @@ -83,9 +84,9 @@ pub(crate) async fn setup_rocket( let rocket = if config.coconut_signer.enabled { // make sure we have some tokens to cover multisig fees let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < coconut::MINIMUM_BALANCE { + if balance.amount < ecash::MINIMUM_BALANCE { let address = nyxd_client.address().await; - let min = Coin::new(coconut::MINIMUM_BALANCE, mix_denom); + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") } @@ -103,15 +104,24 @@ pub(crate) async fn setup_rocket( coconut_keypair: coconut_keypair.clone(), }); + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - rocket.attach(coconut::stage( + + let ecash_state = EcashState::new( + ecash_contract, nyxd_client.clone(), - mix_denom, identity_keypair, coconut_keypair, comm_channel, storage.clone().unwrap(), - )) + ) + .await?; + + rocket.manage(ecash_state) } else { rocket }; diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 833d891473..69f9a49360 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -1,14 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::CoconutError; +use crate::ecash::error::EcashError; use crate::epoch_operations::MixnodeWithPerformance; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; use nym_coconut_dkg_common::dealing::{ DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, @@ -22,6 +21,9 @@ use nym_coconut_dkg_common::{ verification_key::{ContractVKShare, VerificationKeyShare}, }; use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::RewardingParams; @@ -29,14 +31,14 @@ use nym_mixnet_contract_common::{ CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, MixId, RewardedSetNodeStatus, }; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ contract_traits::{ - CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient, - MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, - NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient, - PagedVestingQueryClient, + DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, + MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, + PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, }, cosmwasm_client::types::ExecuteResult, CosmWasmClient, Fee, @@ -45,7 +47,9 @@ use nym_validator_client::nyxd::{ hash::{Hash, SHA256_HASH_SIZE}, AccountId, Coin, TendermintTime, }; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use nym_validator_client::{ + nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, +}; use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; @@ -153,6 +157,15 @@ impl Client { nyxd_query!(self, current_chain_details().clone()) } + pub(crate) async fn get_ecash_contract_address(&self) -> Result { + nyxd_query!( + self, + ecash_contract_address() + .cloned() + .ok_or_else(|| NyxdError::unavailable_contract_address("ecash contract").into()) + ) + } + pub(crate) async fn get_rewarding_validator_address(&self) -> Result { let cosmwasm_addr = nyxd_query!( self, @@ -338,12 +351,12 @@ impl Client { } #[async_trait] -impl crate::coconut::client::Client for Client { +impl crate::ecash::client::Client for Client { async fn address(&self) -> AccountId { self.client_address().await } - async fn dkg_contract_address(&self) -> Result { + async fn dkg_contract_address(&self) -> Result { nyxd_query!( self, dkg_contract_address() @@ -352,37 +365,21 @@ impl crate::coconut::client::Client for Client { ) } - async fn bandwidth_contract_admin(&self) -> crate::coconut::error::Result> { - let guard = self.inner.read().await; - - let bandwidth_contract = query_guard!( - guard, - coconut_bandwidth_contract_address() - .ok_or(CoconutError::MissingBandwidthContractAddress) - )?; - - let contract = query_guard!(guard, get_contract(bandwidth_contract)).await?; - - Ok(contract.contract_info.admin) - } - - async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result { - nyxd_query!(self, get_tx(tx_hash).await).map_err(|source| { - CoconutError::TxRetrievalFailure { - tx_hash: tx_hash.to_string(), - source, - } - }) + async fn get_deposit( + &self, + deposit_id: DepositId, + ) -> crate::ecash::error::Result { + Ok(nyxd_query!(self, get_deposit(deposit_id).await?)) } async fn get_proposal( &self, proposal_id: u64, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_proposal(proposal_id).await?)) } - async fn list_proposals(&self) -> crate::coconut::error::Result> { + async fn list_proposals(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_proposals().await?)) } @@ -390,41 +387,58 @@ impl crate::coconut::client::Client for Client { &self, proposal_id: u64, voter: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_vote(proposal_id, voter).await?)) } - async fn get_spent_credential( + // async fn propose_for_blacklist( + // &self, + // public_key: String, + // ) -> crate::ecash::error::Result { + // Ok(nyxd_signing!( + // self, + // propose_for_blacklist(public_key, None).await? + // )) + // } + + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> crate::coconut::error::Result { + public_key: String, + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, - get_spent_credential(blinded_serial_number).await? + get_blacklisted_account(public_key).await? )) } - async fn contract_state(&self) -> crate::coconut::error::Result { + async fn contract_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_state().await?)) } - async fn get_current_epoch(&self) -> crate::coconut::error::Result { + async fn get_current_epoch(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_current_epoch().await?)) } - async fn group_member(&self, addr: String) -> crate::coconut::error::Result { + async fn group_member(&self, addr: String) -> crate::ecash::error::Result { Ok(nyxd_query!(self, member(addr, None).await?)) } async fn get_current_epoch_threshold( &self, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_current_epoch_threshold().await?)) } + async fn get_epoch_threshold( + &self, + epoch_id: EpochId, + ) -> crate::ecash::error::Result> { + Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) + } + async fn get_self_registered_dealer_details( &self, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let self_address = &self.address().await; Ok(nyxd_query!(self, get_dealer_details(self_address).await?)) } @@ -433,7 +447,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let dealer = dealer .as_str() .parse() @@ -448,7 +462,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealer_dealings_status(epoch_id, dealer).await? @@ -460,14 +474,14 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealing_status(epoch_id, dealer, dealing_index).await? )) } - async fn get_current_dealers(&self) -> crate::coconut::error::Result> { + async fn get_current_dealers(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_current_dealers().await?)) } @@ -476,7 +490,7 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealings_metadata(epoch_id, dealer, dealing_index) @@ -491,7 +505,7 @@ impl crate::coconut::client::Client for Client { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealing_chunk(epoch_id, dealer.to_string(), dealing_index, chunk_index) @@ -504,40 +518,52 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) } async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!( self, get_all_verification_key_shares(epoch_id).await? )) } + async fn get_registered_ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashError> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + } + async fn vote_proposal( &self, proposal_id: u64, vote_yes: bool, fee: Option, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { nyxd_signing!(self, vote_proposal(proposal_id, vote_yes, fee).await?); Ok(()) } - async fn execute_proposal(&self, proposal_id: u64) -> crate::coconut::error::Result<()> { + async fn execute_proposal(&self, proposal_id: u64) -> crate::ecash::error::Result<()> { nyxd_signing!(self, execute_proposal(proposal_id, None).await?); Ok(()) } - async fn can_advance_epoch_state(&self) -> crate::coconut::error::Result { + async fn can_advance_epoch_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, can_advance_state().await?.can_advance())) } - async fn advance_epoch_state(&self) -> crate::coconut::error::Result<()> { + async fn advance_epoch_state(&self) -> crate::ecash::error::Result<()> { nyxd_signing!(self, advance_dkg_epoch_state(None).await?); Ok(()) } @@ -548,7 +574,7 @@ impl crate::coconut::client::Client for Client { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, register_dealer(bte_key, identity_key, announce_address, resharing, None).await? @@ -560,7 +586,7 @@ impl crate::coconut::client::Client for Client { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_dealing_metadata(dealing_index, chunks, resharing, None).await? @@ -570,7 +596,7 @@ impl crate::coconut::client::Client for Client { async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, submit_dealing_chunk(chunk, None).await? @@ -581,7 +607,7 @@ impl crate::coconut::client::Client for Client { &self, share: VerificationKeyShare, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_verification_key_share(share, resharing, None).await? diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index f44bdc399b..bf9e88d370 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -17,6 +17,7 @@ use nym_gateway::GatewayError; use nym_mixnode::MixnodeError; use nym_network_requester::{CustomGatewayDetails, GatewayDetails}; use nym_node::config; +use nym_node::config::entry_gateway::ZkNymTicketHandlerDebug; use nym_node::config::mixnode::DEFAULT_VERLOC_PORT; use nym_node::config::Config; use nym_node::config::{default_config_filepath, ConfigBuilder, NodeMode}; @@ -411,6 +412,16 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { announce_wss_port: cfg.gateway.clients_wss_port, debug: config::entry_gateway::Debug { message_retrieval_limit: cfg.debug.message_retrieval_limit, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: + cfg.debug.zk_nym_tickets.revocation_bandwidth_penalty, + pending_poller: cfg.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: cfg.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: + cfg.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: + cfg.debug.zk_nym_tickets.maximum_time_between_redemption, + }, }, }, )) diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index 67c44977cf..d22b3c9e28 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -69,8 +69,6 @@ fn print_signed_contract_msg( } pub async fn execute(args: Args) -> Result<(), NymNodeError> { - println!("args: {args:?}"); - let config = try_load_current_config(args.config.config_path()).await?; let identity_keypair = load_ed25519_identity_keypair(config.storage_paths.keys.ed25519_identity_storage_paths())?; diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/entry_gateway.rs index d4ef188da9..dd3670f42f 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/entry_gateway.rs @@ -12,6 +12,7 @@ use nym_gateway::node::LocalAuthenticatorOpts; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::Path; +use std::time::Duration; use super::helpers::{base_client_config, EphemeralConfig}; use super::LocalWireguardOpts; @@ -48,9 +49,12 @@ pub struct EntryGatewayConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] +#[serde(default)] pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Debug { @@ -61,6 +65,52 @@ impl Default for Debug { fn default() -> Self { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + pub const DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION: Duration = Duration::from_secs(86400 * 25); +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION, } } } diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 8c258c4b7f..08ffdd8a43 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -57,15 +57,21 @@ mod tests { ) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] != buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] != routing[..]); + assert_ne!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_ne!(new_buffer[mix_params.routing_data_range()], routing[..]); let _ = mix_params .decode_mix_layer(&mut new_buffer[..], &mix_secret) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] == buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] == routing[..]); + assert_eq!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_eq!(new_buffer[mix_params.routing_data_range()], routing[..]); } #[test] @@ -75,14 +81,14 @@ mod tests { let mut message_clone = message.clone(); lion_transform(&mut message_clone[..], &key, [1, 2, 3]).unwrap(); - assert!(message_clone[..] != message[..]); + assert_ne!(message_clone[..], message[..]); let mut message_clone_2 = message.clone(); lion_transform_encrypt(&mut message_clone_2, &key).unwrap(); assert_eq!(message_clone_2, message_clone); lion_transform(&mut message_clone[..], &key[..], [3, 2, 1]).unwrap(); - assert!(message_clone[..] == message[..]); + assert_eq!(message_clone[..], message[..]); } #[test] diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 545d62b194..b8198fb7df 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -33,7 +33,8 @@ humantime-serde.workspace = true # internal nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } nym-config = { path = "../common/config" } -nym-coconut = { path = "../common/nymcoconut" } +nym-ecash-time = { path = "../common/ecash-time" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } nym-crypto = { path = "../common/crypto", features = ["asymmetric"] } nym-credentials = { path = "../common/credentials" } nym-network-defaults = { path = "../common/network-defaults" } diff --git a/nym-validator-rewarder/migrations/03_use_deposit_id.sql b/nym-validator-rewarder/migrations/03_use_deposit_id.sql new file mode 100644 index 0000000000..9c6aef5da3 --- /dev/null +++ b/nym-validator-rewarder/migrations/03_use_deposit_id.sql @@ -0,0 +1,28 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +DROP TABLE validated_deposit; +CREATE TABLE validated_deposit +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); + +-- evidence of attempting to re-use the same deposit id for multiple credentials +DROP TABLE double_signing_evidence; +CREATE TABLE double_signing_evidence +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + original_credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); \ No newline at end of file diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index cc7a756363..25807722d9 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::RewardingRatios; -use nym_coconut::CoconutError; +use nym_compact_ecash::error::CompactEcashError; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::tx::ErrorReport; -use nym_validator_client::nyxd::{AccountId, Coin, Hash}; +use nym_validator_client::nyxd::{AccountId, Coin}; use std::io; use std::path::PathBuf; use thiserror::Error; @@ -119,14 +119,14 @@ pub enum NymRewarderError { MalformedCredentialCommitment { raw: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the partial verification key for runner {runner} is malformed: {source}")] MalformedPartialVerificationKey { runner: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the signature on issued credential with id {credential_id} is invalid")] @@ -135,29 +135,26 @@ pub enum NymRewarderError { #[error("could not verify the blinded credential")] BlindVerificationFailure, - #[error("the same deposit transaction ({tx_hash}) has been used for multiple issued credentials! {first} and {other}")] - DuplicateDepositHash { - tx_hash: Hash, + #[error("the same deposit ({deposit_id}) has been used for multiple issued credentials! {first} and {other}")] + DuplicateDepositId { + deposit_id: u32, first: i64, other: i64, }, - #[error("could not find the deposit value in the event of transaction {tx_hash}")] - DepositValueNotFound { tx_hash: Hash }, + #[error("could not find the deposit details for deposit id {deposit_id}")] + DepositNotFound { deposit_id: u32 }, - #[error("could not find the deposit info in the event of transaction {tx_hash}")] - DepositInfoNotFound { tx_hash: Hash }, - - #[error("the provided deposit value of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit value of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositValue { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, - #[error("the provided deposit info of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit info of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositInfo { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs index 4df0395ec4..d497bd818a 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs @@ -11,11 +11,11 @@ use crate::rewarder::nyxd_client::NyxdClient; use crate::rewarder::storage::RewarderStorage; use bip39::rand::prelude::SliceRandom; use bip39::rand::thread_rng; -use nym_coconut::{ - hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, VerificationKey, -}; use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::bandwidth_credential_params; +use nym_compact_ecash::scheme::expiration_date_signatures::date_scalar; +use nym_compact_ecash::scheme::withdrawal::verify_partial_blind_signature; +use nym_compact_ecash::{Base58, G1Projective, VerificationKeyAuth}; +use nym_credentials::ecash::utils::EcashTime; use nym_crypto::asymmetric::ed25519; use nym_task::TaskClient; use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt}; @@ -69,10 +69,10 @@ impl CredentialIssuanceMonitor { credential_info: &IssuedCredentialBody, ) -> Result { let credential_id = credential_info.credential.id; - let deposit_tx = credential_info.credential.tx_hash; + let deposit_id = credential_info.credential.deposit_id; let prior_id = self .storage - .get_deposit_credential_id(issuer_identity.to_string(), deposit_tx.to_string()) + .get_deposit_credential_id(issuer_identity.to_string(), deposit_id) .await?; match prior_id { @@ -82,7 +82,7 @@ impl CredentialIssuanceMonitor { debug!("we have already verified this credential before"); Ok(true) } else { - error!("double signing detected!! used deposit {deposit_tx} for credentials {prior} and {credential_id}!!"); + error!("double signing detected!! used deposit {deposit_id} for credentials {prior} and {credential_id}!!"); self.storage .insert_double_signing_evidence( issuer_identity.to_string(), @@ -90,8 +90,8 @@ impl CredentialIssuanceMonitor { credential_info, ) .await?; - Err(NymRewarderError::DuplicateDepositHash { - tx_hash: deposit_tx, + Err(NymRewarderError::DuplicateDepositId { + deposit_id, first: prior, other: credential_id, }) @@ -105,48 +105,23 @@ impl CredentialIssuanceMonitor { issued_credential: &IssuedCredentialBody, ) -> Result<(), NymRewarderError> { // check if this deposit even exists - let deposit_tx = issued_credential.credential.tx_hash; + let deposit_id = issued_credential.credential.deposit_id; - let (deposit_value, deposit_info) = self - .nyxd_client - .get_deposit_transaction_attributes(deposit_tx) - .await?; + //not using value anymore, but it should still be there + let _ = self.nyxd_client.get_deposit_details(deposit_id).await?; trace!("deposit exists"); - // check if the deposit values match - let credential_value = issued_credential.credential.public_attributes.first(); - let credential_info = issued_credential.credential.public_attributes.get(1); - - if credential_value != Some(&deposit_value) { - return Err(NymRewarderError::InconsistentDepositValue { - tx_hash: deposit_tx, - request: credential_value.cloned(), - on_chain: deposit_value, - }); - } - trace!("credential values matches the deposit"); - - if credential_info != Some(&deposit_info) { - return Err(NymRewarderError::InconsistentDepositInfo { - tx_hash: deposit_tx, - request: credential_info.cloned(), - on_chain: deposit_info, - }); - } - trace!("credential info matches the deposit"); Ok(()) } fn verify_credential( &mut self, - vk: &VerificationKey, + vk: &VerificationKeyAuth, credential: &IssuedCredential, ) -> Result<(), NymRewarderError> { - let public_attributes = credential - .public_attributes - .iter() - .map(hash_to_scalar) - .collect::>(); + let public_attributes = [date_scalar( + credential.expiration_date.ecash_unix_timestamp(), + )]; #[allow(clippy::map_identity)] let attributes_refs = public_attributes.iter().collect::>(); @@ -168,7 +143,6 @@ impl CredentialIssuanceMonitor { // actually do verify the credential now if !verify_partial_blind_signature( - bandwidth_credential_params(), &public_attribute_commitments, &attributes_refs, &credential.blinded_partial_credential, @@ -181,7 +155,7 @@ impl CredentialIssuanceMonitor { Ok(()) } - #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit = %issued_credential.credential.tx_hash))] + #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit_id = %issued_credential.credential.deposit_id))] async fn validate_issued_credential( &mut self, issuer: &CredentialIssuer, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs index ce8b65b659..dd39df1cd5 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs @@ -6,7 +6,7 @@ use crate::rewarder::epoch::Epoch; use crate::rewarder::helpers::api_client; use crate::rewarder::nyxd_client::NyxdClient; use cosmwasm_std::{Addr, Decimal, Uint128}; -use nym_coconut::VerificationKey; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::{AccountId, Coin}; @@ -341,7 +341,7 @@ pub struct CredentialIssuer { pub public_key: ed25519::PublicKey, pub operator_account: AccountId, pub api_runner: String, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, } // safety: we're converting between different wrappers for bech32 addresses diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs index 540245b5bc..a75aaa0929 100644 --- a/nym-validator-rewarder/src/rewarder/nyxd_client.rs +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -1,18 +1,17 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::NymRewarderError; use crate::rewarder::credential_issuance::types::{addr_to_account_id, CredentialIssuer}; -use nym_coconut::{Base58, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO, DEPOSIT_VALUE, -}; use nym_coconut_dkg_common::types::Epoch; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; use nym_crypto::asymmetric::ed25519; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; -use nym_validator_client::nyxd::helpers::find_tx_attribute; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::{Deposit, DepositId}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, EcashQueryClient, PagedDkgQueryClient, +}; use nym_validator_client::nyxd::module_traits::staking::{ QueryHistoricalInfoResponse, QueryValidatorsResponse, }; @@ -110,7 +109,7 @@ impl NyxdClient { public_key: ed25519::PublicKey::from_base58_string(&info.ed25519_identity)?, operator_account: addr_to_account_id(share.owner), api_runner: share.announce_address, - verification_key: VerificationKey::try_from_bs58(share.share).map_err( + verification_key: VerificationKeyAuth::try_from_bs58(share.share).map_err( |source| NymRewarderError::MalformedPartialVerificationKey { runner: info.address.to_string(), source, @@ -123,22 +122,12 @@ impl NyxdClient { Ok(issuers) } - pub(crate) async fn get_deposit_transaction_attributes( + pub(crate) async fn get_deposit_details( &self, - tx_hash: Hash, - ) -> Result<(String, String), NymRewarderError> { - let tx = self.inner.read().await.get_tx(tx_hash).await?; - - // todo: we need to make it more concrete that the first attribute is the deposit value - // and the second one is the deposit info - let deposit_value = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(NymRewarderError::DepositValueNotFound { tx_hash })?; - - let deposit_info = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(NymRewarderError::DepositInfoNotFound { tx_hash })?; - - Ok((deposit_value, deposit_info)) + deposit_id: DepositId, + ) -> Result { + let res = self.inner.read().await.get_deposit(deposit_id).await?; + res.deposit + .ok_or(NymRewarderError::DepositNotFound { deposit_id }) } } diff --git a/nym-validator-rewarder/src/rewarder/storage/manager.rs b/nym-validator-rewarder/src/rewarder/storage/manager.rs index 453668c3ef..3efa3d9308 100644 --- a/nym-validator-rewarder/src/rewarder/storage/manager.rs +++ b/nym-validator-rewarder/src/rewarder/storage/manager.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::rewarder::epoch::Epoch; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; #[derive(Clone)] pub(crate) struct StorageManager { @@ -186,7 +187,7 @@ impl StorageManager { &self, operator_identity_bs58: String, credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -195,14 +196,14 @@ impl StorageManager { INSERT INTO validated_deposit ( operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?) "#, operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) @@ -214,16 +215,16 @@ impl StorageManager { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, sqlx::Error> { Ok(sqlx::query!( r#" SELECT credential_id FROM validated_deposit - WHERE operator_identity_bs58 = ? AND deposit_tx = ? + WHERE operator_identity_bs58 = ? AND deposit_id = ? "#, operator_identity_bs58, - deposit_tx + deposit_id ) .fetch_optional(&self.connection_pool) .await? @@ -235,7 +236,7 @@ impl StorageManager { operator_identity_bs58: String, credential_id: i64, original_credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -245,7 +246,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?, ?) @@ -253,7 +254,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs index c0463700d0..20969b7c93 100644 --- a/nym-validator-rewarder/src/rewarder/storage/mod.rs +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -7,6 +7,7 @@ use crate::rewarder::epoch::Epoch; use crate::rewarder::storage::manager::StorageManager; use crate::rewarder::{EpochRewards, RewardingResult}; use nym_validator_client::nym_api::IssuedCredentialBody; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -83,11 +84,11 @@ impl RewarderStorage { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, NymRewarderError> { Ok(self .manager - .get_deposit_credential_id(operator_identity_bs58, deposit_tx) + .get_deposit_credential_id(operator_identity_bs58, deposit_id) .await?) } @@ -100,7 +101,7 @@ impl RewarderStorage { .insert_validated_deposit( operator_identity_bs58, credential_info.credential.id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) @@ -119,7 +120,7 @@ impl RewarderStorage { operator_identity_bs58, credential_info.credential.id, original_credential_id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index 782791752e..00e9f1160f 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -48,9 +48,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(COCONUT_BANDWIDTH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 9e45694118..b545dd134b 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -48,9 +48,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(COCONUT_BANDWIDTH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 8708665bd2..f3d463f188 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -50,7 +50,7 @@ base64 = "0.13" zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index b657db8570..09dac6dc5e 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -29,6 +29,7 @@ bytecodec = { workspace = true } httpcodec = { workspace = true } bytes = { workspace = true } http = { workspace = true } +zeroize = { workspace = true } futures = { workspace = true } log = { workspace = true } diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index 96a064cf1d..36858ae654 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -18,10 +18,10 @@ async fn main() -> anyhow::Result<()> { .enable_credentials_mode() .build()?; - let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic)?; + let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic).await?; - // Get a bandwidth credential worth 1000000 unym for the mixnet_client - bandwidth_client.acquire(1000000).await?; + // Get a bandwidth credential for the mixnet_client + bandwidth_client.acquire().await?; // Connect using paid bandwidth credential let mut client = mixnet_client.connect_to_mixnet().await?; diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 72b6568b6d..00ad24dc9a 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -6,6 +6,7 @@ async fn main() { nym_bin_common::logging::setup_logging(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own + // let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); // Be able to get our client address diff --git a/sdk/rust/nym-sdk/src/bandwidth.rs b/sdk/rust/nym-sdk/src/bandwidth.rs index b1548dcb96..de4272ef6b 100644 --- a/sdk/rust/nym-sdk/src/bandwidth.rs +++ b/sdk/rust/nym-sdk/src/bandwidth.rs @@ -15,10 +15,10 @@ //! .build() //! .unwrap(); //! -//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).unwrap(); +//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).await.unwrap(); //! -//! // Get a bandwidth credential worth 1000000 unym for the mixnet_client -//! bandwidth_client.acquire(1000000).await.unwrap(); +//! // Get a bandwidth credential for the mixnet_client +//! bandwidth_client.acquire().await.unwrap(); //! //! // Connect using paid bandwidth credential //! let mut client = mixnet_client.connect_to_mixnet().await.unwrap(); @@ -41,4 +41,4 @@ mod client; -pub use client::{BandwidthAcquireClient, VoucherBlob}; +pub use client::BandwidthAcquireClient; diff --git a/sdk/rust/nym-sdk/src/bandwidth/client.rs b/sdk/rust/nym-sdk/src/bandwidth/client.rs index 5051e5a1d4..162866452a 100644 --- a/sdk/rust/nym-sdk/src/bandwidth/client.rs +++ b/sdk/rust/nym-sdk/src/bandwidth/client.rs @@ -1,18 +1,12 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::{Error, Result}; -use nym_bandwidth_controller::acquire::state::State; +use crate::error::Result; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; +use nym_credential_utils::utils::issue_credential; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::Coin; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; - -/// The serialized version of the yet untransformed bandwidth voucher. It can be used to complete -/// the acquirement process of a bandwidth credential. -/// Its serialized nature makes it easy to store and load it to e.g. disk. -pub type VoucherBlob = Vec; +use zeroize::Zeroizing; /// Represents a client that can be used to acquire bandwidth. You typically create one when you /// want to connect to the mixnet using paid coconut bandwidth credentials. @@ -20,9 +14,9 @@ pub type VoucherBlob = Vec; /// [`crate::mixnet::DisconnectedMixnetClient::create_bandwidth_client`] on the associated mixnet /// client. pub struct BandwidthAcquireClient<'a, St: Storage> { - network_details: NymNetworkDetails, client: DirectSigningHttpRpcNyxdClient, storage: &'a St, + client_id: Zeroizing, } impl<'a, St> BandwidthAcquireClient<'a, St> @@ -34,6 +28,7 @@ where network_details: NymNetworkDetails, mnemonic: String, storage: &'a St, + client_id: String, ) -> Result { let nyxd_url = network_details.endpoints[0].nyxd_url.as_str(); let config = nyxd::Config::try_from_nym_network_details(&network_details)?; @@ -44,40 +39,14 @@ where mnemonic.parse()?, )?; Ok(Self { - network_details, client, storage, + client_id: client_id.into(), }) } - /// Buy a credential worth amount utokens. If [`Error::UnconvertedDeposit`] is returned, it - /// means the tokens have been deposited, but the proper bandwidth credential hasn't yet been - /// created. A [`VoucherBlob`] is returned that can be used for a later recovery of the - /// associated bandwidth credential, using [`Self::recover`]. - pub async fn acquire(&self, amount: u128) -> Result<()> { - let amount = Coin::new(amount, &self.network_details.chain_details.mix_denom.base); - let state = nym_bandwidth_controller::acquire::deposit(&self.client, amount).await?; - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, &self.client, self.storage) - .await - .map_err(|reason| Error::UnconvertedDeposit { - reason, - voucher_blob: state.voucher.to_recovery_bytes(), - }) - } - - /// In case of an error in the mid of the acquire process, this function should be used for - /// later retries to recover the bandwidth credential, either immediately or after some time. - pub async fn recover(&self, voucher_blob: &VoucherBlob) -> Result<()> { - let voucher = IssuanceBandwidthCredential::try_from_recovered_bytes(voucher_blob) - .map_err(|_| Error::InvalidVoucherBlob)?; - let state = State::new(voucher); - nym_bandwidth_controller::acquire::get_bandwidth_voucher( - &state, - &self.client, - self.storage, - ) - .await?; - + pub async fn acquire(&self) -> Result<()> { + issue_credential(&self.client, self.storage, self.client_id.as_bytes()).await?; Ok(()) } } diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index e47b8b784f..7727583529 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -55,15 +55,6 @@ pub enum Error { #[error("socks5 channel could not be started")] Socks5NotStarted, - #[error( - "deposited funds were not converted to a deposit - {reason}; the voucher blob can be used for \ - later retry" - )] - UnconvertedDeposit { - reason: nym_bandwidth_controller::error::BandwidthControllerError, - voucher_blob: crate::bandwidth::VoucherBlob, - }, - #[error("bandwidth controller error: {0}")] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), @@ -88,6 +79,12 @@ pub enum Error { source: Box, }, + #[error(transparent)] + CredentialIssuanceError { + #[from] + source: nym_credential_utils::Error, + }, + #[error("loaded shared gateway key without providing information about what gateway it corresponds to")] GatewayWithUnknownEndpoint, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 57a8e85127..57a17c1569 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -65,7 +65,7 @@ pub use nym_client_core::{ }; pub use nym_credential_storage::{ ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage, - models::StoredIssuedCredential, storage::Storage as CredentialStorage, + models::StoredIssuedTicketbook, storage::Storage as CredentialStorage, }; pub use nym_crypto::asymmetric::ed25519; pub use nym_network_defaults::NymNetworkDetails; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index f22c20e072..f697553d18 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -551,17 +551,29 @@ where /// Creates an associated [`BandwidthAcquireClient`] that can be used to acquire bandwidth /// credentials for this client to consume. - pub fn create_bandwidth_client( + pub async fn create_bandwidth_client( &self, mnemonic: String, ) -> Result> { if !self.config.enabled_credentials_mode { return Err(Error::DisabledCredentialsMode); } + let client_id = self + .storage + .key_store() + .load_keys() + .await + .map_err(|e| Error::KeyStorageError { + source: Box::new(e), + })? + .identity_keypair() + .private_key() + .to_base58_string(); BandwidthAcquireClient::new( self.config.network_details.clone(), mnemonic, self.storage.credential_store(), + client_id, ) } diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index 3de91de6be..d269d5045d 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -15,6 +15,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -76,6 +77,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Sign to prove ownership of this network requester Sign(sign::Sign), @@ -132,6 +136,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), IpPacketRouterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::Sign(m) => sign::execute(&m).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), diff --git a/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..20c58a412c --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index d15d0e1621..11ef00cfd8 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -22,6 +22,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -86,6 +87,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -152,6 +156,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/service-providers/network-requester/src/cli/show_ticketbooks.rs b/service-providers/network-requester/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..b0e7f2febd --- /dev/null +++ b/service-providers/network-requester/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml new file mode 100644 index 0000000000..d8c52671df --- /dev/null +++ b/tools/internal/testnet-manager/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "testnet-manager" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +bip39.workspace = true +bs58.workspace = true +console = "0.15.8" +cw-utils.workspace = true +clap = { workspace = true, features = ["cargo", "derive"] } +indicatif = "0.17.8" +rand.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +tempfile = { workspace = true } +thiserror.workspace = true +time = { workspace = true, features = ["parsing", "formatting"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process"] } +toml = "0.8.14" +tracing.workspace = true +url.workspace = true +zeroize = { workspace = true, features = ["zeroize_derive"] } + + +nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-config = { path = "../../../common/config" } +nym-validator-client = { path = "../../../common/client-libs/validator-client" } +nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } +dkg-bypass-contract = { path = "dkg-bypass-contract" } + +# contracts: +nym-mixnet-contract-common = { path = "../../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" } +nym-vesting-contract-common = { path = "../../../common/cosmwasm-smart-contracts/vesting-contract" } +nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" } +nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } +nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-pemstore = { path = "../../../common/pemstore" } + + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/tools/internal/testnet-manager/Makefile b/tools/internal/testnet-manager/Makefile new file mode 100644 index 0000000000..74399c646a --- /dev/null +++ b/tools/internal/testnet-manager/Makefile @@ -0,0 +1,2 @@ +build-bypass-contract: + $(MAKE) -C dkg-bypass-contract build \ No newline at end of file diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs new file mode 100644 index 0000000000..cdd97f9505 --- /dev/null +++ b/tools/internal/testnet-manager/build.rs @@ -0,0 +1,25 @@ +use sqlx::{Connection, SqliteConnection}; +use std::env; + +#[tokio::main] +async fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{}/nym-api-example.sqlite", out_dir); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml new file mode 100644 index 0000000000..fdd8eac22b --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dkg-bypass-contract" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } + +nym-coconut-dkg-common = { path = "../../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-contracts-common = { path = "../../../../common/cosmwasm-smart-contracts/contracts-common" } \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile new file mode 100644 index 0000000000..2aa57e5ef9 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile @@ -0,0 +1,5 @@ +all: build + +build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown + wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs new file mode 100644 index 0000000000..7a454c60bb --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs @@ -0,0 +1,137 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::msg::MigrateMsg; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + entry_point, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, + StdResult, Storage, +}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, NodeIndex}; +use nym_coconut_dkg_common::verification_key::ContractVKShare; + +pub(crate) type Dealer<'a> = &'a Addr; + +pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + +pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +// use the same storage types as the actual DKG contract +pub(crate) const DEALERS_INDICES: Map = Map::new("dealer_index"); + +pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> = + Map::new("epoch_dealers"); + +type VKShareKey<'a> = (&'a Addr, EpochId); + +pub(crate) struct VkShareIndex<'a> { + pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, +} + +impl<'a> IndexList for VkShareIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.epoch_id]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn vk_shares<'a>() -> IndexedMap<'a, VKShareKey<'a>, ContractVKShare, VkShareIndex<'a>> { + let indexes = VkShareIndex { + epoch_id: MultiIndex::new(|_pk, d| d.epoch_id, "vksp", "vkse"), + }; + IndexedMap::new("vksp", indexes) +} + +pub(crate) fn next_node_index(store: &mut dyn Storage) -> StdResult { + // make sure we don't start from 0, otherwise all the crypto breaks (kinda) + let id: NodeIndex = NODE_INDEX_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NODE_INDEX_COUNTER.save(store, &id)?; + Ok(id) +} + +#[cw_serde] +pub enum EmptyMessage {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +/// Handle an incoming message +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result { + Ok(Default::default()) +} + +// LIMITATION: we're not storing dealings themselves +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { + // on migration immediately attempt to rewrite the storage + let threshold = (2 * msg.dealers.len() as u64 + 3 - 1) / 3; + let epoch = CURRENT_EPOCH.load(deps.storage)?; + assert_eq!(0, epoch.epoch_id); + + // set epoch data + THRESHOLD.save(deps.storage, &threshold)?; + EPOCH_THRESHOLDS.save(deps.storage, 0, &threshold)?; + let duration = epoch + .time_configuration + .state_duration(EpochState::InProgress); + + CURRENT_EPOCH.save( + deps.storage, + &Epoch { + state: EpochState::InProgress, + epoch_id: 0, + state_progress: Default::default(), + time_configuration: epoch.time_configuration, + deadline: duration.map(|d| env.block.time.plus_seconds(d)), + }, + )?; + + // set dealer data + for dealer in msg.dealers { + let node_index = next_node_index(deps.storage)?; + DEALERS_INDICES.save(deps.storage, &dealer.owner, &node_index)?; + + let registration_details = DealerRegistrationDetails { + bte_public_key_with_proof: "fakekey".to_string(), + ed25519_identity: dealer.ed25519_identity, + announce_address: dealer.announce.clone(), + }; + let vk_share = ContractVKShare { + share: dealer.vk, + announce_address: dealer.announce, + node_index, + owner: dealer.owner.clone(), + epoch_id: 0, + verified: true, + }; + + EPOCH_DEALERS_MAP.save(deps.storage, (0, &dealer.owner), ®istration_details)?; + vk_shares().save(deps.storage, (&dealer.owner, 0), &vk_share)?; + } + + Ok(Response::new()) +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs new file mode 100644 index 0000000000..69475aa119 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +pub mod msg; + +pub use msg::MigrateMsg; diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs new file mode 100644 index 0000000000..fbdc165d69 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use nym_coconut_dkg_common::verification_key::VerificationKeyShare; + +#[cw_serde] +pub struct FakeDealerData { + pub vk: VerificationKeyShare, + pub ed25519_identity: String, + pub announce: String, + pub owner: Addr, +} + +#[cw_serde] +pub struct MigrateMsg { + pub dealers: Vec, +} diff --git a/tools/internal/testnet-manager/migrations/01_initial_tables.sql b/tools/internal/testnet-manager/migrations/01_initial_tables.sql new file mode 100644 index 0000000000..6b621e2c99 --- /dev/null +++ b/tools/internal/testnet-manager/migrations/01_initial_tables.sql @@ -0,0 +1,40 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE metadata ( + id INTEGER PRIMARY KEY CHECK (id = 0), + latest_network_id INTEGER REFERENCES network(id), + + master_mnemonic TEXT NOT NULL, + rpc_endpoint TEXT NOT NULL +); + +CREATE TABLE network ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract(id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract(id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract(id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract(id), + + rewarder_address TEXT NOT NULL REFERENCES account(address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE contract ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + admin_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE account ( + address TEXT NOT NULL UNIQUE, + mnemonic TEXT NOT NULL +); \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/cli/build_info.rs b/tools/internal/testnet-manager/src/cli/build_info.rs new file mode 100644 index 0000000000..ed3d7ecadb --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/build_info.rs @@ -0,0 +1,17 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + println!("{}", args.output.format(&bin_info_owned!())); + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/bypass_dkg.rs b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs new file mode 100644 index 0000000000..a938e73d47 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use std::path::PathBuf; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + #[clap(long)] + signer_data_output_directory: Option, + + #[clap(long)] + network_name: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs new file mode 100644 index 0000000000..db9e4fd4ec --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let network = args + .common + .network_manager() + .await? + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await?; + + println!( + "add the following to your .env file: \n{}", + network.unchecked_to_env_file_section() + ); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs new file mode 100644 index 0000000000..47e59a8c11 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(long)] + signer_data_output_directory: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + let env = network.to_env_file_section(); + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + println!("add the following to your .env file: \n{env}",); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/load_network_details.rs b/tools/internal/testnet-manager/src/cli/load_network_details.rs new file mode 100644 index 0000000000..32aa708e2d --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/load_network_details.rs @@ -0,0 +1,36 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::NetworkManager; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(long)] + network_name: Option, + + #[clap(long)] + storage_path: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let storage = args.storage_path.unwrap_or_else(default_db_file); + + let network = NetworkManager::new(storage, None, None) + .await? + .load_existing_network(args.network_name) + .await?; + + println!( + "add the following to your .env file: \n{}", + network.to_env_file_section() + ); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_client.rs b/tools/internal/testnet-manager/src/cli/local_client.rs new file mode 100644 index 0000000000..f1ddc2fdc1 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_client.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-client` binary + #[clap(long)] + nym_client_bin: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmd = manager + .init_local_nym_client(args.nym_client_bin, &network) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmd) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs new file mode 100644 index 0000000000..f0f9a4475a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use tempfile::tempdir; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-api` binary + #[clap(long)] + nym_api_bin: PathBuf, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + number_of_apis: usize, + + #[clap(long)] + network_name: Option, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let endpoints = (0..args.number_of_apis) + .map(|i| format!("http://127.0.0.1:{}", 10000 + i).parse().unwrap()) + .collect::>(); + + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let temp_output = tempdir()?; + + let signer_details = manager + .attempt_bypass_dkg( + endpoints, + &network, + args.bypass_dkg_contract, + temp_output.path(), + ) + .await?; + + let run_cmds = manager + .setup_local_apis(args.nym_api_bin, &network, signer_details) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_nodes.rs b/tools/internal/testnet-manager/src/cli/local_nodes.rs new file mode 100644 index 0000000000..54d26fd6fb --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_nodes.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-node` binary + #[clap(long)] + nym_node_bin: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmds = manager + .init_local_nym_nodes(args.nym_node_bin, &network) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/migrate.rs b/tools/internal/testnet-manager/src/cli/migrate.rs new file mode 100644 index 0000000000..13075aca1a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/migrate.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use clap::Parser; +use nym_validator_client::nyxd::cosmwasm_client::types::{ContractCodeId, EmptyMsg}; + +// nyxd-style command so, for example `migrate ecash 123 '{}'` +#[derive(Debug, Parser)] +pub(crate) struct Args { + pub contract_name: String, + + pub code_id: ContractCodeId, + + pub message: serde_json::Value, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + todo!() +} diff --git a/tools/internal/testnet-manager/src/cli/mod.rs b/tools/internal/testnet-manager/src/cli/mod.rs new file mode 100644 index 0000000000..6fceb5b2b6 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/mod.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::NetworkManager; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; +use url::Url; + +mod build_info; +mod bypass_dkg; +mod initialise_new_network; +mod initialise_post_dkg_network; +mod load_network_details; +mod local_client; +mod local_ecash_apis; +mod local_nodes; +// mod migrate; + +#[derive(clap::Args, Debug)] +pub(crate) struct CommonArgs { + #[clap(long)] + master_mnemonic: Option, + + #[clap(long)] + rpc_endpoint: Option, + + #[clap(long)] + storage_path: Option, +} + +impl CommonArgs { + pub(crate) async fn network_manager(self) -> Result { + let storage = self.storage_path.unwrap_or_else(default_db_file); + NetworkManager::new(storage, self.master_mnemonic, self.rpc_endpoint).await + } +} + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> Result<(), NetworkManagerError> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::InitialiseNewNetwork(args) => initialise_new_network::execute(args).await, + Commands::LoadNetworkDetails(args) => load_network_details::execute(args).await, + Commands::BypassDkg(args) => bypass_dkg::execute(args).await, + Commands::InitialisePostDkgNetwork(args) => { + initialise_post_dkg_network::execute(args).await + } + Commands::CreateLocalEcashApis(args) => local_ecash_apis::execute(args).await, + Commands::BondLocalMixnet(args) => local_nodes::execute(args).await, + Commands::CreateLocalClient(args) => local_client::execute(args).await, + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Initialise new testnet network + InitialiseNewNetwork(initialise_new_network::Args), + + /// Attempt to load testnet network details + LoadNetworkDetails(load_network_details::Args), + + /// Attempt to bypass the DKG by ovewriting the contract state with pre-generated keys + BypassDkg(bypass_dkg::Args), + + /// Initialise new network and bypass the DKG. + /// Equivalent of running `initialise-new-network` and `bypass-dkg` separately. + InitialisePostDkgNetwork(initialise_post_dkg_network::Args), + + /// Attempt to create brand new network, in post DKG-state, using locally running nym-apis + CreateLocalEcashApis(local_ecash_apis::Args), + + /// Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands + BondLocalMixnet(local_nodes::Args), + + /// Initialise a locally run nym-client, adjust its config and output the run command + CreateLocalClient(local_client::Args), +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/tools/internal/testnet-manager/src/error.rs b/tools/internal/testnet-manager/src/error.rs new file mode 100644 index 0000000000..d85d8c7dcb --- /dev/null +++ b/tools/internal/testnet-manager/src/error.rs @@ -0,0 +1,106 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use nym_compact_ecash::CompactEcashError; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NetworkManagerError { + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("failed to parse mnemonic: {0}")] + Bip39Error(#[from] bip39::Error), + + #[error("failed to parse the url: {0}")] + MalformedUrl(#[from] url::ParseError), + + #[error("one of the account addresses was malformed - the developer was too lazy to propagate the actual error message with the address")] + MalformedAccountAddress, + + #[error(transparent)] + Nyxd(#[from] NyxdError), + + #[error("you need to set the master mnemonic on initial run")] + MnemonicNotSet, + + #[error("you need to set the rpc endpoint on initial run")] + RpcEndpointNotSet, + + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("failed to perform startup SQL migration - {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("could not find .wasm file for {name} contract under the provided directory")] + ContractWasmNotFound { name: String }, + + #[error("could not find code_id for {name} contract")] + ContractNotUploaded { name: String }, + + #[error("could not find contract admin for {name} contract")] + ContractAdminNotSet { name: String }, + + #[error("could not find address for {name} contract")] + ContractNotInitialised { name: String }, + + #[error("could not find build information for {name} contract")] + ContractNotQueried { name: String }, + + #[error("contract {name} has been build before build information got standarised. this is not supported")] + MissingBuildInfo { name: String }, + + #[error("there aren't any initialised networks in the storage")] + NoNetworksInitialised, + + #[error("you must specify at least a single api endpoint for the DKG")] + NoApiEndpoints, + + #[error("the DKG process has already been started on the target network")] + DkgAlreadyStarted, + + #[error("the target network is already in non-zero DKG epoch")] + NonZeroEpoch, + + #[error("the target already has registered cw4 members")] + ExistingCW4Members, + + #[error("failed to compute ecash keys: {source}")] + EcashCryptoFailure { + #[from] + source: CompactEcashError, + }, + + #[error("the provided contract path does not point to a valid .wasm file")] + MalformedDkgBypassContractPath, + + #[error("nym api initialisation returned non-zero return code")] + NymApiExecutionFailure, + + #[error("nym node initialisation returned non-zero return code")] + NymNodeExecutionFailure, + + #[error("nym client initialisation returned non-zero return code")] + NymClientExecutionFailure, + + #[error("failed to deserialise nym-api config: {0}")] + TomlDeserialisationFailure(#[from] toml::de::Error), + + #[error("failed to deserialise nym-node output: {0}")] + JsonDeserialisationFailure(#[from] serde_json::Error), + + #[error( + "the corresponding env file hasn't been generated. you need to setup local apis first." + )] + EnvFileNotGenerated, + + #[error("the default, pre-generated, .env file does not have the nym-api endpoint set!")] + NymApiEndpointMissing, + + #[error("timed out while waiting for some gateway to appear in the directory (you don't need to run it)")] + ApiGatewayWaitTimeout, + + #[error("timed out while waiting for the gateway to start receiving traffic (you need to actually run it!)")] + GatewayWaitTimeout, +} diff --git a/tools/internal/testnet-manager/src/helpers.rs b/tools/internal/testnet-manager/src/helpers.rs new file mode 100644 index 0000000000..0c38c8238d --- /dev/null +++ b/tools/internal/testnet-manager/src/helpers.rs @@ -0,0 +1,153 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use indicatif::{HumanDuration, ProgressBar}; +use nym_config::{must_get_home, NYM_DIR}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; +use std::future::Future; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; +use tokio::pin; +use tokio::time::interval; + +// struct Ctx<'a, T> { +// progress: ProgressTracker, +// network: LoadedNetwork<'a>, +// inner: T, +// } + +pub(crate) trait ProgressCtx { + fn progress_tracker(&self) -> &ProgressTracker; + + fn println>(&self, msg: I) { + self.progress_tracker().println(msg) + } + + fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_tracker().set_pb_prefix(prefix) + } + + fn set_pb_message(&self, msg: impl Into>) { + self.progress_tracker().set_pb_message(msg) + } + + async fn async_with_progress(&self, fut: F) -> T + where + F: Future, + { + async_with_progress(fut, &self.progress_tracker().progress_bar).await + } +} + +// pub(crate) trait NetworkCtx { +// fn loaded_network(&self) -> &LoadedNetwork; +// } + +#[derive(Serialize, Deserialize)] +pub struct RunCommands(pub(crate) Vec); + +impl Display for RunCommands { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for cmd in &self.0 { + writeln!(f, "{cmd}")? + } + Ok(()) + } +} + +pub(crate) struct ProgressTracker { + start: Instant, + pub(crate) progress_bar: ProgressBar, +} + +impl ProgressTracker { + pub(crate) fn new>(msg: I) -> Self { + let progress_bar = ProgressBar::new_spinner(); + progress_bar.println(msg); + + ProgressTracker { + start: Instant::now(), + progress_bar, + } + } + + pub(crate) fn println>(&self, msg: I) { + self.progress_bar.println(msg) + } + + pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_bar.set_prefix(prefix) + } + + pub(crate) fn set_pb_message(&self, msg: impl Into>) { + self.progress_bar.set_message(msg) + } + + pub(crate) fn output_run_commands(&self, cmds: &RunCommands) { + self.println("🏇 run the binaries with the following commands:"); + for cmd in &cmds.0 { + self.println(cmd) + } + } +} + +impl Default for ProgressTracker { + fn default() -> Self { + ProgressTracker { + start: Instant::now(), + progress_bar: ProgressBar::new_spinner(), + } + } +} + +impl Drop for ProgressTracker { + fn drop(&mut self) { + self.progress_bar.println(format!( + "✨ Done in {}", + HumanDuration(self.start.elapsed()) + )); + self.progress_bar.finish_and_clear(); + } +} + +pub(crate) fn default_storage_dir() -> PathBuf { + must_get_home().join(NYM_DIR).join("testnet-manager") +} + +pub(crate) fn default_db_file() -> PathBuf { + default_storage_dir().join("network-data.sqlite") +} + +pub(crate) async fn async_with_progress(fut: F, pb: &ProgressBar) -> T +where + F: Future, +{ + pb.tick(); + pin!(fut); + let mut update_interval = interval(Duration::from_millis(50)); + + loop { + tokio::select! { + _ = update_interval.tick() => { + pb.tick() + } + res = &mut fut => { + return res + } + } + } +} + +pub(crate) fn wasm_code>(path: P) -> Result, NetworkManagerError> { + let path = path.as_ref(); + assert!(path.exists()); + let mut file = std::fs::File::open(path)?; + let mut data = Vec::new(); + + file.read_to_end(&mut data)?; + Ok(data) +} diff --git a/tools/internal/testnet-manager/src/main.rs b/tools/internal/testnet-manager/src/main.rs new file mode 100644 index 0000000000..c92be01324 --- /dev/null +++ b/tools/internal/testnet-manager/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::Cli; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; + +pub(crate) mod cli; +pub(crate) mod error; +mod helpers; +mod manager; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // std::env::set_var( + // "RUST_LOG", + // "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn", + // ); + + let cli = Cli::parse(); + setup_tracing_logger(); + + cli.execute().await?; + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs new file mode 100644 index 0000000000..8295550dfc --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -0,0 +1,283 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use nym_mixnet_contract_common::ContractBuildInformation; +use nym_validator_client::nyxd::cosmwasm_client::types::{ + ContractCodeId, InstantiateResult, MigrateResult, UploadResult, +}; +use nym_validator_client::nyxd::{AccountId, Hash}; +use nym_validator_client::DirectSecp256k1HdWallet; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedNymContracts { + pub(crate) mixnet: LoadedContract, + pub(crate) vesting: LoadedContract, + pub(crate) ecash: LoadedContract, + pub(crate) cw3_multisig: LoadedContract, + pub(crate) cw4_group: LoadedContract, + pub(crate) dkg: LoadedContract, +} + +impl From for LoadedNymContracts { + fn from(value: NymContracts) -> Self { + LoadedNymContracts { + mixnet: value.mixnet.into(), + vesting: value.vesting.into(), + ecash: value.ecash.into(), + cw3_multisig: value.cw3_multisig.into(), + cw4_group: value.cw4_group.into(), + dkg: value.dkg.into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NymContracts { + pub(crate) mixnet: Contract, + pub(crate) vesting: Contract, + pub(crate) ecash: Contract, + pub(crate) cw3_multisig: Contract, + pub(crate) cw4_group: Contract, + pub(crate) dkg: Contract, +} + +impl NymContracts { + pub(crate) fn fake_iter(&self) -> Vec<&Contract> { + vec![ + &self.mixnet, + &self.vesting, + &self.ecash, + &self.cw3_multisig, + &self.cw4_group, + &self.dkg, + ] + } + + pub(crate) fn fake_iter_mut(&mut self) -> Vec<&mut Contract> { + vec![ + &mut self.mixnet, + &mut self.vesting, + &mut self.ecash, + &mut self.cw3_multisig, + &mut self.cw4_group, + &mut self.dkg, + ] + } + + pub(crate) fn count(&self) -> usize { + 6 + } + + pub(crate) fn discover_paths>( + &mut self, + base_path: P, + ) -> Result<(), NetworkManagerError> { + // just look in the base path, don't traverse + for entry_res in base_path.as_ref().read_dir()? { + let entry = entry_res?; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + if name.ends_with(".wasm") { + if name.contains("mixnet") { + self.mixnet.wasm_path = Some(entry.path()) + } + if name.contains("vesting") { + self.vesting.wasm_path = Some(entry.path()) + } + if name.contains("ecash") { + self.ecash.wasm_path = Some(entry.path()) + } + if name.contains("cw4") { + self.cw4_group.wasm_path = Some(entry.path()) + } + if name.contains("cw3") { + self.cw3_multisig.wasm_path = Some(entry.path()) + } + if name.contains("dkg") { + self.dkg.wasm_path = Some(entry.path()) + } + } + } + + if let Some(no_path) = self.fake_iter().iter().find(|c| c.wasm_path.is_none()) { + return Err(NetworkManagerError::ContractWasmNotFound { + name: no_path.name.clone(), + }); + } + + Ok(()) + } +} + +impl Default for NymContracts { + fn default() -> Self { + NymContracts { + mixnet: Contract::new("mixnet"), + vesting: Contract::new("vesting"), + ecash: Contract::new("ecash"), + cw4_group: Contract::new("cw4_group"), + cw3_multisig: Contract::new("cw3_multisig"), + dkg: Contract::new("dkg"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Account { + pub(crate) address: AccountId, + pub(crate) mnemonic: bip39::Mnemonic, +} + +impl Account { + pub(crate) fn new() -> Account { + let mnemonic = bip39::Mnemonic::generate(24).unwrap(); + // sure, we're using hardcoded prefix, but realistically this will never change + let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone()); + let acc = wallet.try_derive_accounts().unwrap().pop().unwrap(); + Account { + address: acc.address, + mnemonic, + } + } + + pub(crate) fn address(&self) -> AccountId { + self.address.clone() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalUploadInfo { + pub transaction_hash: Hash, + pub code_id: ContractCodeId, +} + +impl From for MinimalUploadInfo { + fn from(value: UploadResult) -> Self { + MinimalUploadInfo { + transaction_hash: value.transaction_hash, + code_id: value.code_id, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalInitInfo { + pub transaction_hash: Hash, + pub contract_address: AccountId, +} + +impl From for MinimalInitInfo { + fn from(value: InstantiateResult) -> Self { + MinimalInitInfo { + transaction_hash: value.transaction_hash, + contract_address: value.contract_address, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalMigrateInfo { + pub transaction_hash: Hash, +} + +impl From for MinimalMigrateInfo { + fn from(value: MigrateResult) -> Self { + MinimalMigrateInfo { + transaction_hash: value.transaction_hash, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedContract { + pub(crate) name: String, + pub(crate) address: AccountId, + pub(crate) admin_address: AccountId, + pub(crate) admin_mnemonic: bip39::Mnemonic, +} + +impl From for LoadedContract { + fn from(value: Contract) -> Self { + let admin = value.admin.expect("no admin set"); + LoadedContract { + name: value.name, + address: value.init_info.expect("uninitialised").contract_address, + admin_address: admin.address, + admin_mnemonic: admin.mnemonic, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Contract { + pub(crate) name: String, + pub(crate) wasm_path: Option, + pub(crate) upload_info: Option, + pub(crate) admin: Option, + pub(crate) init_info: Option, + pub(crate) migrate_info: Option, + pub(crate) build_info: Option, +} + +impl Contract { + pub(crate) fn new>(name: S) -> Self { + Contract { + name: name.into(), + wasm_path: None, + upload_info: None, + admin: None, + init_info: None, + migrate_info: None, + build_info: None, + } + } + + pub(crate) fn wasm_path(&self) -> Result<&PathBuf, NetworkManagerError> { + self.wasm_path + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractWasmNotFound { + name: self.name.clone(), + }) + } + + pub(crate) fn upload_info(&self) -> Result<&MinimalUploadInfo, NetworkManagerError> { + self.upload_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotUploaded { + name: self.name.clone(), + }) + } + + pub(crate) fn admin(&self) -> Result<&Account, NetworkManagerError> { + self.admin + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractAdminNotSet { + name: self.name.clone(), + }) + } + + pub(crate) fn init_info(&self) -> Result<&MinimalInitInfo, NetworkManagerError> { + self.init_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotInitialised { + name: self.name.clone(), + }) + } + + #[allow(dead_code)] + pub(crate) fn build_info(&self) -> Result<&ContractBuildInformation, NetworkManagerError> { + self.build_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotQueried { + name: self.name.clone(), + }) + } + + pub(crate) fn address(&self) -> Result<&AccountId, NetworkManagerError> { + self.init_info().map(|info| &info.contract_address) + } +} diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs new file mode 100644 index 0000000000..4cacc8acd4 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -0,0 +1,473 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use dkg_bypass_contract::msg::FakeDealerData; +use nym_compact_ecash::{ttp_keygen, Base58, KeyPairAuth}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::Addr; +use nym_pemstore::traits::PemStorableKey; +use nym_pemstore::{store_key, store_keypair, KeyPairPath}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, GroupSigningClient, PagedGroupQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm::ContractCodeId; +use nym_validator_client::nyxd::cw4::Member; +use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use rand::rngs::OsRng; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +pub(crate) struct EcashSigner { + pub(crate) ed25519_keypair: ed25519::KeyPair, + pub(crate) ecash_keypair: nym_compact_ecash::KeyPairAuth, + pub(crate) cosmos_account: Account, + pub(crate) endpoint: Url, +} + +#[derive(Default)] +pub(crate) struct EcashSignerPaths { + pub(crate) ecash_key: PathBuf, + pub(crate) ed25519_keys: KeyPairPath, + pub(crate) mnemonic_path: PathBuf, + pub(crate) endpoint_path: PathBuf, +} + +pub(crate) struct EcashSignerWithPaths { + pub(crate) data: EcashSigner, + pub(crate) paths: EcashSignerPaths, +} + +// perform the same serialisation as the nym-api keys +struct FakeDkgKey<'a> { + inner: &'a KeyPairAuth, +} + +impl<'a> FakeDkgKey<'a> { + fn new(inner: &'a KeyPairAuth) -> Self { + FakeDkgKey { inner } + } +} + +impl<'a> PemStorableKey for FakeDkgKey<'a> { + type Error = NetworkManagerError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + // our fake key is ALWAYS issued for epoch 0 + let mut bytes = vec![0u8; 8]; + bytes.append(&mut self.inner.secret_key().to_bytes()); + bytes + } + + fn from_bytes(_: &[u8]) -> Result { + unimplemented!("this is not meant to be ever called") + } +} + +impl EcashSignerWithPaths { + pub(crate) fn api_port(&self) -> u16 { + self.data.endpoint.port().unwrap() + } +} + +struct DkgSkipCtx<'a> { + progress: ProgressTracker, + network: &'a LoadedNetwork, + dkg_admin: DirectSigningHttpRpcNyxdClient, + ecash_signers: Vec, +} + +impl<'a> ProgressCtx for DkgSkipCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> DkgSkipCtx<'a> { + fn dkg_contract(&self) -> &AccountId { + &self.network.contracts.dkg.address + } + + fn new(network: &'a LoadedNetwork) -> Result { + let progress = ProgressTracker::new(format!( + "\n🥷 attempting to skip DKG on network '{}'", + network.name + )); + + Ok(DkgSkipCtx { + progress, + dkg_admin: network.dkg_signing_client()?, + network, + ecash_signers: vec![], + }) + } + + fn group_signing_client(&self) -> Result { + self.network.cw4_group_signing_client() + } + + fn admin_signing_client( + &self, + mnemonic: bip39::Mnemonic, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + mnemonic, + )?) + } +} + +impl NetworkManager { + fn generate_ecash_signer_data( + &self, + ctx: &mut DkgSkipCtx, + api_endpoints: Vec, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating ecash keys for all signers...", + style("[1/8]").bold().dim() + )); + + // generate required materials + let n = api_endpoints.len(); + let threshold = (2 * n + 3 - 1) / 3; + + let ecash_keys = ttp_keygen(threshold as u64, n as u64)?; + + let mut ecash_signers = Vec::new(); + let mut rng = OsRng; + for (endpoint, ecash_keypair) in api_endpoints.into_iter().zip(ecash_keys.into_iter()) { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + let data = EcashSigner { + ed25519_keypair, + ecash_keypair, + cosmos_account: Account::new(), + endpoint, + }; + ctx.println(format!( + "\t{} will be managed by {}", + data.endpoint, data.cosmos_account.address + )); + let full = EcashSignerWithPaths { + data, + paths: EcashSignerPaths::default(), + }; + ecash_signers.push(full) + } + ctx.ecash_signers = ecash_signers; + + ctx.println("\t✅ generated ecash keys for all signers"); + Ok(()) + } + + async fn validate_existing_contracts<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result { + ctx.println(format!( + "🔬 {}Validating the current DKG and group contracts...", + style("[2/8]").bold().dim() + )); + + ctx.set_pb_prefix("[1/3]"); + ctx.set_pb_message("checking DKG epoch data..."); + let epoch_fut = ctx.dkg_admin.get_current_epoch(); + let dkg_epoch = ctx.async_with_progress(epoch_fut).await?; + if dkg_epoch.epoch_id != 0 { + return Err(NetworkManagerError::NonZeroEpoch); + } + + if !dkg_epoch.state.is_waiting_initialisation() { + return Err(NetworkManagerError::DkgAlreadyStarted); + } + + ctx.set_pb_prefix("[2/3]"); + ctx.set_pb_message("retrieving DKG contract code_id..."); + let code_fut = ctx + .dkg_admin + .get_contract_code_history(&ctx.network.contracts.dkg.address); + let code_history = ctx.async_with_progress(code_fut).await?; + + // SAFETY: + // if this is empty our abci query is invalid since we have just queried the contract so it must exist + let current_code = code_history.last().unwrap().code_id; + ctx.println("\tthe DKG contract is all good!"); + + ctx.set_pb_prefix("[3/3]"); + ctx.set_pb_message("checking cw4 group members data..."); + let members_fut = ctx.dkg_admin.get_all_members(); + let members = ctx.async_with_progress(members_fut).await?; + if !members.is_empty() { + return Err(NetworkManagerError::ExistingCW4Members); + } + + ctx.println("\tthe group contract is all good!"); + ctx.println("\t✅ the existing contracts are all good!"); + + Ok(current_code) + } + + async fn persist_dkg_keys<'a, P: AsRef>( + &self, + ctx: &mut DkgSkipCtx<'a>, + output_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Persisting the signer keys...", + style("[3/8]").bold().dim() + )); + + ctx.set_pb_message("storing the signer data on disk..."); + + let output_dir = output_dir.as_ref(); + let pb = &ctx.progress.progress_bar; + + for signer in &mut ctx.ecash_signers { + let address = &signer.data.cosmos_account.address; + let url = &signer.data.endpoint; + let signer_dir = output_dir.join(address.to_string()); + fs::create_dir_all(&signer_dir)?; + + let fake_ecash_key = FakeDkgKey::new(&signer.data.ecash_keypair); + + let ecash_path = signer_dir.join("ecash"); + + let ed25519_paths = KeyPairPath { + private_key_path: signer_dir.join("ed25519"), + public_key_path: signer_dir.join("ed25519.pub"), + }; + + let mnemonic_path = signer_dir.join("mnemonic"); + let endpoint_path = signer_dir.join("announce_address"); + + store_key(&fake_ecash_key, &ecash_path)?; + store_keypair(&signer.data.ed25519_keypair, &ed25519_paths)?; + + fs::write( + &mnemonic_path, + &Zeroizing::new(signer.data.cosmos_account.mnemonic.to_string()), + )?; + fs::write(&endpoint_path, url.as_str())?; + + signer.paths.ecash_key = ecash_path; + signer.paths.ed25519_keys = ed25519_paths; + signer.paths.mnemonic_path = mnemonic_path; + signer.paths.endpoint_path = endpoint_path; + + pb.println(format!( + "\tpersisted {address} (endpoint: {url}) data under {}", + signer_dir.display() + )); + } + + ctx.println("\t✅ persisted all the signer keys!"); + Ok(()) + } + + async fn upload_bypass_contract<'a, P: AsRef>( + &self, + ctx: &DkgSkipCtx<'a>, + dkg_bypass_contract: P, + ) -> Result { + ctx.println(format!( + "🚚 {}Uploading the bypass contract...", + style("[4/8]").bold().dim() + )); + + ctx.set_pb_message("uploading the bypass contract..."); + + let res = self + .upload_contract( + &ctx.dkg_admin, + &ctx.progress.progress_bar, + dkg_bypass_contract, + ) + .await?; + + ctx.println("\t✅ uploaded the bypass contract!"); + + Ok(res.code_id) + } + + async fn migrate_to_bypass_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔀 {}Attempting to migrate into the bypass contract...", + style("[5/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = dkg_bypass_contract::MigrateMsg { + dealers: ctx + .ecash_signers + .iter() + .map(|signer| FakeDealerData { + vk: signer.data.ecash_keypair.verification_key().to_bs58(), + ed25519_identity: signer.data.ed25519_keypair.public_key().to_base58_string(), + announce: signer.data.endpoint.to_string(), + owner: Addr::unchecked(signer.data.cosmos_account.address.as_ref()), + }) + .collect(), + }; + + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating bypass DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ migrated the DKG into the bypass contract!"); + + Ok(()) + } + + async fn restore_dkg_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "↩️ {}Attempting to migrate back into the original DKG contract...", + style("[6/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = nym_coconut_dkg_common::msg::MigrateMsg {}; + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating initial DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ restored the original DKG contract!"); + + Ok(()) + } + + async fn add_group_members<'a>(&self, ctx: &DkgSkipCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "👪 {}Adding all the cw4 group members...", + style("[7/8]").bold().dim() + )); + + ctx.set_pb_message("⛽creating a new big cw4 family..."); + let admin = ctx.group_signing_client()?; + let new_members = ctx + .ecash_signers + .iter() + .map(|s| Member { + addr: s.data.cosmos_account.address.to_string(), + weight: 1, + }) + .collect(); + + let update_fut = admin.update_members(new_members, Vec::new(), None); + + ctx.async_with_progress(update_fut).await?; + ctx.println("\t✅ new cw4 group members got added"); + Ok(()) + } + + async fn transfer_signer_tokens<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the new signers...", + style("[8/8]").bold().dim() + )); + + let admin = ctx.admin_signing_client(self.admin.deref().clone())?; + + let mut receivers = Vec::new(); + for signer in &ctx.ecash_signers { + // send 101nym to the admin + receivers.push(( + signer.data.cosmos_account.address.clone(), + admin.mix_coins(101_000000), + )) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = admin.send_multiple( + receivers, + "signers token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + pub(crate) async fn attempt_bypass_dkg( + &self, + api_endpoints: Vec, + network: &LoadedNetwork, + dkg_bypass_contract: P1, + data_output_dir: P2, + ) -> Result, NetworkManagerError> + where + P1: AsRef, + P2: AsRef, + { + if api_endpoints.is_empty() { + return Err(NetworkManagerError::NoApiEndpoints); + } + + let dkg_bypass_contract = dkg_bypass_contract.as_ref(); + if !dkg_bypass_contract.is_file() { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + let Some(ext) = dkg_bypass_contract.extension() else { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + }; + if ext != "wasm" { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + + let mut ctx = DkgSkipCtx::new(network)?; + + self.generate_ecash_signer_data(&mut ctx, api_endpoints)?; + let current_code_id = self.validate_existing_contracts(&ctx).await?; + self.persist_dkg_keys(&mut ctx, data_output_dir).await?; + let new_code_id = self + .upload_bypass_contract(&ctx, dkg_bypass_contract) + .await?; + self.migrate_to_bypass_contract(&ctx, new_code_id).await?; + self.restore_dkg_contract(&ctx, current_code_id).await?; + self.add_group_members(&ctx).await?; + self.transfer_signer_tokens(&ctx).await?; + + Ok(ctx.ecash_signers) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs new file mode 100644 index 0000000000..89f6c94d25 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -0,0 +1,224 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::dkg_skip::EcashSignerWithPaths; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{ + must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_NYM_APIS_DIR, NYM_DIR, +}; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct LocalApisCtx<'a> { + nym_api_binary: PathBuf, + progress: ProgressTracker, + network: &'a LoadedNetwork, + signers: Vec, +} + +impl<'a> ProgressCtx for LocalApisCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalApisCtx<'a> { + fn signer_id(&self, signer: &EcashSignerWithPaths) -> String { + format!( + "{}-{}", + signer.data.cosmos_account.address, self.network.name + ) + } + + fn new( + nym_api_binary: PathBuf, + network: &'a LoadedNetwork, + signers: Vec, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local signing nym-APIs for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalApisCtx { + nym_api_binary, + network, + progress, + signers, + }) + } +} + +impl NetworkManager { + fn nym_api_config(&self, api_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_APIS_DIR) + .join(api_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn initialise_api<'a>( + &self, + ctx: &LocalApisCtx<'a>, + info: &EcashSignerWithPaths, + ) -> Result<(), NetworkManagerError> { + let address = &info.data.cosmos_account.address; + + ctx.set_pb_message(format!("initialising api {address}...")); + + let id = ctx.signer_id(info); + + // setup the binary itself + let mut child = Command::new(&ctx.nym_api_binary) + .args([ + "init", + "--id", + &id, + "--nyxd-validator", + ctx.network.rpc_endpoint.as_ref(), + "--mnemonic", + &Zeroizing::new(info.data.cosmos_account.mnemonic.to_string()), + "--enable-zk-nym", + "--announce-address", + info.data.endpoint.as_ref(), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .kill_on_drop(true) + .spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymApiExecutionFailure); + } + + // load the config (and do very nasty things to it) + let config_path = self.nym_api_config(&id); + let config_content = fs::read_to_string(config_path)?; + let parsed_config: toml::Table = toml::from_str(&config_content)?; + let storage_paths = &parsed_config["base"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed"); + + let priv_id = &storage_paths["private_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let pub_id = &storage_paths["public_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let ecash = &parsed_config["coconut_signer"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed")["coconut_key_path"] + .as_str() + .expect("nym-api config serialisation has changed"); + + // overwrite pre-generated files + fs::copy(&info.paths.ecash_key, ecash)?; + fs::copy(&info.paths.ed25519_keys.private_key_path, priv_id)?; + fs::copy(&info.paths.ed25519_keys.public_key_path, pub_id)?; + + ctx.println(format!("\t nym-API {address} got initialised")); + + Ok(()) + } + + async fn initialise_apis<'a>(&self, ctx: &LocalApisCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-apis...", + style("[1/1]").bold().dim() + )); + + for signer in &ctx.signers { + self.initialise_api(ctx, signer).await? + } + + ctx.println("\t✅ all APIs got initialised!"); + Ok(()) + } + + fn prepare_api_run_commands>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result { + let bin_canon = fs::canonicalize(&ctx.nym_api_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for signer in &ctx.signers { + let port = signer.api_port(); + let id = ctx.signer_id(signer); + + cmds.push(format!( + "ROCKET_PORT={port} {bin_canon_display} -c {env_canon_display} run --id {id}" + )); + } + Ok(RunCommands(cmds)) + } + + fn output_api_run_commands(&self, ctx: &LocalApisCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + fn prepare_env_file>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result<(), NetworkManagerError> { + let base_env = ctx.network.to_env_file_section(); + let updated_env = format!("{base_env}NYM_API={}", ctx.signers[0].data.endpoint); + + let env_file_path = env_file.as_ref(); + if let Some(parent) = env_file_path.parent() { + fs::create_dir_all(parent)?; + } + + let latest = self.default_latest_env_file_path(); + if fs::read_link(&latest).is_ok() { + fs::remove_file(&latest)?; + } + + let mut env_file = File::create(env_file_path)?; + env_file.write_all(updated_env.as_bytes())?; + + // make symlink for usability purposes + std::os::unix::fs::symlink(env_file_path, &latest)?; + + Ok(()) + } + + pub(crate) async fn setup_local_apis>( + &self, + nym_api_binary: P, + network: &LoadedNetwork, + signer_data: Vec, + ) -> Result { + let ctx = LocalApisCtx::new(nym_api_binary.as_ref().to_path_buf(), network, signer_data)?; + let env_file = ctx.network.default_env_file_path(); + + self.initialise_apis(&ctx).await?; + self.prepare_env_file(&ctx, &env_file)?; + let cmds = self.prepare_api_run_commands(&ctx, env_file)?; + self.output_api_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs new file mode 100644 index 0000000000..bd13c8a834 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -0,0 +1,265 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::NymApiClient; +use rand::{thread_rng, RngCore}; +use std::fs; +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::process::Command; +use tokio::time::sleep; +use url::Url; + +struct LocalClientCtx<'a> { + nym_client_binary: PathBuf, + client_id: String, + + progress: ProgressTracker, + network: &'a LoadedNetwork, +} + +impl<'a> ProgressCtx for LocalClientCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalClientCtx<'a> { + fn new( + nym_client_binary: PathBuf, + network: &'a LoadedNetwork, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-client for network '{}' over {}", + network.name, network.rpc_endpoint + )); + let mut rng = thread_rng(); + let client_id = format!("{}-client-{}", network.name, rng.next_u32()); + + Ok(LocalClientCtx { + nym_client_binary, + network, + progress, + client_id, + }) + } + + // hehe, that's disgusting, but it's not meant to be used by users + fn nym_api_url(&self) -> Result { + let env_file = fs::read_to_string(self.network.default_env_file_path())?; + for entry in env_file.lines() { + if let Some(raw_url) = entry.strip_prefix("NYM_API=") { + return Ok(raw_url.parse()?); + } + } + Err(NetworkManagerError::NymApiEndpointMissing) + } +} + +impl NetworkManager { + fn nym_client_config(&self, client_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join("clients") + .join(client_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn wait_for_api_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result { + // create api client + // hehe, that's disgusting, but it's not meant to be used by users + let api_url = ctx.nym_api_url()?; + ctx.set_pb_message(format!( + "⌛waiting for any gateway to appear in the directory ({api_url})..." + )); + + let api_client = NymApiClient::new(api_url); + + let wait_fut = async { + let inner_fut = async { + loop { + let mut gateways = match api_client.nym_api.get_basic_gateways(None).await { + Ok(gateways) => gateways, + Err(err) => { + ctx.println(format!( + "❌ {} {err}", + style("[API QUERY FAILURE]: ").bold().dim() + )); + continue; + } + }; + + if let Some(node) = gateways.nodes.pop() { + return SocketAddr::new(node.ip_addresses[0], node.entry.unwrap().ws_port); + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + match ctx.async_with_progress(wait_fut).await { + Ok(endpoint) => { + ctx.println(format!( + "\twe finally got a gateway in the directory! it's at: {endpoint}" + )); + Ok(endpoint) + } + Err(_) => Err(NetworkManagerError::ApiGatewayWaitTimeout), + } + } + + async fn wait_for_gateway_endpoint<'a>( + &self, + ctx: &LocalClientCtx<'a>, + gateway: SocketAddr, + ) -> Result<(), NetworkManagerError> { + ctx.set_pb_message(format!( + "⌛waiting for gateway at {gateway} to start receiving traffic..." + )); + + let wait_fut = async { + let inner_fut = async { + loop { + if TcpStream::connect(gateway).await.is_ok() { + break; + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + if ctx.async_with_progress(wait_fut).await.is_err() { + return Err(NetworkManagerError::GatewayWaitTimeout); + } + + ctx.println(format!( + "\tthe gateway at {gateway} has finally come online" + )); + + Ok(()) + } + + async fn wait_for_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + let endpoint = self.wait_for_api_gateway(ctx).await?; + self.wait_for_gateway_endpoint(ctx, endpoint).await + } + + async fn prepare_nym_client<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-client...", + style("[1/1]").bold().dim() + )); + + let env = ctx.network.default_env_file_path(); + let id = &ctx.client_id; + + self.wait_for_gateway(ctx).await?; + + ctx.set_pb_message(format!("initialising client {id}...")); + ctx.println(format!("\tinitialising client {id}...")); + let mut child = Command::new(&ctx.nym_client_binary) + .args([ + "-c", + &env.display().to_string(), + "init", + "--id", + id, + "--enabled-credentials-mode", + "true", + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .spawn()?; + + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymClientExecutionFailure); + } + + ctx.println(format!("\tupdating client {id} config...")); + + let config_path = self.nym_client_config(id); + let mut config_file = OpenOptions::new().append(true).open(config_path)?; + + // make the client ignore the performance of the nodes since we're not running network monitor + writeln!( + config_file, + r#" + +[debug.topology] +minimum_mixnode_performance = 0 +minimum_gateway_performance = 0 +"# + )?; + + ctx.println(format!("\t✅client {id} is ready to use!")); + + Ok(()) + } + + fn prepare_client_run_command( + &self, + ctx: &LocalClientCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_client_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let id = &ctx.client_id; + + Ok(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id}" + )) + } + + pub(crate) async fn init_local_nym_client>( + &self, + nym_client_binary: P, + network: &LoadedNetwork, + ) -> Result { + let ctx = LocalClientCtx::new(nym_client_binary.as_ref().to_path_buf(), network)?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.prepare_nym_client(&ctx).await?; + let cmd = self.prepare_client_run_command(&ctx)?; + + ctx.println("🏇 run the binary with the following commands:"); + ctx.println(&cmd); + + Ok(cmd) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs new file mode 100644 index 0000000000..578d72b0fe --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -0,0 +1,546 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::contract::Account; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::{ + construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Addr, Gateway, + Layer, LayerAssignment, MixNode, MixNodeCostParams, Percent, +}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::CosmWasmCoin; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct NymNode { + // host is always 127.0.0.1 + mix_port: u16, + verloc_port: u16, + http_port: u16, + clients_port: u16, + sphinx_key: String, + identity_key: String, + version: String, + + owner: Account, + bonding_signature: String, +} + +impl NymNode { + fn new_empty() -> NymNode { + NymNode { + mix_port: 0, + verloc_port: 0, + http_port: 0, + clients_port: 0, + sphinx_key: "".to_string(), + identity_key: "".to_string(), + version: "".to_string(), + owner: Account::new(), + bonding_signature: "".to_string(), + } + } + + fn pledge(&self) -> CosmWasmCoin { + CosmWasmCoin::new(100_000000, "unym") + } + + fn gateway(&self) -> Gateway { + Gateway { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + clients_port: self.clients_port, + location: "foomp".to_string(), + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + fn mixnode(&self) -> MixNode { + MixNode { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + verloc_port: self.verloc_port, + http_api_port: self.http_port, + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + fn cost_params(&self) -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), + } + } + + fn bonding_signature(&self) -> MessageSignature { + // this is a valid bs58 + self.bonding_signature.parse().unwrap() + } + + fn mixnode_bonding_payload(&self) -> String { + let payload = construct_mixnode_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.mixnode(), + self.cost_params(), + ); + payload.to_base58_string().unwrap() + } + + fn gateway_bonding_payload(&self) -> String { + let payload = construct_gateway_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.gateway(), + ); + payload.to_base58_string().unwrap() + } +} + +struct LocalNodesCtx<'a> { + nym_node_binary: PathBuf, + + progress: ProgressTracker, + network: &'a LoadedNetwork, + admin: DirectSigningHttpRpcNyxdClient, + + mix_nodes: Vec, + gateway: Option, +} + +impl<'a> ProgressCtx for LocalNodesCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalNodesCtx<'a> { + fn nym_node_id(&self, node: &NymNode) -> String { + format!("{}-{}", node.owner.address, self.network.name) + } + + fn new( + nym_node_binary: PathBuf, + network: &'a LoadedNetwork, + admin_mnemonic: bip39::Mnemonic, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-nodes for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalNodesCtx { + nym_node_binary, + network, + admin: DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + network.client_config()?, + network.rpc_endpoint.as_str(), + admin_mnemonic, + )?, + mix_nodes: Vec::new(), + progress, + gateway: None, + }) + } + + fn signing_node_owner( + &self, + node: &NymNode, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + node.owner.mnemonic.clone(), + )?) + } + + fn signing_rewarder(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .auxiliary_addresses + .mixnet_rewarder + .mnemonic + .clone(), + )?) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "node_type")] +pub enum BondingInformationV1 { + Mixnode(MixnodeBondingInformation), + Gateway(GatewayBondingInformation), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MixnodeBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GatewayBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) location: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Deserialize)] +struct ReducedSignatureOut { + encoded_signature: String, +} + +impl NetworkManager { + async fn initialise_nym_node<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + offset: u16, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let mut node = NymNode::new_empty(); + let env = ctx.network.default_env_file_path(); + let id = ctx.nym_node_id(&node); + + let output_dir = tempfile::tempdir()?; + let output_file_path = output_dir.path().join("bonding_info.json"); + + ctx.set_pb_message(format!("initialising node {id}...")); + let mix_port = 5000 + offset; + let verloc_port = 6000 + offset; + let clients_port = 7000 + offset; + let http_port = 8000 + offset; + + node.mix_port = mix_port; + node.verloc_port = verloc_port; + node.clients_port = clients_port; + node.http_port = http_port; + + let mut cmd = Command::new(&ctx.nym_node_binary); + cmd.args([ + "-c", + &env.display().to_string(), + "run", + "--id", + &id, + "--init-only", + "--public-ips", + "127.0.0.1", + "--http-bind-address", + &format!("127.0.0.1:{http_port}"), + "--mixnet-bind-address", + &format!("127.0.0.1:{mix_port}"), + "--verloc-bind-address", + &format!("127.0.0.1:{verloc_port}"), + "--entry-bind-address", + &format!("127.0.0.1:{clients_port}"), + "--mnemonic", + &Zeroizing::new(node.owner.mnemonic.to_string()), + "--local", + "--output", + "json", + "--bonding-information-output", + &output_file_path.display().to_string(), + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + if is_gateway { + cmd.args(["--mode", "entry"]); + } + + let mut child = cmd.spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + + let output_file = fs::File::open(&output_file_path)?; + let bonding_info: BondingInformationV1 = serde_json::from_reader(&output_file)?; + + match bonding_info { + BondingInformationV1::Mixnode(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + BondingInformationV1::Gateway(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + } + + ctx.set_pb_message(format!("generating bonding signature for node {id}...")); + + let msg = if is_gateway { + node.gateway_bonding_payload() + } else { + node.mixnode_bonding_payload() + }; + + let child = Command::new(&ctx.nym_node_binary) + .args([ + "--no-banner", + "sign", + "--id", + &id, + "--contract-msg", + &msg, + "--output", + "json", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .output(); + let out = ctx.async_with_progress(child).await?; + if !out.status.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + let signature: ReducedSignatureOut = serde_json::from_slice(&out.stdout)?; + node.bonding_signature = signature.encoded_signature; + + ctx.println(format!( + "\tinitialised node {} (gateway: {})", + node.identity_key, is_gateway + )); + + if is_gateway { + ctx.gateway = Some(node) + } else { + ctx.mix_nodes.push(node) + } + Ok(()) + } + + async fn initialise_nym_nodes<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-nodes...", + style("[1/4]").bold().dim() + )); + + // 3 mixnodes, 1 gateway; maybe at some point make it configurable + for i in 0..4 { + let is_gateway = i == 0; + self.initialise_nym_node(ctx, i, is_gateway).await?; + } + + ctx.println("\t✅ all nym nodes got initialised!"); + + Ok(()) + } + + async fn transfer_bonding_tokens<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the bond owners...", + style("[2/4]").bold().dim() + )); + + let mut receivers = Vec::new(); + for node in ctx + .mix_nodes + .iter() + .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) + { + // send 101nym to the owner + receivers.push((node.owner.address.clone(), ctx.admin.mix_coins(101_000000))) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = ctx.admin.send_multiple( + receivers, + "bond owners token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + async fn bond_node<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + node: &NymNode, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let prefix = if is_gateway { "[gateway]" } else { "[mixnode]" }; + ctx.set_pb_prefix(prefix); + + let id = ctx.nym_node_id(node); + ctx.set_pb_message(format!("attempting to bond node {id}...")); + + let owner = ctx.signing_node_owner(node)?; + + let bonding_fut = if is_gateway { + owner.bond_gateway( + node.gateway(), + node.bonding_signature(), + node.pledge().into(), + None, + ) + } else { + owner.bond_mixnode( + node.mixnode(), + node.cost_params(), + node.bonding_signature(), + node.pledge().into(), + None, + ) + }; + let res = ctx.async_with_progress(bonding_fut).await?; + ctx.println(format!( + "\t{id} bonded in transaction: {}", + res.transaction_hash + )); + + Ok(()) + } + + async fn bond_nym_nodes<'a>(&self, ctx: &LocalNodesCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "⛓️ {}Bonding the local nym-nodes...", + style("[3/4]").bold().dim() + )); + + self.bond_node(ctx, ctx.gateway.as_ref().unwrap(), true) + .await?; + for mix_node in &ctx.mix_nodes { + self.bond_node(ctx, mix_node, false).await?; + } + + ctx.println("\t✅ all nym nodes got bonded!"); + + Ok(()) + } + + async fn assign_to_active_set<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔌 {}Assigning mixnodes to the active set...", + style("[4/4]").bold().dim() + )); + + let rewarder = ctx.signing_rewarder()?; + + ctx.set_pb_message("starting epoch transition..."); + let fut = rewarder.begin_epoch_transition(None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("reconciling (no) epoch events..."); + let fut = rewarder.reconcile_epoch_events(None, None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("finally assigning the active set..."); + let fut = rewarder.get_rewarding_parameters(); + let rewarding_params = ctx.async_with_progress(fut).await?; + let active_set_size = rewarding_params.active_set_size; + + let layer_assignment = vec![ + LayerAssignment::new(1, Layer::One), + LayerAssignment::new(2, Layer::Two), + LayerAssignment::new(3, Layer::Three), + ]; + let fut = rewarder.advance_current_epoch(layer_assignment, active_set_size, None); + ctx.async_with_progress(fut).await?; + + Ok(()) + } + + fn prepare_nym_nodes_run_commands( + &self, + ctx: &LocalNodesCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_node_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for node in ctx + .mix_nodes + .iter() + .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) + { + let id = ctx.nym_node_id(node); + cmds.push(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + )); + } + + Ok(RunCommands(cmds)) + } + + fn output_nym_nodes_run_commands(&self, ctx: &LocalNodesCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + pub(crate) async fn init_local_nym_nodes>( + &self, + nym_node_binary: P, + network: &LoadedNetwork, + ) -> Result { + let mut ctx = LocalNodesCtx::new( + nym_node_binary.as_ref().to_path_buf(), + network, + self.admin.deref().clone(), + )?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.initialise_nym_nodes(&mut ctx).await?; + self.transfer_bonding_tokens(&ctx).await?; + self.bond_nym_nodes(&ctx).await?; + self.assign_to_active_set(&ctx).await?; + let cmds = self.prepare_nym_nodes_run_commands(&ctx)?; + self.output_nym_nodes_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/mod.rs b/tools/internal/testnet-manager/src/manager/mod.rs new file mode 100644 index 0000000000..5502bc4fdd --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/mod.rs @@ -0,0 +1,127 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, default_storage_dir, wasm_code}; +use crate::manager::network::LoadedNetwork; +use crate::manager::storage::NetworkManagerStorage; +use bip39::rand::prelude::SliceRandom; +use bip39::rand::thread_rng; +use indicatif::ProgressBar; +use nym_config::defaults::NymNetworkDetails; +use nym_validator_client::nyxd::cosmwasm_client::types::UploadResult; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +mod contract; +mod dkg_skip; +mod local_apis; +mod local_client; +mod local_nodes; +pub(crate) mod network; +mod network_init; +pub(crate) mod storage; + +pub(crate) struct NetworkManager { + admin: Zeroizing, + storage: NetworkManagerStorage, + rpc_endpoint: Url, +} + +impl NetworkManager { + pub(crate) async fn new>( + database_path: P, + mnemonic: Option, + rpc_endpoint: Option, + ) -> Result { + let storage = NetworkManagerStorage::init(database_path).await?; + + let (mnemonic, rpc_endpoint) = if !storage.metadata_set().await? { + let mnemonic = mnemonic.ok_or(NetworkManagerError::MnemonicNotSet)?; + let rpc_endpoint = rpc_endpoint.ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + storage + .set_initial_metadata(&mnemonic, &rpc_endpoint) + .await?; + (mnemonic, rpc_endpoint) + } else { + let mnemonic = storage + .get_master_mnemonic() + .await? + .ok_or(NetworkManagerError::MnemonicNotSet)?; + + let rpc_endpoint = storage + .get_rpc_endpoint() + .await? + .ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + (mnemonic, rpc_endpoint) + }; + + Ok(NetworkManager { + admin: Zeroizing::new(mnemonic), + storage, + rpc_endpoint, + }) + } + + pub fn default_latest_env_file_path(&self) -> PathBuf { + default_storage_dir().join("latest.env") + } + + #[allow(unused)] + pub(crate) fn query_client( + &self, + network: &LoadedNetwork, + ) -> Result { + let network_details = NymNetworkDetails::from(network); + let config = Config::try_from_nym_network_details(&network_details)?; + + Ok(QueryHttpRpcNyxdClient::connect( + config, + self.rpc_endpoint.as_str(), + )?) + } + + fn get_network_name(&self, user_provided: Option) -> String { + user_provided.unwrap_or_else(|| { + // a hack to get human-readable words without extra deps : ) + let mut rng = thread_rng(); + + let words = bip39::Language::English.word_list(); + let first = words.choose(&mut rng).unwrap(); + let second = words.choose(&mut rng).unwrap(); + format!("{first}-{second}") + }) + } + + async fn upload_contract>( + &self, + admin: &DirectSigningHttpRpcNyxdClient, + pb: &ProgressBar, + path: P, + ) -> Result { + let wasm = wasm_code(path)?; + let upload_future = admin.upload(wasm, "contract upload from testnet-manager", None); + + async_with_progress(upload_future, pb) + .await + .map_err(Into::into) + } + + pub(crate) async fn load_existing_network( + &self, + network_name: Option, + ) -> Result { + let network_name = if let Some(explicit) = network_name { + explicit + } else { + self.storage.get_latest_network_name().await? + }; + + self.storage.try_load_network(&network_name).await + } +} diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs new file mode 100644 index 0000000000..aeda4a344d --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -0,0 +1,200 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::contract::{Account, LoadedNymContracts, NymContracts}; +use nym_config::defaults::{NymNetworkDetails, ValidatorDetails}; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use time::OffsetDateTime; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Network { + pub name: String, + + pub rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + + pub contracts: NymContracts, + + pub auxiliary_addresses: SpecialAddresses, +} + +impl Network { + pub fn unchecked_to_env_file_section(&self) -> String { + format!( + "CONFIGURED=true\n\ +\n\ +BECH32_PREFIX=n\n\ +MIX_DENOM=unym\n\ +MIX_DENOM_DISPLAY=nym\n\ +STAKE_DENOM=unyx\n\ +STAKE_DENOM_DISPLAY=nyx\n\ +DENOMS_EXPONENT=6\n\ +\n\ +REWARDING_VALIDATOR_ADDRESS={}\n\ +MIXNET_CONTRACT_ADDRESS={}\n\ +VESTING_CONTRACT_ADDRESS={}\n\ +ECASH_CONTRACT_ADDRESS={}\n\ +GROUP_CONTRACT_ADDRESS={}\n\ +MULTISIG_CONTRACT_ADDRESS={}\n\ +COCONUT_DKG_CONTRACT_ADDRESS={}\n\ +NYXD={}\n\ +", + self.auxiliary_addresses.mixnet_rewarder.address, + self.contracts.mixnet.address().unwrap(), + self.contracts.vesting.address().unwrap(), + self.contracts.ecash.address().unwrap(), + self.contracts.cw4_group.address().unwrap(), + self.contracts.cw3_multisig.address().unwrap(), + self.contracts.dkg.address().unwrap(), + self.rpc_endpoint, + ) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct LoadedNetwork { + pub(crate) name: String, + + pub(crate) rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub(crate) created_at: OffsetDateTime, + + pub(crate) contracts: LoadedNymContracts, + + pub(crate) auxiliary_addresses: SpecialAddresses, +} + +impl From for LoadedNetwork { + fn from(value: Network) -> Self { + LoadedNetwork { + name: value.name, + rpc_endpoint: value.rpc_endpoint, + created_at: value.created_at, + contracts: value.contracts.into(), + auxiliary_addresses: value.auxiliary_addresses, + } + } +} + +impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { + fn from(value: &'a LoadedNetwork) -> Self { + let contracts = nym_config::defaults::NymContracts { + mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), + vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + ecash_contract_address: Some(value.contracts.ecash.address.to_string()), + group_contract_address: Some(value.contracts.cw4_group.address.to_string()), + multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), + coconut_dkg_contract_address: Some(value.contracts.dkg.address.to_string()), + }; + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), + endpoints: vec![ValidatorDetails { + nyxd_url: value.rpc_endpoint.to_string(), + websocket_url: None, + api_url: None, + }], + contracts, + explorer_api: None, + } + } +} + +impl LoadedNetwork { + pub fn default_env_file_path(&self) -> PathBuf { + default_storage_dir() + .join(&self.name) + .join(format!("{}.env", &self.name)) + } + + #[allow(dead_code)] + pub fn query_client(&self) -> Result { + Ok(QueryHttpRpcNyxdClient::connect( + self.client_config()?, + self.rpc_endpoint.as_str(), + )?) + } + + pub fn dkg_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.dkg.admin_mnemonic.clone(), + )?) + } + + pub fn client_config(&self) -> Result { + let network_details = NymNetworkDetails::from(self); + let config = Config::try_from_nym_network_details(&network_details)?; + Ok(config) + } + + pub fn cw4_group_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.cw4_group.admin_mnemonic.clone(), + )?) + } + + pub fn to_env_file_section(&self) -> String { + format!( + "CONFIGURED=true\n\ +\n\ +BECH32_PREFIX=n\n\ +MIX_DENOM=unym\n\ +MIX_DENOM_DISPLAY=nym\n\ +STAKE_DENOM=unyx\n\ +STAKE_DENOM_DISPLAY=nyx\n\ +DENOMS_EXPONENT=6\n\ +\n\ +REWARDING_VALIDATOR_ADDRESS={}\n\ +MIXNET_CONTRACT_ADDRESS={}\n\ +VESTING_CONTRACT_ADDRESS={}\n\ +ECASH_CONTRACT_ADDRESS={}\n\ +GROUP_CONTRACT_ADDRESS={}\n\ +MULTISIG_CONTRACT_ADDRESS={}\n\ +COCONUT_DKG_CONTRACT_ADDRESS={}\n\ +NYXD={}\n\ +", + self.auxiliary_addresses.mixnet_rewarder.address, + self.contracts.mixnet.address, + self.contracts.vesting.address, + self.contracts.ecash.address, + self.contracts.cw4_group.address, + self.contracts.cw3_multisig.address, + self.contracts.dkg.address, + self.rpc_endpoint, + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpecialAddresses { + pub ecash_holding_account: Account, + pub mixnet_rewarder: Account, +} + +impl Default for SpecialAddresses { + fn default() -> Self { + SpecialAddresses { + ecash_holding_account: Account::new(), + mixnet_rewarder: Account::new(), + } + } +} diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs new file mode 100644 index 0000000000..baeb614433 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -0,0 +1,701 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::Network; +use crate::manager::NetworkManager; +use console::style; +use cw_utils::Threshold; +use indicatif::HumanDuration; +use nym_coconut_dkg_common::types::TimeConfiguration; +use nym_config::defaults::NymNetworkDetails; +use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use nym_validator_client::nyxd::Config; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::ops::Deref; +use std::path::Path; +use std::time::Duration; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; +use url::Url; + +struct InitCtx { + progress: ProgressTracker, + network: Network, + admin: DirectSigningHttpRpcNyxdClient, +} + +impl InitCtx { + fn dummy_client_config() -> Result { + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + let network_details = NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), // does this matter? + endpoints: vec![], + contracts: Default::default(), + explorer_api: None, + }; + Ok(Config::try_from_nym_network_details(&network_details)?) + } + + fn new( + network_name: String, + admin_mnemonic: bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result { + let admin = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + rpc_endpoint.as_str(), + admin_mnemonic, + )?; + + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new testnet '{network_name}' over {rpc_endpoint}", + )); + + Ok(InitCtx { + progress, + network: Network { + name: network_name, + rpc_endpoint: rpc_endpoint.clone(), + created_at: OffsetDateTime::now_utc(), + contracts: Default::default(), + auxiliary_addresses: Default::default(), + }, + admin, + }) + } + + fn mixnet_signing_client(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network.contracts.mixnet.admin()?.mnemonic.clone(), + )?) + } + + fn multisig_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .contracts + .cw3_multisig + .admin()? + .mnemonic + .clone(), + )?) + } +} + +impl ProgressCtx for InitCtx { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl NetworkManager { + fn mixnet_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_mixnet_contract_common::MigrateMsg { + vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()), + }) + } + + fn multisig_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::MigrateMsg { + coconut_bandwidth_address: ctx.network.contracts.ecash.address()?.to_string(), + coconut_dkg_address: ctx.network.contracts.dkg.address()?.to_string(), + }) + } + + fn mixnet_init_message( + &self, + ctx: &InitCtx, + custom_epoch_duration: Option, + ) -> Result { + Ok(nym_mixnet_contract_common::InstantiateMsg { + rewarding_validator_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER \/ + vesting_contract_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER /\ + rewarding_denom: ctx.admin.mix_coin(0).denom, + epochs_in_interval: 720, + epoch_duration: custom_epoch_duration.unwrap_or(Duration::from_secs(60 * 60)), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(), + initial_staking_supply: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::from_percentage_value(50).unwrap(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_size: 240, + active_set_size: 240, + }, + }) + } + + fn vesting_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_vesting_contract_common::InitMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + mix_denom: ctx.admin.mix_coin(0).denom, + }) + } + + fn dkg_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_coconut_dkg_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + time_configuration: Some(TimeConfiguration { + public_key_submission_time_secs: 3600, + dealing_exchange_time_secs: 3600, + verification_key_submission_time_secs: 3600, + verification_key_validation_time_secs: 3600, + verification_key_finalization_time_secs: 3600, + in_progress_time_secs: 10000000000, + }), + mix_denom: ctx.admin.mix_coin(0).denom, + key_size: 5, + }) + } + + fn ecash_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_ecash_contract_common::msg::InstantiateMsg { + holding_account: ctx + .network + .auxiliary_addresses + .ecash_holding_account + .address + .to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + mix_denom: ctx.admin.mix_coin(0).denom, + }) + } + + fn cw3_multisig_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + + // PLACEHOLDER \/ + coconut_bandwidth_contract_address: ctx + .network + .contracts + .cw4_group + .address()? + .to_string(), + coconut_dkg_contract_address: ctx.network.contracts.cw4_group.address()?.to_string(), + // PLACEHOLDER /\ + threshold: Threshold::AbsolutePercentage { + percentage: "0.67".parse().unwrap(), + }, + max_voting_period: cw_utils::Duration::Time(3600), + executor: None, + proposal_deposit: None, + }) + } + + fn cw4_group_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_group_contract_common::msg::InstantiateMsg { + admin: Some( + ctx.network + .contracts + .cw4_group + .admin()? + .address() + .to_string(), + ), + // TODO: prepopulate + members: vec![], + }) + } + + fn find_contracts>( + &self, + ctx: &mut InitCtx, + base_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.network.contracts.discover_paths(base_dir)?; + + ctx.println(format!( + "🔍 {}Locating .wasm files...", + style("[1/8]").bold().dim() + )); + ctx.println(format!( + "\tdiscovered mixnet contract at '{}'", + ctx.network.contracts.mixnet.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered vesting contract at '{}'", + ctx.network.contracts.vesting.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered ecash contract at '{}'", + ctx.network.contracts.ecash.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw4_group contract at '{}'", + ctx.network.contracts.cw4_group.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw3_multisig contract at '{}'", + ctx.network.contracts.cw3_multisig.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered dkg contract at '{}'", + ctx.network.contracts.dkg.wasm_path()?.display() + )); + + ctx.println("\t✅ found all the contracts!"); + + Ok(()) + } + + async fn upload_contracts(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🚚 {}Uploading contracts...", + style("[2/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("uploading {name} contract...")); + let upload_res = self + .upload_contract( + &ctx.admin, + &ctx.progress.progress_bar, + &contract.wasm_path()?, + ) + .await?; + pb.println(format!( + "\t{name} contract uploaded with code: {}", + upload_res.code_id + )); + contract.upload_info = Some(upload_res.into()); + } + + ctx.println("\t✅ uploaded all the contracts!"); + + Ok(()) + } + + fn create_contract_admins_mnemonics( + &self, + ctx: &mut InitCtx, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating admin mnemonics...", + style("[3/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("generating admin mnemonic for {name} contract...")); + let admin = Account::new(); + pb.println(format!( + "\t{} is going to be admin for the {name} contract", + admin.address + )); + contract.admin = Some(admin) + } + + ctx.println("\t✅ generated all admin mnemonics!"); + + Ok(()) + } + + async fn transfer_admin_tokens(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the admin accounts...", + style("[4/8]").bold().dim() + )); + + let mut receivers = Vec::new(); + for contract in ctx.network.contracts.fake_iter() { + // send 10nym to the admin + receivers.push((contract.admin()?.address(), ctx.admin.mix_coins(10_000000))) + } + + // also send them to the rewarder + receivers.push(( + ctx.network.auxiliary_addresses.mixnet_rewarder.address(), + ctx.admin.mix_coins(10_000000), + )); + + ctx.set_pb_message("attempting to send admin tokens..."); + + let send_future = + ctx.admin + .send_multiple(receivers, "admin token transfer from testnet-manager", None); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + + Ok(()) + } + + async fn instantiate_contracts( + &self, + ctx: &mut InitCtx, + custom_epoch_duration: Option, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💽 {}Instantiating all the contracts...", + style("[5/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + // mixnet + ctx.set_pb_prefix(format!("[1/{total}]")); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let admin = ctx.network.contracts.mixnet.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.mixnet_init_message(ctx, custom_epoch_duration)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.mixnet.init_info = Some(res.into()); + + // vesting + ctx.set_pb_prefix(format!("[2/{total}]")); + let name = &ctx.network.contracts.vesting.name; + let code_id = ctx.network.contracts.vesting.upload_info()?.code_id; + let admin = ctx.network.contracts.vesting.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.vesting_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.vesting.init_info = Some(res.into()); + + // group + ctx.set_pb_prefix(format!("[3/{total}]")); + let name = &ctx.network.contracts.cw4_group.name; + let code_id = ctx.network.contracts.cw4_group.upload_info()?.code_id; + let admin = ctx.network.contracts.cw4_group.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw4_group_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw4_group.init_info = Some(res.into()); + + // multisig + ctx.set_pb_prefix(format!("[4/{total}]")); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let admin = ctx.network.contracts.cw3_multisig.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw3_multisig_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw3_multisig.init_info = Some(res.into()); + + // dkg + ctx.set_pb_prefix(format!("[5/{total}]")); + let name = &ctx.network.contracts.dkg.name; + let code_id = ctx.network.contracts.dkg.upload_info()?.code_id; + let admin = ctx.network.contracts.dkg.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.dkg_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.dkg.init_info = Some(res.into()); + + // ecash + ctx.set_pb_prefix(format!("[6/{total}]")); + let name = &ctx.network.contracts.ecash.name; + let code_id = ctx.network.contracts.ecash.upload_info()?.code_id; + let admin = ctx.network.contracts.ecash.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.ecash_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.ecash.init_info = Some(res.into()); + + ctx.println("\t✅ instantiated all the contracts!"); + + Ok(()) + } + + async fn perform_final_migrations(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🧹 {}Performing final migrations and contract cleanup...", + style("[6/8]").bold().dim() + )); + + // migrate mixnet + ctx.set_pb_prefix("[1/2]"); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let address = ctx.network.contracts.mixnet.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.mixnet_migrate_message(ctx)?; + let client = ctx.mixnet_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.mixnet.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + // migrate multisig + ctx.set_pb_prefix("[2/2]"); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let address = ctx.network.contracts.cw3_multisig.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.multisig_migrate_message(ctx)?; + let client = ctx.multisig_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.cw3_multisig.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + ctx.println("\t✅ performed all the needed migrations!"); + + Ok(()) + } + + async fn get_build_info(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🏗️ {}Obtaining contracts build information", + style("[7/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + let address = contract.address()?; + pb.set_message(format!("querying {name} contract...")); + let build_info_fut = ctx.admin.try_get_contract_build_information(address); + let build_info = async_with_progress(build_info_fut, &ctx.progress.progress_bar) + .await + .ok_or_else(|| NetworkManagerError::MissingBuildInfo { + name: name.to_string(), + })?; + + let now = OffsetDateTime::now_utc(); + // SAFETY: all the information saved in our contracts should be well-formed + let commit_timestamp = OffsetDateTime::parse(&build_info.commit_timestamp, &Rfc3339) + .expect("malformed commit timestamp"); + + let age = now - commit_timestamp; + + pb.println(format!( + "\t{name} contract was built from branch: {} (sha: {}); age: {}", + build_info.commit_branch, + build_info.commit_sha, + HumanDuration(age.unsigned_abs()) + )); + + if age > time::Duration::days(30) { + pb.println(format!( + "\t\t️☠️️ {}", + style("this commit is ANCIENT - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(7) { + pb.println(format!( + "\t\t️❗️ {}", + style("this commit is rather old - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(1) { + pb.println(format!( + "\t\t️️⚠️ {}", + style("this commit seems outdated - please double check if this is intended") + .bold() + .yellow() + )) + } + + contract.build_info = Some(build_info); + } + + ctx.println("\t✅ updated all contract metadata!"); + + Ok(()) + } + + async fn persist_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Storing all the results in the database", + style("[8/8]").bold().dim() + )); + + ctx.set_pb_message("attempting to persist network data..."); + let save_future = self.storage.persist_network(&ctx.network); + ctx.async_with_progress(save_future).await?; + + ctx.println("\t✅ the network information got persisted in the database for future use"); + + Ok(()) + } + + pub(crate) async fn initialise_new_network>( + &self, + contracts: P, + network_name: Option, + custom_epoch_duration: Option, + ) -> Result { + let network_name = self.get_network_name(network_name); + let mut ctx = InitCtx::new(network_name, self.admin.deref().clone(), &self.rpc_endpoint)?; + + self.find_contracts(&mut ctx, contracts)?; + self.upload_contracts(&mut ctx).await?; + self.create_contract_admins_mnemonics(&mut ctx)?; + self.transfer_admin_tokens(&ctx).await?; + self.instantiate_contracts(&mut ctx, custom_epoch_duration) + .await?; + self.perform_final_migrations(&mut ctx).await?; + self.get_build_info(&mut ctx).await?; + self.persist_in_database(&ctx).await?; + + Ok(ctx.network.clone()) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs new file mode 100644 index 0000000000..7cde58aeed --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -0,0 +1,183 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub(crate) async fn metadata_set(&self) -> Result { + Ok(sqlx::query("SELECT id FROM metadata") + .fetch_optional(&self.connection_pool) + .await? + .is_some()) + } + + pub(crate) async fn get_master_mnemonic(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT master_mnemonic FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.master_mnemonic)) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT rpc_endpoint FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.rpc_endpoint)) + } + + pub(crate) async fn get_latest_network_id(&self) -> Result, sqlx::Error> { + let maybe_record = sqlx::query!("SELECT latest_network_id FROM metadata") + .fetch_optional(&self.connection_pool) + .await?; + Ok(maybe_record.and_then(|r| r.latest_network_id)) + } + + pub(crate) async fn get_network_name(&self, network_id: i64) -> Result { + sqlx::query!("SELECT name FROM network WHERE id = ?", network_id) + .fetch_one(&self.connection_pool) + .await + .map(|record| record.name) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &str, + rpc_endpoint: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO metadata (id, master_mnemonic, rpc_endpoint) VALUES (0, ?, ?)", + master_mnemonic, + rpc_endpoint + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn save_latest_network_id( + &self, + latest_network_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE metadata SET latest_network_id = ?", + latest_network_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn save_network( + &self, + name: &str, + created_at: OffsetDateTime, + mixnet_id: i64, + vesting_id: i64, + ecash_id: i64, + cw3_id: i64, + cw4_id: i64, + dkg_id: i64, + rewarder_address: &str, + ecash_holding_address: &str, + ) -> Result { + let network_id = sqlx::query!( + r#" + INSERT INTO network ( + name, + created_at, + mixnet_contract_id, + vesting_contract_id, + ecash_contract_id, + cw3_multisig_contract_id, + cw4_group_contract_id, + dkg_contract_id, + rewarder_address, + ecash_holding_account_address + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, + name, + created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_id, + cw4_id, + dkg_id, + rewarder_address, + ecash_holding_address, + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(network_id) + } + + pub(crate) async fn load_network(&self, name: &str) -> Result { + sqlx::query_as("SELECT * FROM network WHERE name = ?") + .bind(name) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_contract( + &self, + name: &str, + address: &str, + admin_address: &str, + ) -> Result { + let id = sqlx::query!( + "INSERT INTO contract (name, address, admin_address) VALUES (?, ?, ?)", + name, + address, + admin_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn load_contract(&self, id: i64) -> Result { + sqlx::query_as( + r#" + SELECT t1.id, t1.name, t1.address, t1.admin_address, t2.mnemonic + FROM contract t1 + JOIN account t2 ON t1.admin_address = t2.address + WHERE t1.id = ?"#, + ) + .bind(id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_account( + &self, + address: &str, + mnemonic: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO account (address, mnemonic) VALUES (?, ?)", + address, + mnemonic + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn load_account(&self, address: &str) -> Result { + sqlx::query_as("SELECT * FROM account WHERE address = ?") + .bind(address) + .fetch_one(&self.connection_pool) + .await + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs new file mode 100644 index 0000000000..8a7960acd4 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -0,0 +1,243 @@ +use std::fs; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, Contract, LoadedNymContracts}; +use crate::manager::network::{LoadedNetwork, Network, SpecialAddresses}; +use crate::manager::storage::manager::StorageManager; +use sqlx::ConnectOptions; +use std::path::Path; +use tracing::{error, info}; +use url::Url; +use zeroize::Zeroizing; + +mod manager; +mod models; + +#[derive(Clone)] +pub(crate) struct NetworkManagerStorage { + manager: StorageManager, +} + +impl NetworkManagerStorage { + pub async fn init>(database_path: P) -> Result { + let database_path = database_path.as_ref(); + info!( + "attempting to initialise storage at {}", + database_path.display() + ); + + if let Some(parent) = database_path.parent() { + fs::create_dir_all(parent)?; + } + + // TODO: we can inject here more stuff based on our nym-api global config + // struct. Maybe different pool size or timeout intervals? + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + // TODO: do we want auto_vacuum ? + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + let storage = NetworkManagerStorage { + manager: StorageManager { connection_pool }, + }; + + Ok(storage) + } + + pub(crate) async fn metadata_set(&self) -> Result { + Ok(self.manager.metadata_set().await?) + } + + pub(crate) async fn get_master_mnemonic( + &self, + ) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_master_mnemonic() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_rpc_endpoint() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_latest_network_name(&self) -> Result { + let Some(id) = self.manager.get_latest_network_id().await? else { + return Err(NetworkManagerError::NoNetworksInitialised); + }; + Ok(self.manager.get_network_name(id).await?) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result<(), NetworkManagerError> { + let master = Zeroizing::new(master_mnemonic.to_string()); + Ok(self + .manager + .set_initial_metadata(master.as_str(), rpc_endpoint.as_str()) + .await?) + } + + async fn persist_contract(&self, contract: &Contract) -> Result { + Ok(self + .manager + .save_contract( + &contract.name, + contract.init_info()?.contract_address.as_ref(), + contract.admin()?.address.as_ref(), + ) + .await?) + } + + async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + let as_str = Zeroizing::new(account.mnemonic.to_string()); + Ok(self + .manager + .save_account(account.address.as_ref(), as_str.as_str()) + .await?) + } + + pub(crate) async fn persist_network( + &self, + network: &Network, + ) -> Result<(), NetworkManagerError> { + self.persist_account(network.contracts.mixnet.admin()?) + .await?; + self.persist_account(network.contracts.vesting.admin()?) + .await?; + self.persist_account(network.contracts.ecash.admin()?) + .await?; + self.persist_account(network.contracts.cw3_multisig.admin()?) + .await?; + self.persist_account(network.contracts.cw4_group.admin()?) + .await?; + self.persist_account(network.contracts.dkg.admin()?).await?; + + self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) + .await?; + self.persist_account(&network.auxiliary_addresses.ecash_holding_account) + .await?; + + let mixnet_id = self.persist_contract(&network.contracts.mixnet).await?; + let vesting_id = self.persist_contract(&network.contracts.vesting).await?; + let ecash_id = self.persist_contract(&network.contracts.ecash).await?; + let cw3_multisig_id = self + .persist_contract(&network.contracts.cw3_multisig) + .await?; + let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; + let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + + let network_id = self + .manager + .save_network( + &network.name, + network.created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_multisig_id, + cw4_group_id, + dkg_id, + network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), + network + .auxiliary_addresses + .ecash_holding_account + .address + .as_ref(), + ) + .await?; + + self.manager.save_latest_network_id(network_id).await?; + + Ok(()) + } + + pub(crate) async fn try_load_network( + &self, + name: &str, + ) -> Result { + let base_network = self.manager.load_network(name).await?; + let rpc_endpoint = self + .get_rpc_endpoint() + .await? + .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + + Ok(LoadedNetwork { + name: base_network.name, + rpc_endpoint, + created_at: base_network.created_at, + contracts: LoadedNymContracts { + mixnet: self + .manager + .load_contract(base_network.mixnet_contract_id) + .await? + .try_into()?, + vesting: self + .manager + .load_contract(base_network.vesting_contract_id) + .await? + .try_into()?, + ecash: self + .manager + .load_contract(base_network.ecash_contract_id) + .await? + .try_into()?, + cw3_multisig: self + .manager + .load_contract(base_network.cw3_multisig_contract_id) + .await? + .try_into()?, + cw4_group: self + .manager + .load_contract(base_network.cw4_group_contract_id) + .await? + .try_into()?, + dkg: self + .manager + .load_contract(base_network.dkg_contract_id) + .await? + .try_into()?, + }, + auxiliary_addresses: SpecialAddresses { + ecash_holding_account: self + .manager + .load_account(&base_network.ecash_holding_account_address) + .await? + .try_into()?, + mixnet_rewarder: self + .manager + .load_account(&base_network.rewarder_address) + .await? + .try_into()?, + }, + }) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs new file mode 100644 index 0000000000..141b481272 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, LoadedContract}; +use sqlx::FromRow; +use time::OffsetDateTime; + +#[derive(FromRow)] +pub(crate) struct RawAccount { + pub(crate) address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for Account { + type Error = NetworkManagerError; + + fn try_from(value: RawAccount) -> Result { + Ok(Account { + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + mnemonic: value.mnemonic.parse()?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawContract { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) address: String, + pub(crate) admin_address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for LoadedContract { + type Error = NetworkManagerError; + + fn try_from(value: RawContract) -> Result { + Ok(LoadedContract { + name: value.name, + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_address: value + .admin_address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_mnemonic: value + .mnemonic + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawNetwork { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) created_at: OffsetDateTime, + + pub(crate) mixnet_contract_id: i64, + pub(crate) vesting_contract_id: i64, + pub(crate) ecash_contract_id: i64, + pub(crate) cw3_multisig_contract_id: i64, + pub(crate) cw4_group_contract_id: i64, + pub(crate) dkg_contract_id: i64, + + pub(crate) rewarder_address: String, + pub(crate) ecash_holding_account_address: String, +} diff --git a/tools/nym-cli/src/coconut/mod.rs b/tools/nym-cli/src/coconut/mod.rs index 2939175aed..6fda72e6a0 100644 --- a/tools/nym-cli/src/coconut/mod.rs +++ b/tools/nym-cli/src/coconut/mod.rs @@ -3,33 +3,26 @@ use nym_network_defaults::NymNetworkDetails; pub(crate) async fn execute( global_args: ClientArgs, - coconut: nym_cli_commands::coconut::Coconut, + coconut: nym_cli_commands::coconut::Ecash, network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match coconut.command { - nym_cli_commands::coconut::CoconutCommands::GenerateFreepass(args) => { - nym_cli_commands::coconut::generate_freepass::execute( + nym_cli_commands::coconut::EcashCommands::IssueTicketBook(args) => { + nym_cli_commands::coconut::issue_ticket_book::execute( args, create_signing_client(global_args, network_details)?, ) .await? } - nym_cli_commands::coconut::CoconutCommands::IssueCredentials(args) => { - nym_cli_commands::coconut::issue_credentials::execute( - args, - create_signing_client(global_args, network_details)?, - ) - .await? - } - nym_cli_commands::coconut::CoconutCommands::RecoverCredentials(args) => { - nym_cli_commands::coconut::recover_credentials::execute( + nym_cli_commands::coconut::EcashCommands::RecoverTicketBook(args) => { + nym_cli_commands::coconut::recover_ticket_book::execute( args, create_query_client(network_details)?, ) .await? } - nym_cli_commands::coconut::CoconutCommands::ImportCredential(args) => { - nym_cli_commands::coconut::import_credential::execute(args).await? + nym_cli_commands::coconut::EcashCommands::ImportTicketBook(args) => { + nym_cli_commands::coconut::import_ticket_book::execute(args).await? } } Ok(()) diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 16b771d33e..12defe4817 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -62,8 +62,8 @@ pub(crate) enum Commands { Account(nym_cli_commands::validator::account::Account), /// Sign and verify messages Signature(nym_cli_commands::validator::signature::Signature), - /// Coconut related stuff - Coconut(nym_cli_commands::coconut::Coconut), + /// Ecash related stuff + Ecash(nym_cli_commands::coconut::Ecash), /// Query chain blocks Block(nym_cli_commands::validator::block::Block), /// Manage and execute WASM smart contracts @@ -104,7 +104,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { Commands::Signature(signature) => { validator::signature::execute(signature, &network_details, mnemonic).await? } - Commands::Coconut(coconut) => coconut::execute(args, coconut, &network_details).await?, + Commands::Ecash(coconut) => coconut::execute(args, coconut, &network_details).await?, Commands::Block(block) => validator::block::execute(block, &network_details).await?, Commands::Cosmwasm(cosmwasm) => { validator::cosmwasm::execute(args, cosmwasm, &network_details).await? diff --git a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs index aba0aaa9a5..5c576e5991 100644 --- a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs +++ b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs @@ -10,9 +10,9 @@ pub(crate) async fn execute( args, ) => nym_cli_commands::validator::cosmwasm::generators::vesting::generate(args).await, - nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutBandwidth( + nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::EcashBandwidth( args, - ) => nym_cli_commands::validator::cosmwasm::generators::coconut_bandwidth::generate(args).await, + ) => nym_cli_commands::validator::cosmwasm::generators::ecash_bandwidth::generate(args).await, nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutDKG( args, diff --git a/wasm/zknym-lib/Cargo.toml b/wasm/zknym-lib/Cargo.toml index 6fdfb431bb..5ef55b0314 100644 --- a/wasm/zknym-lib/Cargo.toml +++ b/wasm/zknym-lib/Cargo.toml @@ -27,11 +27,12 @@ reqwest = { workspace = true } wasmtimer = { workspace = true } zeroize.workspace = true -rand = { version = "0.7.3", features = ["wasm-bindgen"] } +rand = { workspace = true } nym-bin-common = { path = "../../common/bin-common" } nym-coconut = { path = "../../common/nymcoconut", features = ["key-zeroize"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } nym-http-api-client = { path = "../../common/http-api-client" } diff --git a/wasm/zknym-lib/src/bandwidth_voucher.rs b/wasm/zknym-lib/src/bandwidth_voucher.rs index b047550c1e..d64fdd58f3 100644 --- a/wasm/zknym-lib/src/bandwidth_voucher.rs +++ b/wasm/zknym-lib/src/bandwidth_voucher.rs @@ -1,201 +1,201 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; -use crate::vpn_api_client::types::{ - AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, -}; -use nym_coconut::{ - Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, - SignatureShare, VerificationKey, -}; -use nym_credentials::coconut::bandwidth::issuance::Coin; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use nym_credentials::IssuedBandwidthCredential; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_utils::console_error; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -// tiny 'hacks' to just allow passing responses from vpn-api queries -pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; -pub type VoucherShares = BandwidthVoucherResponse; -pub type VoucherIssuers = PartialVerificationKeysResponse; - -#[wasm_bindgen] -#[derive(Debug)] -#[allow(dead_code)] -pub struct NymIssuanceBandwidthVoucher { - serial_number: PrivateAttribute, - - binding_number: PrivateAttribute, - - credential_amount: Coin, - - prehashed_amount: PublicAttribute, - prehashed_type: PublicAttribute, - - blind_sign_request: BlindSignRequest, - pedersen_commitments_openings: Zeroizing>, -} - -#[wasm_bindgen] -impl NymIssuanceBandwidthVoucher { - #[wasm_bindgen(js_name = "prepareNew", constructor)] - pub fn prepare_new( - opts: NymIssuanceBandwidthVoucherOpts, - parameters: Option, - ) -> Result { - let deposit_amount: u128 = opts - .credential_amount_string - .parse() - .map_err(|_| ZkNymError::InvalidDepositAmount)?; - let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); - - let params = get_params(¶meters); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; - let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; - - let public_attributes = vec![&prehashed_amount, &prehashed_type]; - let private_attributes = vec![&serial_number, &binding_number]; - - let (pedersen_commitments_openings, blind_sign_request) = - nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; - - Ok(NymIssuanceBandwidthVoucher { - serial_number, - binding_number, - credential_amount, - prehashed_amount, - prehashed_type, - blind_sign_request, - pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), - }) - } - - #[wasm_bindgen(js_name = "getBlindSignRequest")] - pub fn get_blind_sign_request(&self) -> String { - self.blind_sign_request.to_bs58() - } - - #[wasm_bindgen(js_name = "unblindShare")] - pub fn unblind_share(&self, share: UnblindableShare) -> Result { - let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; - let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; - - Ok(blinded_sig - .unblind(&vk, &self.pedersen_commitments_openings) - .into()) - } - - #[wasm_bindgen(js_name = "unblindShares")] - pub fn unblind_shares( - self, - shares: VoucherShares, - issuers: VoucherIssuers, - ) -> Result { - if shares.epoch_id != issuers.epoch_id { - console_error!( - "the provided shares and issuers are not from the same epoch! {} and {}", - shares.epoch_id, - issuers.epoch_id - ); - return Err(ZkNymError::InconsistentEpochId { - shares: shares.epoch_id, - issuers: issuers.epoch_id, - }); - } - - let mut decoded_keys = HashMap::new(); - for key in issuers.keys { - let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; - decoded_keys.insert(key.node_index, vk); - } - - let mut credential_shares = Vec::new(); - for share in shares.shares { - let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; - let Some(vk) = decoded_keys.get(&share.node_index) else { - console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); - continue; - }; - let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); - credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); - } - - let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; - - let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( - BandwidthVoucherIssuedData::new(self.credential_amount), - ); - - Ok(NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - signature, - voucher_data, - self.prehashed_type, - shares.epoch_id, - ), - }) - } -} - -#[wasm_bindgen] -pub struct NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential, -} - -#[wasm_bindgen] -impl NymIssuedBandwidthVoucher { - #[wasm_bindgen(js_name = "ensureIsValid")] - pub fn ensure_is_valid( - &self, - master_vk: String, - parameters: Option, - ) -> bool { - let params = get_params(¶meters); - let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { - console_error!("malformed master verification key"); - return false; - }; - - let spending_req = match self.inner.prepare_for_spending(&master_vk) { - Ok(req) => req, - Err(err) => { - console_error!("failed to prepare spending request: {err}"); - return false; - } - }; - - spending_req.verify(params, &master_vk) - } - - pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { - SerialisedNymIssuedBandwidthVoucher { - serialisation_revision: - nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, - bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), - } - } -} - -#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct SerialisedNymIssuedBandwidthVoucher { - pub serialisation_revision: u8, - pub bs58_encoded_data: String, -} +// use crate::error::ZkNymError; +// use crate::generic_scheme::get_params; +// use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; +// use crate::vpn_api_client::types::{ +// AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, +// }; +// use nym_coconut::{ +// Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, +// SignatureShare, VerificationKey, +// }; +// use nym_credentials::ecash::bandwidth::issuance::Coin; +// use nym_credentials::ecash::bandwidth::issued::BandwidthCredentialIssuedDataVariant; +// use nym_credentials::ecash::bandwidth::voucher::BandwidthVoucherIssuedData; +// use nym_credentials::IssuedBandwidthCredential; +// use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; +// use tsify::Tsify; +// use wasm_bindgen::prelude::wasm_bindgen; +// use wasm_utils::console_error; +// use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +// +// // tiny 'hacks' to just allow passing responses from vpn-api queries +// pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; +// pub type VoucherShares = BandwidthVoucherResponse; +// pub type VoucherIssuers = PartialVerificationKeysResponse; +// +// #[wasm_bindgen] +// #[derive(Debug)] +// #[allow(dead_code)] +// pub struct NymIssuanceBandwidthVoucher { +// serial_number: PrivateAttribute, +// +// binding_number: PrivateAttribute, +// +// credential_amount: Coin, +// +// prehashed_amount: PublicAttribute, +// prehashed_type: PublicAttribute, +// +// blind_sign_request: BlindSignRequest, +// pedersen_commitments_openings: Zeroizing>, +// } +// +// #[wasm_bindgen] +// impl NymIssuanceBandwidthVoucher { +// #[wasm_bindgen(js_name = "prepareNew", constructor)] +// pub fn prepare_new( +// opts: NymIssuanceBandwidthVoucherOpts, +// parameters: Option, +// ) -> Result { +// let deposit_amount: u128 = opts +// .credential_amount_string +// .parse() +// .map_err(|_| ZkNymError::InvalidDepositAmount)?; +// let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); +// +// let params = get_params(¶meters); +// let serial_number = params.random_scalar(); +// let binding_number = params.random_scalar(); +// +// let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; +// let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; +// +// let public_attributes = vec![&prehashed_amount, &prehashed_type]; +// let private_attributes = vec![&serial_number, &binding_number]; +// +// let (pedersen_commitments_openings, blind_sign_request) = +// nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; +// +// Ok(NymIssuanceBandwidthVoucher { +// serial_number, +// binding_number, +// credential_amount, +// prehashed_amount, +// prehashed_type, +// blind_sign_request, +// pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), +// }) +// } +// +// #[wasm_bindgen(js_name = "getBlindSignRequest")] +// pub fn get_blind_sign_request(&self) -> String { +// self.blind_sign_request.to_bs58() +// } +// +// #[wasm_bindgen(js_name = "unblindShare")] +// pub fn unblind_share(&self, share: UnblindableShare) -> Result { +// let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; +// let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; +// +// Ok(blinded_sig +// .unblind(&vk, &self.pedersen_commitments_openings) +// .into()) +// } +// +// #[wasm_bindgen(js_name = "unblindShares")] +// pub fn unblind_shares( +// self, +// shares: VoucherShares, +// issuers: VoucherIssuers, +// ) -> Result { +// if shares.epoch_id != issuers.epoch_id { +// console_error!( +// "the provided shares and issuers are not from the same epoch! {} and {}", +// shares.epoch_id, +// issuers.epoch_id +// ); +// return Err(ZkNymError::InconsistentEpochId { +// shares: shares.epoch_id, +// issuers: issuers.epoch_id, +// }); +// } +// +// let mut decoded_keys = HashMap::new(); +// for key in issuers.keys { +// let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; +// decoded_keys.insert(key.node_index, vk); +// } +// +// let mut credential_shares = Vec::new(); +// for share in shares.shares { +// let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; +// let Some(vk) = decoded_keys.get(&share.node_index) else { +// console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); +// continue; +// }; +// let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); +// credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); +// } +// +// let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; +// +// let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( +// BandwidthVoucherIssuedData::new(self.credential_amount), +// ); +// +// Ok(NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential::new( +// self.serial_number, +// self.binding_number, +// signature, +// voucher_data, +// self.prehashed_type, +// shares.epoch_id, +// ), +// }) +// } +// } +// +// #[wasm_bindgen] +// pub struct NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential, +// } +// +// #[wasm_bindgen] +// impl NymIssuedBandwidthVoucher { +// #[wasm_bindgen(js_name = "ensureIsValid")] +// pub fn ensure_is_valid( +// &self, +// master_vk: String, +// parameters: Option, +// ) -> bool { +// let params = get_params(¶meters); +// let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { +// console_error!("malformed master verification key"); +// return false; +// }; +// +// let spending_req = match self.inner.prepare_for_spending(&master_vk) { +// Ok(req) => req, +// Err(err) => { +// console_error!("failed to prepare spending request: {err}"); +// return false; +// } +// }; +// +// spending_req.verify(params, &master_vk) +// } +// +// pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { +// SerialisedNymIssuedBandwidthVoucher { +// serialisation_revision: +// nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, +// bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), +// } +// } +// } +// +// #[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +// #[tsify(into_wasm_abi, from_wasm_abi)] +// #[serde(rename_all = "camelCase")] +// pub struct SerialisedNymIssuedBandwidthVoucher { +// pub serialisation_revision: u8, +// pub bs58_encoded_data: String, +// } // #[cfg(test)] // mod tests { diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index b283908438..37c49aa672 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -7,12 +7,18 @@ use wasm_utils::wasm_error; #[derive(Debug, Error)] pub enum ZkNymError { - #[error("cryptographic failure: {source}")] + #[error("[coconut] cryptographic failure: {source}")] CoconutFailure { #[from] source: nym_coconut::CoconutError, }, + #[error("[ecash] cryptographic failure: {source}")] + EcashFailure { + #[from] + source: nym_compact_ecash::CompactEcashError, + }, + #[error("failed to contact the vpn api")] HttpClientFailure { #[from] diff --git a/wasm/zknym-lib/src/generic_scheme.rs b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs similarity index 88% rename from wasm/zknym-lib/src/generic_scheme.rs rename to wasm/zknym-lib/src/generic_scheme/coconut/mod.rs index 53f9c805f4..178f53106c 100644 --- a/wasm/zknym-lib/src/generic_scheme.rs +++ b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs @@ -1,24 +1,27 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::types::{ +use super::coconut::types::{ BlindSignRequestData, BlindSignRequestWrapper, BlindedCredentialWrapper, CredentialShareWrapper, CredentialWrapper, KeyPairWrapper, ParametersWrapper, ScalarsWrapper, VerificationKeyShareWrapper, VerificationKeyWrapper, VerifyCredentialRequestWrapper, }; -use crate::GLOBAL_PARAMS; +use crate::error::ZkNymError; +use crate::GLOBAL_COCONUT_PARAMS; use nym_coconut::{hash_to_scalar, Parameters, SignerIndex}; -use nym_credentials::IssuanceBandwidthCredential; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use tsify::Tsify; use wasm_bindgen::prelude::wasm_bindgen; +pub mod types; + // works under the assumption of having 4 attributes in the underlying credential(s) +const DEFAULT_ATTRIBUTES: u32 = 4; + pub fn default_bandwidth_credential_params() -> &'static Parameters { static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) + BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(|| Parameters::new(DEFAULT_ATTRIBUTES).unwrap()) } // attempt to extract appropriate system parameters in the following order: @@ -30,7 +33,7 @@ pub(crate) fn get_params(explicit_params: &Option) -> &Parame return explicit; } - if let Some(global) = GLOBAL_PARAMS.get() { + if let Some(global) = GLOBAL_COCONUT_PARAMS.get() { return global; } @@ -48,16 +51,14 @@ pub struct SetupOpts { pub set_global: Option, } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutSetup")] pub fn setup(opts: SetupOpts) -> Result { - let num_attributes = opts - .num_attributes - .unwrap_or(IssuanceBandwidthCredential::ENCODED_ATTRIBUTES); + let num_attributes = opts.num_attributes.unwrap_or(DEFAULT_ATTRIBUTES); let params = nym_coconut::setup(num_attributes)?; if let Some(true) = opts.set_global { - GLOBAL_PARAMS + GLOBAL_COCONUT_PARAMS .set(params.clone()) .map_err(|_| ZkNymError::GlobalParamsAlreadySet)?; } @@ -65,7 +66,7 @@ pub fn setup(opts: SetupOpts) -> Result { Ok(params.into()) } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutKeygen")] pub fn keygen(parameters: Option) -> KeyPairWrapper { let params = get_params(¶meters); nym_coconut::keygen(params).into() @@ -80,7 +81,7 @@ pub struct TtpKeygenOpts { pub authorities: u64, } -#[wasm_bindgen(js_name = "ttpKeygen")] +#[wasm_bindgen(js_name = "coconutTtpKeygen")] pub fn ttp_keygen( opts: TtpKeygenOpts, parameters: Option, @@ -91,7 +92,7 @@ pub fn ttp_keygen( Ok(keys.into_iter().map(Into::into).collect()) } -#[wasm_bindgen(js_name = "signSimple")] +#[wasm_bindgen(js_name = "coconutSignSimple")] pub fn sign_simple( attributes: Vec, keys: &KeyPairWrapper, @@ -110,7 +111,7 @@ pub fn sign_simple( .map_err(Into::into) } -#[wasm_bindgen(js_name = "prepareBlindSign")] +#[wasm_bindgen(js_name = "coconutPrepareBlindSign")] pub fn prepare_blind_sign( private_attributes: Vec, public_attributes: Vec, @@ -138,7 +139,7 @@ pub fn prepare_blind_sign( }) } -#[wasm_bindgen(js_name = "blindSign")] +#[wasm_bindgen(js_name = "coconutBlindSign")] pub fn blind_sign( keys: &KeyPairWrapper, blind_sign_request: &BlindSignRequestWrapper, @@ -163,7 +164,7 @@ pub fn blind_sign( .map_err(Into::into) } -#[wasm_bindgen(js_name = "unblindSignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindSignatureShare")] pub fn unblind_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -176,7 +177,7 @@ pub fn unblind_signature_share( ) } -#[wasm_bindgen(js_name = "unblindAndVerifySignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindAndVerifySignatureShare")] pub fn unblind_and_verify_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -195,7 +196,7 @@ pub fn unblind_and_verify_signature_share( ) } -#[wasm_bindgen(js_name = "aggregateSignatureShares")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureShares")] pub fn aggregate_signature_shares( shares: Vec, ) -> Result { @@ -206,7 +207,7 @@ pub fn aggregate_signature_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateSignatureSharesAndVerify")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureSharesAndVerify")] pub fn aggregate_signature_shares_and_verify( verification_key: &VerificationKeyWrapper, parameters: Option, @@ -242,7 +243,7 @@ pub fn aggregate_signature_shares_and_verify( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeyShares")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeyShares")] pub fn aggregate_verification_key_shares( shares: Vec, ) -> Result { @@ -253,7 +254,7 @@ pub fn aggregate_verification_key_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeys")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeys")] pub fn aggregate_verification_keys( keys: Vec, indices: Vec, @@ -265,7 +266,7 @@ pub fn aggregate_verification_keys( .map_err(Into::into) } -#[wasm_bindgen(js_name = "proveBandwidthCredential")] +#[wasm_bindgen(js_name = "coconutProveBandwidthCredential")] pub fn prove_bandwidth_credential( verification_key: &VerificationKeyWrapper, credential: &CredentialWrapper, @@ -286,7 +287,7 @@ pub fn prove_bandwidth_credential( .map_err(Into::into) } -#[wasm_bindgen(js_name = "verifyCredential")] +#[wasm_bindgen(js_name = "coconutVerifyCredential")] pub fn verify_credential( verification_key: &VerificationKeyWrapper, verification_request: &VerifyCredentialRequestWrapper, @@ -309,7 +310,7 @@ pub fn verify_credential( ) } -#[wasm_bindgen(js_name = "verifySimple")] +#[wasm_bindgen(js_name = "coconutVerifySimple")] pub fn verify_simple( verification_key: &VerificationKeyWrapper, attributes: Vec, @@ -327,7 +328,7 @@ pub fn verify_simple( nym_coconut::verify(params, verification_key, &attributes_ref, credential) } -#[wasm_bindgen(js_name = "simpleRandomiseCredential")] +#[wasm_bindgen(js_name = "coconutSimpleRandomiseCredential")] pub fn simple_randomise_credential( credential: &CredentialWrapper, parameters: Option, diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs new file mode 100644 index 0000000000..8370fcc636 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs @@ -0,0 +1,170 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ZkNymError; +use crate::{data_pointer_clone, wasm_wrapper, wasm_wrapper_bs58}; +use nym_coconut::{ + hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, + SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, + VerifyCredentialRequest, +}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::str::FromStr; +use tsify::Tsify; +use wasm_bindgen::prelude::wasm_bindgen; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +wasm_wrapper!(Parameters, ParametersWrapper); +wasm_wrapper_bs58!(Signature, CredentialWrapper); +wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); +wasm_wrapper!(SignatureShare, CredentialShareWrapper); +wasm_wrapper_bs58!(Scalar, ScalarWrapper); + +wasm_wrapper!(KeyPair, KeyPairWrapper); +wasm_wrapper!(SecretKey, SecretKeyWrapper); +wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); +wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); +wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); +wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); + +data_pointer_clone!(VerificationKeyShareWrapper); +data_pointer_clone!(CredentialShareWrapper); +data_pointer_clone!(BlindSignRequestWrapper); + +#[wasm_bindgen] +impl BlindedCredentialWrapper { + pub fn unblind( + &self, + partial_verification_key: &VerificationKeyWrapper, + pedersen_commitments_openings: &ScalarsWrapper, + ) -> CredentialWrapper { + self.inner + .unblind(partial_verification_key, pedersen_commitments_openings) + .into() + } + + #[wasm_bindgen(js_name = "unblindAndVerify")] + pub fn unblind_and_verify( + &self, + partial_verification_key: &VerificationKeyWrapper, + request: &BlindSignRequestData, + private_attributes: Vec, + public_attributes: Vec, + parameters: Option, + ) -> Result { + let params = super::get_params(¶meters); + + let public_attributes = public_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let public_attributes_ref = public_attributes.iter().collect::>(); + + let private_attributes = private_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let private_attributes_ref = private_attributes.iter().collect::>(); + + let unblinded_signature = self.inner.unblind_and_verify( + params, + partial_verification_key, + &private_attributes_ref, + &public_attributes_ref, + &request.blind_sign_request.get_commitment_hash(), + &request.pedersen_commitments_openings, + )?; + + Ok(unblinded_signature.into()) + } +} + +#[wasm_bindgen] +impl CredentialWrapper { + #[wasm_bindgen(js_name = "intoShare")] + pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { + CredentialShareWrapper { + inner: SignatureShare::new(self.inner, index), + } + } +} + +#[wasm_bindgen] +impl KeyPairWrapper { + #[wasm_bindgen(js_name = "verificationKey")] + pub fn verification_key(&self) -> VerificationKeyWrapper { + self.inner.verification_key().clone().into() + } + + pub fn index(&self) -> Option { + self.inner.index + } + + #[wasm_bindgen(js_name = "verificationKeyShare")] + pub fn verification_key_share(&self) -> Option { + self.inner.to_verification_key_share().map(Into::into) + } +} + +#[wasm_bindgen] +pub struct BlindSignRequestData { + pub(crate) blind_sign_request: BlindSignRequest, + pub(crate) pedersen_commitments_openings: Vec, +} + +#[wasm_bindgen] +impl BlindSignRequestData { + #[wasm_bindgen(js_name = "blindSignRequest")] + pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { + self.blind_sign_request.clone().into() + } + + #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] + pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { + ScalarsWrapper(self.pedersen_commitments_openings.clone()) + } +} + +#[wasm_bindgen] +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct ScalarsWrapper(pub(crate) Vec); + +impl Deref for ScalarsWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive( + Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, +)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct KeypairWrapper { + pub private_key: String, + pub public_key: String, +} + +#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct UnblindableShare { + pub issuer_index: u64, + pub issuer_key_bs58: String, + pub blinded_share_bs58: String, +} + +#[wasm_bindgen] +impl UnblindableShare { + #[wasm_bindgen(constructor)] + pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { + UnblindableShare { + issuer_index, + issuer_key_bs58, + blinded_share_bs58, + } + } +} diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs new file mode 100644 index 0000000000..c2ddfbb025 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod types; diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/types.rs b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs new file mode 100644 index 0000000000..cbb465df6c --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::wasm_wrapper; +use nym_compact_ecash::GroupParameters; +use wasm_bindgen::prelude::wasm_bindgen; + +wasm_wrapper!(GroupParameters, GroupParametersWrapper); diff --git a/wasm/zknym-lib/src/generic_scheme/mod.rs b/wasm/zknym-lib/src/generic_scheme/mod.rs new file mode 100644 index 0000000000..ad786c7841 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod coconut; +pub mod ecash; diff --git a/wasm/zknym-lib/src/helpers.rs b/wasm/zknym-lib/src/helpers.rs new file mode 100644 index 0000000000..a99ec7753b --- /dev/null +++ b/wasm/zknym-lib/src/helpers.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[macro_export] +macro_rules! wasm_wrapper { + ($base:ident, $wrapper:ident) => { + #[wasm_bindgen] + pub struct $wrapper { + pub(crate) inner: $base, + } + + impl std::ops::Deref for $wrapper { + type Target = $base; + + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl From<$base> for $wrapper { + fn from(inner: $base) -> Self { + $wrapper { inner } + } + } + + impl From<$wrapper> for $base { + fn from(value: $wrapper) -> Self { + value.inner + } + } + }; +} + +#[macro_export] +macro_rules! data_pointer_clone { + ($wrapper:ident) => { + #[wasm_bindgen] + impl $wrapper { + #[wasm_bindgen(js_name = "cloneDataPointer")] + pub fn clone_data_pointer(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + }; +} + +#[macro_export] +macro_rules! wasm_wrapper_bs58 { + ($base:ident, $wrapper:ident) => { + wasm_wrapper!($base, $wrapper); + + impl std::fmt::Display for $wrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.to_bs58().fmt(f) + } + } + + impl FromStr for $wrapper { + type Err = ZkNymError; + + fn from_str(s: &str) -> Result { + Ok($base::try_from_bs58(s)?.into()) + } + } + + #[wasm_bindgen] + impl $wrapper { + pub fn stringify(&self) -> String { + self.to_string() + } + + #[wasm_bindgen(js_name = "fromString")] + pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { + raw.parse() + } + } + }; +} diff --git a/wasm/zknym-lib/src/lib.rs b/wasm/zknym-lib/src/lib.rs index d94f90385e..540a386ee8 100644 --- a/wasm/zknym-lib/src/lib.rs +++ b/wasm/zknym-lib/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![allow(unknown_lints)] +// clippy::empty_docs is not on stable as of 1.77 + // due to the code generated by Tsify #![allow(clippy::empty_docs)] @@ -11,13 +14,15 @@ use wasm_bindgen::prelude::*; pub mod bandwidth_voucher; pub mod error; pub mod generic_scheme; +pub(crate) mod helpers; +pub mod ticketbook; pub mod types; // keep in internal to the crate since I'm not sure how temporary this thing is going to be // I mostly got it, so I could test the whole thing end to end pub(crate) mod vpn_api_client; -pub(crate) static GLOBAL_PARAMS: OnceLock = OnceLock::new(); +pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock = OnceLock::new(); #[wasm_bindgen(start)] // #[cfg(target_arch = "wasm32")] diff --git a/wasm/zknym-lib/src/ticketbook.rs b/wasm/zknym-lib/src/ticketbook.rs new file mode 100644 index 0000000000..257ac1a0e7 --- /dev/null +++ b/wasm/zknym-lib/src/ticketbook.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct SerialisedNymTicketBook { + pub serialisation_revision: u8, + pub bs58_encoded_data: String, +} diff --git a/wasm/zknym-lib/src/types.rs b/wasm/zknym-lib/src/types.rs index 552f44381e..755fb6cc8b 100644 --- a/wasm/zknym-lib/src/types.rs +++ b/wasm/zknym-lib/src/types.rs @@ -1,245 +1,2 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use nym_coconut::{ - hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, - SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, - VerifyCredentialRequest, -}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::str::FromStr; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -macro_rules! wasm_wrapper { - ($base:ident, $wrapper:ident) => { - #[wasm_bindgen] - pub struct $wrapper { - pub(crate) inner: $base, - } - - impl Deref for $wrapper { - type Target = $base; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl From<$base> for $wrapper { - fn from(inner: $base) -> Self { - $wrapper { inner } - } - } - - impl From<$wrapper> for $base { - fn from(value: $wrapper) -> Self { - value.inner - } - } - }; -} - -macro_rules! data_pointer_clone { - ($wrapper:ident) => { - #[wasm_bindgen] - impl $wrapper { - #[wasm_bindgen(js_name = "cloneDataPointer")] - pub fn clone_data_pointer(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } - } - }; -} - -macro_rules! wasm_wrapper_bs58 { - ($base:ident, $wrapper:ident) => { - wasm_wrapper!($base, $wrapper); - - impl std::fmt::Display for $wrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.to_bs58().fmt(f) - } - } - - impl FromStr for $wrapper { - type Err = ZkNymError; - - fn from_str(s: &str) -> Result { - Ok($base::try_from_bs58(s)?.into()) - } - } - - #[wasm_bindgen] - impl $wrapper { - pub fn stringify(&self) -> String { - self.to_string() - } - - #[wasm_bindgen(js_name = "fromString")] - pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { - raw.parse() - } - } - }; -} - -wasm_wrapper!(Parameters, ParametersWrapper); -wasm_wrapper_bs58!(Signature, CredentialWrapper); -wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); -wasm_wrapper!(SignatureShare, CredentialShareWrapper); -wasm_wrapper_bs58!(Scalar, ScalarWrapper); - -wasm_wrapper!(KeyPair, KeyPairWrapper); -wasm_wrapper!(SecretKey, SecretKeyWrapper); -wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); -wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); -wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); -wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); - -data_pointer_clone!(VerificationKeyShareWrapper); -data_pointer_clone!(CredentialShareWrapper); -data_pointer_clone!(BlindSignRequestWrapper); - -#[wasm_bindgen] -impl BlindedCredentialWrapper { - pub fn unblind( - &self, - partial_verification_key: &VerificationKeyWrapper, - pedersen_commitments_openings: &ScalarsWrapper, - ) -> CredentialWrapper { - self.inner - .unblind(partial_verification_key, pedersen_commitments_openings) - .into() - } - - #[wasm_bindgen(js_name = "unblindAndVerify")] - pub fn unblind_and_verify( - &self, - partial_verification_key: &VerificationKeyWrapper, - request: &BlindSignRequestData, - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, - ) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let private_attributes_ref = private_attributes.iter().collect::>(); - - let unblinded_signature = self.inner.unblind_and_verify( - params, - partial_verification_key, - &private_attributes_ref, - &public_attributes_ref, - &request.blind_sign_request.get_commitment_hash(), - &request.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature.into()) - } -} - -#[wasm_bindgen] -impl CredentialWrapper { - #[wasm_bindgen(js_name = "intoShare")] - pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { - CredentialShareWrapper { - inner: SignatureShare::new(self.inner, index), - } - } -} - -#[wasm_bindgen] -impl KeyPairWrapper { - #[wasm_bindgen(js_name = "verificationKey")] - pub fn verification_key(&self) -> VerificationKeyWrapper { - self.inner.verification_key().clone().into() - } - - pub fn index(&self) -> Option { - self.inner.index - } - - #[wasm_bindgen(js_name = "verificationKeyShare")] - pub fn verification_key_share(&self) -> Option { - self.inner.to_verification_key_share().map(Into::into) - } -} - -#[wasm_bindgen] -pub struct BlindSignRequestData { - pub(crate) blind_sign_request: BlindSignRequest, - pub(crate) pedersen_commitments_openings: Vec, -} - -#[wasm_bindgen] -impl BlindSignRequestData { - #[wasm_bindgen(js_name = "blindSignRequest")] - pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { - self.blind_sign_request.clone().into() - } - - #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] - pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { - ScalarsWrapper(self.pedersen_commitments_openings.clone()) - } -} - -#[wasm_bindgen] -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct ScalarsWrapper(pub(crate) Vec); - -impl Deref for ScalarsWrapper { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive( - Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, -)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct KeypairWrapper { - pub private_key: String, - pub public_key: String, -} - -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct UnblindableShare { - pub issuer_index: u64, - pub issuer_key_bs58: String, - pub blinded_share_bs58: String, -} - -#[wasm_bindgen] -impl UnblindableShare { - #[wasm_bindgen(constructor)] - pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { - UnblindableShare { - issuer_index, - issuer_key_bs58, - blinded_share_bs58, - } - } -} diff --git a/wasm/zknym-lib/src/vpn_api_client/types.rs b/wasm/zknym-lib/src/vpn_api_client/types.rs index 691fb99cbf..cdb8d47534 100644 --- a/wasm/zknym-lib/src/vpn_api_client/types.rs +++ b/wasm/zknym-lib/src/vpn_api_client/types.rs @@ -83,13 +83,6 @@ pub struct AttributesResponse { pub bs58_prehashed_amount: String, } -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct FreepassCredentialResponse { - pub bs58_encoded_value: String, -} - #[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")]