From 87cb8a6b207d4121d9a1b48bc63389020baa4131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 5 Jun 2023 10:32:58 +0200 Subject: [PATCH] Sign when announcing service providers to the directory contract (#3459) * create_payload and call from nym-cli * Remove some commented out code * wip * Service announce now compiles * Fix other compilation issues * Move ServiceDetails into Service * Move service_id inside Service type * wip: start sorting out tests * wip: sorting out testing * wip: first announce test now works * wip: more work on announce test * Move nonce * Add check for nonce * Extract out some helpers to separate files * reenable state::services tests * wip: start going through the integration tests * All integration tests reenabled * Remove some unused stuff * Iterate on integration tests * More iteration on test setup * Rename to test_setup.rs * Add more tests specific to signing * Tweak * Another nonce test and reorg * Rename to announce.rs and delete.rs * Tidy * Make some inner modules private * Use IdentityKey alias * Update nym-api contract cache * Fix that nym-cli was asking for signing nonce from wrong contract * Add sign comment to network-requester * Uploaded updated service provider contract to qwerty * Allow large enum variant * lock files * Remove dbg * Move error.rs to service-provider common * Update code for moving errors.rs to common crate * Rename to SpContractError * constants module not pub * lock file * rustfmt * Move IdentityKey type to contract-common * clippy --- Cargo.lock | 22 ++ Cargo.toml | 3 +- common/client-core/src/init/mod.rs | 21 +- .../nyxd/traits/sp_directory_query_client.rs | 18 +- .../traits/sp_directory_signing_client.rs | 11 +- common/commands/Cargo.toml | 1 + common/commands/src/validator/mixnet/mod.rs | 1 + .../src/validator/mixnet/operators/mod.rs | 1 + .../mixnet/operators/service/announce.rs | 16 +- .../service/announce_sign_payload.rs | 61 +++ .../validator/mixnet/operators/service/mod.rs | 4 + .../query/query_all_service_providers.rs | 2 +- .../contracts-common/src/types.rs | 3 + .../mixnet-contract/src/types.rs | 3 +- .../service-provider-directory/Cargo.toml | 4 + .../service-provider-directory/src/error.rs | 23 +- .../service-provider-directory/src/events.rs | 10 +- .../service-provider-directory/src/lib.rs | 2 + .../service-provider-directory/src/msg.rs | 19 +- .../src/response.rs | 34 +- .../src/signing_types.rs | 33 ++ .../service-provider-directory/src/types.rs | 40 +- contracts/Cargo.lock | 8 + contracts/mixnet/src/lib.rs | 2 +- contracts/name-service/src/state/names.rs | 2 +- .../service-provider-directory/Cargo.toml | 6 +- .../src/constants.rs | 2 + .../src/contract.rs | 175 ++++++--- .../src/contract/execute.rs | 108 ++++-- .../src/contract/query.rs | 27 +- .../src/integration_tests.rs | 355 ------------------ .../src/integration_tests/announce.rs | 183 +++++++++ .../src/integration_tests/delete.rs | 144 +++++++ .../src/integration_tests/mod.rs | 13 + .../src/integration_tests/query.rs | 77 ++++ .../src/integration_tests/service_id.rs | 89 +++++ .../src/integration_tests/test_service.rs | 75 ++++ .../test_setup.rs | 133 +++++-- .../service-provider-directory/src/lib.rs | 12 +- .../src/state/admin.rs | 2 +- .../src/state/config.rs | 2 +- .../src/state/mod.rs | 13 +- .../src/state/nonce.rs | 71 ++++ .../src/state/service_id_counter.rs | 78 ++-- .../src/state/services.rs | 177 ++++----- .../src/test_helpers/assert.rs | 26 +- .../src/test_helpers/fixture.rs | 112 ++++-- .../src/test_helpers/helpers.rs | 64 +--- .../src/test_helpers/mod.rs | 3 +- .../src/test_helpers/signing.rs | 41 ++ .../src/test_helpers/transactions.rs | 69 ++++ envs/qa-qwerty.env | 2 +- gateway/Cargo.toml | 35 +- nym-api/src/nym_contract_cache/cache/data.rs | 4 +- nym-api/src/nym_contract_cache/cache/mod.rs | 6 +- nym-connect/desktop/Cargo.lock | 18 + .../src-tauri/src/operations/directory/mod.rs | 2 +- nym-wallet/Cargo.lock | 18 + .../network-requester/Cargo.toml | 3 + .../network-requester/src/cli/mod.rs | 36 ++ .../network-requester/src/cli/run.rs | 35 +- .../network-requester/src/cli/sign.rs | 96 +++++ tools/nym-cli/src/main.rs | 1 + .../mixnet/operators/services/mod.rs | 1 + 64 files changed, 1799 insertions(+), 859 deletions(-) create mode 100644 common/commands/src/validator/mixnet/operators/service/announce_sign_payload.rs rename {contracts => common/cosmwasm-smart-contracts}/service-provider-directory/src/error.rs (67%) create mode 100644 common/cosmwasm-smart-contracts/service-provider-directory/src/signing_types.rs delete mode 100644 contracts/service-provider-directory/src/integration_tests.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/announce.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/delete.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/mod.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/query.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/service_id.rs create mode 100644 contracts/service-provider-directory/src/integration_tests/test_service.rs rename contracts/service-provider-directory/src/{test_helpers => integration_tests}/test_setup.rs (51%) create mode 100644 contracts/service-provider-directory/src/state/nonce.rs create mode 100644 contracts/service-provider-directory/src/test_helpers/signing.rs create mode 100644 contracts/service-provider-directory/src/test_helpers/transactions.rs create mode 100644 service-providers/network-requester/src/cli/sign.rs diff --git a/Cargo.lock b/Cargo.lock index ca90aba388..0d0ef892e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,6 +1201,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-controllers" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-storage-plus" version = "0.13.4" @@ -3268,6 +3282,7 @@ dependencies = [ "nym-name-service-common", "nym-network-defaults", "nym-service-provider-directory-common", + "nym-sphinx", "nym-validator-client", "nym-vesting-contract-common", "rand 0.6.5", @@ -3819,6 +3834,7 @@ version = "1.1.19" dependencies = [ "async-file-watcher", "async-trait", + "bs58", "clap 4.2.7", "dirs", "futures", @@ -3841,11 +3857,13 @@ dependencies = [ "nym-sphinx", "nym-statistics-common", "nym-task", + "nym-types", "pretty_env_logger", "publicsuffix", "rand 0.7.3", "reqwest", "serde", + "serde_json", "sqlx 0.6.3", "tap", "tempfile", @@ -3970,8 +3988,12 @@ name = "nym-service-provider-directory-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2dc2f24aed..0dacee69a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,8 +119,9 @@ cosmwasm-derive = "=1.0.0" cosmwasm-schema = "=1.0.0" cosmwasm-std = "=1.0.0" cosmwasm-storage = "=1.0.0" -cw-utils = "=0.13.4" +cw-controllers = "=0.13.4" cw-storage-plus = "=0.13.4" +cw-utils = "=0.13.4" cw2 = { version = "=0.13.4" } cw3 = { version = "=0.13.4" } cw3-fixed-multisig = { version = "=0.13.4" } diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index b7d632a57a..8a3733273a 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -186,6 +186,15 @@ pub fn get_client_address( ) } +pub fn load_identity_keys( + pathfinder: &ClientKeyPathfinder, +) -> Result { + let identity_keypair: identity::KeyPair = + nym_pemstore::load_keypair(&pathfinder.identity_key_pair_path()) + .tap_err(|_| log::error!("Failed to read stored identity key files"))?; + Ok(identity_keypair) +} + /// Get the client address by loading the keys from stored files. pub fn get_client_address_from_stored_keys( config: &Config, @@ -193,18 +202,6 @@ pub fn get_client_address_from_stored_keys( where T: nym_config::NymConfig, { - fn load_identity_keys( - pathfinder: &ClientKeyPathfinder, - ) -> Result { - let identity_keypair: identity::KeyPair = - nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( - pathfinder.private_identity_key().to_owned(), - pathfinder.public_identity_key().to_owned(), - )) - .tap_err(|_| log::error!("Failed to read stored identity key files"))?; - Ok(identity_keypair) - } - fn load_sphinx_keys( pathfinder: &ClientKeyPathfinder, ) -> Result { diff --git a/common/client-libs/validator-client/src/nyxd/traits/sp_directory_query_client.rs b/common/client-libs/validator-client/src/nyxd/traits/sp_directory_query_client.rs index 3d4b469fbe..0ed99d5c1c 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/sp_directory_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/sp_directory_query_client.rs @@ -1,12 +1,12 @@ use async_trait::async_trait; use cosmrs::AccountId; -use nym_contracts_common::ContractBuildInformation; +use nym_contracts_common::{signing::Nonce, ContractBuildInformation}; use nym_service_provider_directory_common::{ msg::QueryMsg as SpQueryMsg, response::{ ConfigResponse, PagedServicesListResponse, ServiceInfoResponse, ServicesListResponse, }, - NymAddress, ServiceId, ServiceInfo, + NymAddress, Service, ServiceId, }; use serde::Deserialize; @@ -63,17 +63,14 @@ pub trait SpDirectoryQueryClient { .await } - async fn get_all_services(&self) -> Result, NyxdError> { + async fn get_all_services(&self) -> Result, NyxdError> { let mut services = Vec::new(); let mut start_after = None; - loop { let mut paged_response = self.get_services_paged(start_after.take(), None).await?; - - let last_id = paged_response.services.last().map(|serv| serv.service_id); services.append(&mut paged_response.services); - if let Some(start_after_res) = last_id { + if let Some(start_after_res) = paged_response.start_next_after { start_after = Some(start_after_res) } else { break; @@ -82,6 +79,13 @@ pub trait SpDirectoryQueryClient { Ok(services) } + + async fn get_service_signing_nonce(&self, address: &AccountId) -> Result { + self.query_service_provider_contract(SpQueryMsg::SigningNonce { + address: address.to_string(), + }) + .await + } } #[async_trait] diff --git a/common/client-libs/validator-client/src/nyxd/traits/sp_directory_signing_client.rs b/common/client-libs/validator-client/src/nyxd/traits/sp_directory_signing_client.rs index e535566b5e..31cf30839a 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/sp_directory_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/sp_directory_signing_client.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; +use nym_contracts_common::signing::MessageSignature; use nym_service_provider_directory_common::{ - msg::ExecuteMsg as SpExecuteMsg, NymAddress, ServiceId, ServiceType, + msg::ExecuteMsg as SpExecuteMsg, NymAddress, ServiceDetails, ServiceId, }; use crate::nyxd::{ @@ -22,16 +23,16 @@ pub trait SpDirectorySigningClient { async fn announce_service_provider( &self, - nym_address: NymAddress, - service_type: ServiceType, + service: ServiceDetails, + owner_signature: MessageSignature, deposit: Coin, fee: Option, ) -> Result { self.execute_service_provider_directory_contract( fee, SpExecuteMsg::Announce { - nym_address, - service_type, + service, + owner_signature, }, vec![deposit], ) diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 6fd2eae958..7f72077383 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -40,3 +40,4 @@ nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" } nym-service-provider-directory-common = { path = "../cosmwasm-smart-contracts/service-provider-directory" } nym-name-service-common = { path = "../cosmwasm-smart-contracts/name-service" } +nym-sphinx = { path = "../../common/nymsphinx" } diff --git a/common/commands/src/validator/mixnet/mod.rs b/common/commands/src/validator/mixnet/mod.rs index 9429ea6118..12cca7380f 100644 --- a/common/commands/src/validator/mixnet/mod.rs +++ b/common/commands/src/validator/mixnet/mod.rs @@ -14,6 +14,7 @@ pub struct Mixnet { pub command: MixnetCommands, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetCommands { /// Query the mixnet directory diff --git a/common/commands/src/validator/mixnet/operators/mod.rs b/common/commands/src/validator/mixnet/operators/mod.rs index fa92fb5518..008adf8ae2 100644 --- a/common/commands/src/validator/mixnet/operators/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mod.rs @@ -15,6 +15,7 @@ pub struct MixnetOperators { pub command: MixnetOperatorsCommands, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetOperatorsCommands { /// Manage your mixnode diff --git a/common/commands/src/validator/mixnet/operators/service/announce.rs b/common/commands/src/validator/mixnet/operators/service/announce.rs index 178993f203..a3e86bc674 100644 --- a/common/commands/src/validator/mixnet/operators/service/announce.rs +++ b/common/commands/src/validator/mixnet/operators/service/announce.rs @@ -1,6 +1,7 @@ use clap::Parser; use log::info; -use nym_service_provider_directory_common::{Coin, NymAddress, ServiceType}; +use nym_contracts_common::signing::MessageSignature; +use nym_service_provider_directory_common::{Coin, NymAddress, ServiceDetails, ServiceType}; use nym_validator_client::nyxd::traits::SpDirectorySigningClient; use crate::context::SigningClient; @@ -10,9 +11,15 @@ pub struct Args { #[clap(long)] pub nym_address: String, + #[clap(long)] + pub signature: MessageSignature, + /// Deposit to be made to the service provider directory, in curent DENOMINATION (e.g. 'unym') #[clap(long)] pub deposit: u128, + + #[clap(long)] + pub identity_key: String, } pub async fn announce(args: Args, client: SigningClient) { @@ -20,12 +27,17 @@ pub async fn announce(args: Args, client: SigningClient) { let nym_address = NymAddress::Address(args.nym_address); let service_type = ServiceType::NetworkRequester; + let service = ServiceDetails { + nym_address, + service_type, + identity_key: args.identity_key, + }; let denom = client.current_chain_details().mix_denom.base.as_str(); let deposit = Coin::new(args.deposit, denom); let res = client - .announce_service_provider(nym_address, service_type, deposit.into(), None) + .announce_service_provider(service, args.signature, deposit.into(), None) .await .expect("Failed to announce service provider"); diff --git a/common/commands/src/validator/mixnet/operators/service/announce_sign_payload.rs b/common/commands/src/validator/mixnet/operators/service/announce_sign_payload.rs new file mode 100644 index 0000000000..f8ac587f9b --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/service/announce_sign_payload.rs @@ -0,0 +1,61 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + context::SigningClient, + utils::{account_id_to_cw_addr, DataWrapper}, +}; + +use clap::Parser; +use cosmwasm_std::Coin; + +use nym_bin_common::output_format::OutputFormat; +use nym_service_provider_directory_common::{ + signing_types::construct_service_provider_announce_sign_payload, NymAddress, + ServiceType::NetworkRequester, +}; +use nym_sphinx::addressing::clients::Recipient; +use nym_validator_client::nyxd::traits::SpDirectoryQueryClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub nym_address: Recipient, + + #[clap(long)] + pub amount: u128, + + #[clap(long)] + pub identity_key: String, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let service = nym_service_provider_directory_common::ServiceDetails { + nym_address: NymAddress::new(&args.nym_address.to_string()), + service_type: NetworkRequester, + identity_key: args.identity_key, + }; + + let denom = client.current_chain_details().mix_denom.base.as_str(); + let deposit = Coin::new(args.amount, denom); + + let nonce = match client.get_service_signing_nonce(client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(client.address()); + let payload = + construct_service_provider_announce_sign_payload(nonce, address, deposit, service); + let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); + println!("{}", args.output.format(&wrapper)) +} diff --git a/common/commands/src/validator/mixnet/operators/service/mod.rs b/common/commands/src/validator/mixnet/operators/service/mod.rs index 4007909404..1367c51bf6 100644 --- a/common/commands/src/validator/mixnet/operators/service/mod.rs +++ b/common/commands/src/validator/mixnet/operators/service/mod.rs @@ -1,6 +1,7 @@ use clap::{Args, Subcommand}; pub mod announce; +pub mod announce_sign_payload; pub mod delete; #[derive(Debug, Args)] @@ -10,10 +11,13 @@ pub struct MixnetOperatorsService { pub command: MixnetOperatorsServiceCommands, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetOperatorsServiceCommands { /// Announce service provider to the world Announce(announce::Args), /// Delete entry for service provider from the directory Delete(delete::Args), + /// Create base58-encoded payload required for producing valid announce signature. + CreateServiceAnnounceSignPayload(announce_sign_payload::Args), } diff --git a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs index f803078eb8..e13f483f93 100644 --- a/common/commands/src/validator/mixnet/query/query_all_service_providers.rs +++ b/common/commands/src/validator/mixnet/query/query_all_service_providers.rs @@ -37,7 +37,7 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { for service in res.services { table.add_row(vec![ service.service_id.to_string(), - service.service.announcer.to_string(), + service.announcer.to_string(), service.service.service_type.to_string(), service.service.nym_address.to_string(), ]); diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 8ae7331d15..858907bafa 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -11,6 +11,9 @@ use std::ops::Mul; use std::str::FromStr; use thiserror::Error; +pub type IdentityKey = String; +pub type IdentityKeyRef<'a> = &'a str; + pub fn truncate_decimal(amount: Decimal) -> Uint128 { amount * Uint128::new(1) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index c17520a2b6..1ea03fec87 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -4,6 +4,7 @@ use crate::error::MixnetContractError; use crate::families::{Family, FamilyHead}; use crate::{Layer, RewardedSetNodeStatus}; +use contracts_common::IdentityKey; use cosmwasm_std::Addr; use cosmwasm_std::Coin; use schemars::JsonSchema; @@ -11,8 +12,6 @@ use serde::{Deserialize, Serialize}; use std::ops::Index; // type aliases for better reasoning about available data -pub type IdentityKey = String; -pub type IdentityKeyRef<'a> = &'a str; pub type SphinxKey = String; pub type SphinxKeyRef<'a> = &'a str; pub type EpochId = u32; diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/Cargo.toml b/common/cosmwasm-smart-contracts/service-provider-directory/Cargo.toml index 436cc543e3..6767dda750 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/Cargo.toml +++ b/common/cosmwasm-smart-contracts/service-provider-directory/Cargo.toml @@ -7,5 +7,9 @@ edition = "2021" [dependencies] cosmwasm-std = { workspace = true } +nym-contracts-common = { path = "../contracts-common", version = "0.4.0" } schemars = "0.8" serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +cw-utils = { workspace = true } +cw-controllers = { workspace = true } diff --git a/contracts/service-provider-directory/src/error.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/error.rs similarity index 67% rename from contracts/service-provider-directory/src/error.rs rename to common/cosmwasm-smart-contracts/service-provider-directory/src/error.rs index 8aca3d95f0..9d103be9c8 100644 --- a/contracts/service-provider-directory/src/error.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/error.rs @@ -1,10 +1,12 @@ use cosmwasm_std::{Addr, StdError}; use cw_controllers::AdminError; -use nym_service_provider_directory_common::{NymAddress, ServiceId}; +use nym_contracts_common::signing::verifier::ApiVerifierError; use thiserror::Error; +use crate::{NymAddress, ServiceId}; + #[derive(Error, Debug, PartialEq)] -pub enum ContractError { +pub enum SpContractError { #[error("{0}")] Std(#[from] StdError), @@ -46,6 +48,21 @@ pub enum ContractError { value: String, error_message: String, }, + + #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] + MalformedEd25519IdentityKey(String), + + #[error("Failed to recover ed25519 signature from its base58 representation - {0}")] + MalformedEd25519Signature(String), + + #[error("Provided ed25519 signature did not verify correctly")] + InvalidEd25519Signature, + + #[error("failed to verify message signature: {source}")] + SignatureVerificationFailure { + #[from] + source: ApiVerifierError, + }, } -pub(crate) type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/events.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/events.rs index 545f0b3267..80ce9c3593 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/src/events.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/events.rs @@ -39,16 +39,16 @@ pub fn new_announce_event(service_id: ServiceId, service: Service) -> Event { Event::new(ServiceProviderEventType::Announce) .add_attribute(ACTION, ServiceProviderEventType::Announce) .add_attribute(SERVICE_ID, service_id.to_string()) - .add_attribute(SERVICE_TYPE, service.service_type.to_string()) - .add_attribute(NYM_ADDRESS, service.nym_address.to_string()) + .add_attribute(SERVICE_TYPE, service.service.service_type.to_string()) + .add_attribute(NYM_ADDRESS, service.service.nym_address.to_string()) .add_attribute(OWNER, service.announcer.to_string()) } -pub fn new_delete_id_event(service_id: ServiceId, service: Service) -> Event { +pub fn new_delete_id_event(service: Service) -> Event { Event::new(ServiceProviderEventType::DeleteId) .add_attribute(ACTION, ServiceProviderEventType::DeleteId) - .add_attribute(SERVICE_ID, service_id.to_string()) - .add_attribute(NYM_ADDRESS, service.nym_address.to_string()) + .add_attribute(SERVICE_ID, service.service_id.to_string()) + .add_attribute(NYM_ADDRESS, service.service.nym_address.to_string()) } pub fn new_update_deposit_required_event(deposit_required: Coin) -> Event { diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/lib.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/lib.rs index 4609473c60..71a702b7c1 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/src/lib.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/lib.rs @@ -1,6 +1,8 @@ +pub mod error; pub mod events; pub mod msg; pub mod response; +pub mod signing_types; pub mod types; // Re-export all types at the top-level diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/msg.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/msg.rs index ba746c3b10..f66ad787a5 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/src/msg.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/msg.rs @@ -1,5 +1,6 @@ -use crate::{NymAddress, ServiceId, ServiceType}; +use crate::{NymAddress, ServiceDetails, ServiceId}; use cosmwasm_std::Coin; +use nym_contracts_common::signing::MessageSignature; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] @@ -22,8 +23,8 @@ pub struct MigrateMsg {} #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { Announce { - nym_address: NymAddress, - service_type: ServiceType, + service: ServiceDetails, + owner_signature: MessageSignature, }, DeleteId { service_id: ServiceId, @@ -44,9 +45,12 @@ impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { ExecuteMsg::Announce { - nym_address, - service_type, - } => format!("announcing {nym_address} as type {service_type}"), + service, + owner_signature: _, + } => format!( + "announcing {} as type {}", + service.nym_address, service.service_type + ), ExecuteMsg::DeleteId { service_id } => { format!("deleting service with service id {service_id}") } @@ -76,6 +80,9 @@ pub enum QueryMsg { limit: Option, start_after: Option, }, + SigningNonce { + address: String, + }, Config {}, GetContractVersion {}, #[serde(rename = "get_cw2_contract_version")] diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/response.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/response.rs index b7d4241762..8b6bf16fbf 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/src/response.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/response.rs @@ -1,4 +1,4 @@ -use crate::{msg::ExecuteMsg, Service, ServiceId, ServiceInfo}; +use crate::{Service, ServiceId}; use cosmwasm_std::Coin; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -13,22 +13,17 @@ pub struct ServiceInfoResponse { #[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct ServicesListResponse { - pub services: Vec, + pub services: Vec, } impl ServicesListResponse { - pub fn new(services: Vec<(ServiceId, Service)>) -> ServicesListResponse { - ServicesListResponse { - services: services - .into_iter() - .map(|(service_id, service)| ServiceInfo::new(service_id, service)) - .collect(), - } + pub fn new(services: Vec) -> ServicesListResponse { + ServicesListResponse { services } } } -impl From<&[ServiceInfo]> for ServicesListResponse { - fn from(services: &[ServiceInfo]) -> Self { +impl From<&[Service]> for ServicesListResponse { + fn from(services: &[Service]) -> Self { Self { services: services.to_vec(), } @@ -38,21 +33,17 @@ impl From<&[ServiceInfo]> for ServicesListResponse { #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[serde(rename_all = "snake_case")] pub struct PagedServicesListResponse { - pub services: Vec, + pub services: Vec, pub per_page: usize, pub start_next_after: Option, } impl PagedServicesListResponse { pub fn new( - services: Vec<(ServiceId, Service)>, + services: Vec, per_page: usize, start_next_after: Option, ) -> PagedServicesListResponse { - let services = services - .into_iter() - .map(|(service_id, service)| ServiceInfo::new(service_id, service)) - .collect(); PagedServicesListResponse { services, per_page, @@ -66,12 +57,3 @@ impl PagedServicesListResponse { pub struct ConfigResponse { pub deposit_required: Coin, } - -impl From for ExecuteMsg { - fn from(service: Service) -> Self { - ExecuteMsg::Announce { - nym_address: service.nym_address, - service_type: service.service_type, - } - } -} diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/signing_types.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/signing_types.rs new file mode 100644 index 0000000000..742470b2cd --- /dev/null +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/signing_types.rs @@ -0,0 +1,33 @@ +use cosmwasm_std::{Addr, Coin}; +use nym_contracts_common::signing::{ + ContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, +}; +use serde::Serialize; + +use crate::ServiceDetails; + +pub type SignableServiceProviderAnnounceMsg = + SignableMessage>; + +#[derive(Serialize)] +pub struct ServiceProviderAnnounce { + service: ServiceDetails, +} + +impl SigningPurpose for ServiceProviderAnnounce { + fn message_type() -> MessageType { + MessageType::new("service-provider-announce") + } +} + +pub fn construct_service_provider_announce_sign_payload( + nonce: Nonce, + sender: Addr, + deposit: Coin, + service: ServiceDetails, +) -> SignableServiceProviderAnnounceMsg { + let payload = ServiceProviderAnnounce { service }; + let proxy = None; + let content = ContractMessageContent::new(sender, proxy, vec![deposit], payload); + SignableMessage::new(nonce, content) +} diff --git a/common/cosmwasm-smart-contracts/service-provider-directory/src/types.rs b/common/cosmwasm-smart-contracts/service-provider-directory/src/types.rs index faae77263b..c3ad296fa2 100644 --- a/common/cosmwasm-smart-contracts/service-provider-directory/src/types.rs +++ b/common/cosmwasm-smart-contracts/service-provider-directory/src/types.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; use cosmwasm_std::{Addr, Coin}; +use nym_contracts_common::IdentityKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,11 +10,11 @@ pub type ServiceId = u32; #[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)] pub struct Service { - /// The address of the service. - pub nym_address: NymAddress, - /// The service type. - pub service_type: ServiceType, - /// Service owner. + /// Unique id assigned to the anounced service. + pub service_id: ServiceId, + /// The announced service. + pub service: ServiceDetails, + /// Address of the service owner. pub announcer: Addr, /// Block height at which the service was added. pub block_height: u64, @@ -21,6 +22,16 @@ pub struct Service { pub deposit: Coin, } +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)] +pub struct ServiceDetails { + /// The address of the service. + pub nym_address: NymAddress, + /// The service type. + pub service_type: ServiceType, + /// The identity key of the service. + pub identity_key: IdentityKey, +} + /// The types of addresses supported. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -28,7 +39,7 @@ pub enum NymAddress { /// String representation of a nym address, which is of the form /// client_id.client_enc@gateway_id. Address(String), - // For the future when we have a nym-dns contract + // String name that can looked up in the nym-name-service contract (once it exists) //Name(String), } @@ -41,6 +52,7 @@ impl NymAddress { pub fn as_str(&self) -> &str { match self { NymAddress::Address(address) => address, + //NymAddress::Name(name) => name, } } } @@ -66,19 +78,3 @@ impl std::fmt::Display for ServiceType { write!(f, "{service_type}") } } - -#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ServiceInfo { - pub service_id: ServiceId, - pub service: Service, -} - -impl ServiceInfo { - pub fn new(service_id: ServiceId, service: Service) -> Self { - Self { - service_id, - service, - } - } -} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index cccdd68c81..d3394aadb2 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1265,6 +1265,7 @@ name = "nym-service-provider-directory" version = "0.1.0" dependencies = [ "anyhow", + "bs58", "cosmwasm-std", "cw-controllers", "cw-multi-test", @@ -1272,7 +1273,10 @@ dependencies = [ "cw-utils", "cw2", "nym-contracts-common", + "nym-crypto", "nym-service-provider-directory-common", + "rand_chacha 0.2.2", + "rstest", "semver", "serde", "thiserror", @@ -1284,8 +1288,12 @@ name = "nym-service-provider-directory-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 2a78d3b1c7..80e6822f89 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -14,7 +14,7 @@ mod mixnet_contract_settings; mod mixnodes; mod queued_migrations; mod rewards; -mod signing; +pub mod signing; mod support; #[cfg(feature = "contract-testing")] diff --git a/contracts/name-service/src/state/names.rs b/contracts/name-service/src/state/names.rs index 6fdd4181ad..81190687f6 100644 --- a/contracts/name-service/src/state/names.rs +++ b/contracts/name-service/src/state/names.rs @@ -188,6 +188,7 @@ mod tests { use super::*; type TestDeps = OwnedDeps; + #[rstest::fixture] fn deps() -> TestDeps { instantiate_test_contract() @@ -691,7 +692,6 @@ mod tests { #[test] #[ignore] fn max_page_limit_is_applied() { - // WIP(JON) todo!(); } } diff --git a/contracts/service-provider-directory/Cargo.toml b/contracts/service-provider-directory/Cargo.toml index ea502b163a..7181a0b162 100644 --- a/contracts/service-provider-directory/Cargo.toml +++ b/contracts/service-provider-directory/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] +bs58 = "0.4.0" cosmwasm-std = { workspace = true } cw-controllers = { workspace = true } cw-storage-plus = { workspace = true } @@ -22,5 +23,8 @@ thiserror = "1.0.39" vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] } [dev-dependencies] -cw-multi-test = { workspace = true } anyhow = "1.0.40" +cw-multi-test = { workspace = true } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } +rand_chacha = "0.2" +rstest = "0.17.0" diff --git a/contracts/service-provider-directory/src/constants.rs b/contracts/service-provider-directory/src/constants.rs index ded70c2592..e83c6297e8 100644 --- a/contracts/service-provider-directory/src/constants.rs +++ b/contracts/service-provider-directory/src/constants.rs @@ -13,3 +13,5 @@ pub const SERVICE_ID_COUNTER_KEY: &str = "sidc"; pub const SERVICES_PK_NAMESPACE: &str = "sernames"; pub const SERVICES_ANNOUNCER_IDX_NAMESPACE: &str = "serown"; pub const SERVICES_NYM_ADDRESS_IDX_NAMESPACE: &str = "sernyma"; + +pub const SIGNING_NONCES_NAMESPACE: &str = "sn"; diff --git a/contracts/service-provider-directory/src/contract.rs b/contracts/service-provider-directory/src/contract.rs index 7b5e4f49e9..9a760c7788 100644 --- a/contracts/service-provider-directory/src/contract.rs +++ b/contracts/service-provider-directory/src/contract.rs @@ -1,6 +1,6 @@ use crate::{ - error::{ContractError, Result}, state::{self, Config}, + Result, SpContractError, }; use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; use nym_service_provider_directory_common::msg::{ @@ -34,22 +34,25 @@ pub fn instantiate( .add_attribute("admin", info.sender)) } -pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate( + deps: DepsMut<'_>, + _env: Env, + _msg: MigrateMsg, +) -> Result { // Note: don't remove this particular bit of code as we have to ALWAYS check whether we have to // update the stored version - let version: Version = - CONTRACT_VERSION - .parse() - .map_err(|error: semver::Error| ContractError::SemVerFailure { - value: CONTRACT_VERSION.to_string(), - error_message: error.to_string(), - })?; + let version: Version = CONTRACT_VERSION.parse().map_err(|error: semver::Error| { + SpContractError::SemVerFailure { + value: CONTRACT_VERSION.to_string(), + error_message: error.to_string(), + } + })?; let storage_version_raw = cw2::get_contract_version(deps.storage)?.version; let storage_version: Version = storage_version_raw .parse() - .map_err(|error: semver::Error| ContractError::SemVerFailure { + .map_err(|error: semver::Error| SpContractError::SemVerFailure { value: storage_version_raw, error_message: error.to_string(), })?; @@ -69,12 +72,12 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result { match msg { ExecuteMsg::Announce { - nym_address: client_address, - service_type, - } => execute::announce(deps, env, info, client_address, service_type), + service, + owner_signature, + } => execute::announce(deps, env, info, service, owner_signature), ExecuteMsg::DeleteId { service_id } => execute::delete_id(deps, info, service_id), ExecuteMsg::DeleteNymAddress { nym_address } => { execute::delete_nym_address(deps, info, nym_address) @@ -95,6 +98,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { QueryMsg::All { limit, start_after } => { to_binary(&query::query_all_paged(deps, limit, start_after)?) } + QueryMsg::SigningNonce { address } => { + to_binary(&query::query_current_signing_nonce(deps, address)?) + } QueryMsg::Config {} => to_binary(&query::query_config(deps)?), QueryMsg::GetContractVersion {} => to_binary(&query::query_contract_version()), QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?), @@ -107,16 +113,19 @@ mod tests { use super::*; use crate::test_helpers::{ - assert::{assert_config, assert_empty, assert_not_found, assert_service, assert_services}, - fixture::service_fixture, - helpers::{get_attribute, nyms}, + assert::{ + assert_config, assert_current_nonce, assert_empty, assert_not_found, assert_service, + assert_services, + }, + fixture::new_service_details_with_sign, + helpers::{get_attribute, nyms, test_rng}, }; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, Addr, Coin, }; - use nym_service_provider_directory_common::{msg::ExecuteMsg, ServiceId, ServiceInfo}; + use nym_service_provider_directory_common::{msg::ExecuteMsg, Service, ServiceId}; const DENOM: &str = "unym"; @@ -140,27 +149,33 @@ mod tests { } #[test] - fn announce_fails_incorrect_deposit() { + fn announce_fails_incorrect_deposit_too_small() { + let mut rng = test_rng(); let mut deps = mock_dependencies(); let msg = InstantiateMsg::new(nyms(100)); let info = mock_info("creator", &[]); - let admin = info.sender.clone(); let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!(res.messages.len(), 0); - // Announce - let msg: ExecuteMsg = service_fixture().into(); - let announcer = service_fixture().announcer.to_string(); + // Setup service + let deposit = nyms(99); + let announcer = "steve"; + let (service, owner_signature) = + new_service_details_with_sign(deps.as_mut(), &mut rng, "nym", announcer, deposit); + let msg = ExecuteMsg::Announce { + service, + owner_signature, + }; assert_eq!( execute( deps.as_mut(), mock_env(), - mock_info(&announcer, &[nyms(99)]), + mock_info(announcer, &[nyms(99)]), msg.clone() ) .unwrap_err(), - ContractError::InsufficientDeposit { + SpContractError::InsufficientDeposit { funds: 99u128.into(), deposit_required: 100u128.into(), } @@ -170,11 +185,55 @@ mod tests { execute( deps.as_mut(), mock_env(), - mock_info(&announcer, &[nyms(101)]), - msg + mock_info(announcer, &[nyms(100)]), + msg, ) .unwrap_err(), - ContractError::TooLargeDeposit { + SpContractError::InvalidEd25519Signature, + ); + } + + // Announcing a service fails due to the signed deposit being different from the deposit in + // the message. + #[test] + fn announce_fails_incorrect_deposit_too_large() { + let mut rng = test_rng(); + let mut deps = mock_dependencies(); + let msg = InstantiateMsg::new(nyms(100)); + let info = mock_info("creator", &[]); + let admin = info.sender.clone(); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + // Setup service + let deposit = nyms(101); + let announcer = "steve"; + let (service, owner_signature) = + new_service_details_with_sign(deps.as_mut(), &mut rng, "nym", announcer, deposit); + let msg = ExecuteMsg::Announce { + service, + owner_signature, + }; + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(announcer, &[nyms(100)]), + msg.clone() + ) + .unwrap_err(), + SpContractError::InvalidEd25519Signature, + ); + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(announcer, &[nyms(101)]), + msg, + ) + .unwrap_err(), + SpContractError::TooLargeDeposit { funds: 101u128.into(), deposit_required: 100u128.into(), } @@ -186,15 +245,26 @@ mod tests { #[test] fn announce_success() { + let mut rng = test_rng(); let mut deps = mock_dependencies(); let msg = InstantiateMsg::new(nyms(100)); let info = mock_info("creator", &[]); let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!(res.messages.len(), 0); + assert_current_nonce(deps.as_ref(), &Addr::unchecked("steve"), 0); + + // Setup service + let deposit = nyms(100); + let owner = "steve"; + let (service, owner_signature) = + new_service_details_with_sign(deps.as_mut(), &mut rng, "nym", owner, deposit.clone()); // Announce - let msg: ExecuteMsg = service_fixture().into(); - let info = mock_info("steve", &[nyms(100)]); + let msg = ExecuteMsg::Announce { + service: service.clone(), + owner_signature, + }; + let info = mock_info("steve", &[deposit.clone()]); let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); // Check that the service has had service id assigned to it @@ -208,10 +278,17 @@ mod tests { "network_requester".to_string() ); + // Check that the nonce has been incremented, but only for the owner + assert_current_nonce(deps.as_ref(), &Addr::unchecked("steve"), 1); + assert_current_nonce(deps.as_ref(), &Addr::unchecked("timmy"), 0); + // The expected announced service - let expected_service = ServiceInfo { + let expected_service = Service { service_id: expected_id, - service: service_fixture(), + service, + announcer: Addr::unchecked("steve"), + block_height: 12345, + deposit, }; assert_services(deps.as_ref(), &[expected_service.clone()]); assert_service(deps.as_ref(), &expected_service); @@ -219,6 +296,7 @@ mod tests { #[test] fn delete() { + let mut rng = test_rng(); let mut deps = mock_dependencies(); let msg = InstantiateMsg::new(Coin::new(100, "unym")); let info = mock_info("creator", &[]); @@ -226,16 +304,25 @@ mod tests { assert_eq!(res.messages.len(), 0); // Announce - let msg: ExecuteMsg = service_fixture().into(); - let info_steve = mock_info("steve", &[nyms(100)]); - assert_eq!(info_steve.sender, service_fixture().announcer); - execute(deps.as_mut(), mock_env(), info_steve, msg).unwrap(); + let deposit = nyms(100); + let steve = "steve"; + let (service, owner_signature) = + new_service_details_with_sign(deps.as_mut(), &mut rng, "nym", steve, deposit.clone()); + let msg = ExecuteMsg::Announce { + service: service.clone(), + owner_signature, + }; + let info_steve = mock_info(steve, &[deposit.clone()]); + execute(deps.as_mut(), mock_env(), info_steve.clone(), msg).unwrap(); // The expected announced service let expected_id = 1; - let expected_service = ServiceInfo { + let expected_service = Service { service_id: expected_id, - service: service_fixture(), + service, + announcer: Addr::unchecked(steve), + block_height: 12345, + deposit, }; assert_services(deps.as_ref(), &[expected_service]); @@ -244,27 +331,23 @@ mod tests { let info_timmy = mock_info("timmy", &[]); assert_eq!( execute(deps.as_mut(), mock_env(), info_timmy, msg).unwrap_err(), - ContractError::Unauthorized { + SpContractError::Unauthorized { sender: Addr::unchecked("timmy") } ); // Removing an non-existent service will fail let msg = ExecuteMsg::delete_id(expected_id + 1); - let info_announcer = MessageInfo { - sender: service_fixture().announcer, - funds: vec![], - }; assert_eq!( - execute(deps.as_mut(), mock_env(), info_announcer.clone(), msg).unwrap_err(), - ContractError::NotFound { + execute(deps.as_mut(), mock_env(), info_steve.clone(), msg).unwrap_err(), + SpContractError::NotFound { service_id: expected_id + 1 } ); // Remove as correct announcer succeeds let msg = ExecuteMsg::delete_id(expected_id); - let res = execute(deps.as_mut(), mock_env(), info_announcer, msg).unwrap(); + let res = execute(deps.as_mut(), mock_env(), info_steve, msg).unwrap(); assert_eq!( get_attribute(&res, "delete_id", "service_id"), expected_id.to_string() diff --git a/contracts/service-provider-directory/src/contract/execute.rs b/contracts/service-provider-directory/src/contract/execute.rs index 7f4e091dd5..f3e240f7ad 100644 --- a/contracts/service-provider-directory/src/contract/execute.rs +++ b/contracts/service-provider-directory/src/contract/execute.rs @@ -1,24 +1,28 @@ use crate::{ constants::{MAX_NUMBER_OF_ALIASES_FOR_NYM_ADDRESS, MAX_NUMBER_OF_PROVIDERS_PER_ANNOUNCER}, - error::{ContractError, Result}, - state, + state, Result, SpContractError, }; use cosmwasm_std::{Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Response, Uint128}; +use nym_contracts_common::{ + signing::{MessageSignature, Verifier}, + IdentityKey, +}; use nym_service_provider_directory_common::{ events::{new_announce_event, new_delete_id_event, new_update_deposit_required_event}, - NymAddress, Service, ServiceId, ServiceType, + signing_types::construct_service_provider_announce_sign_payload, + NymAddress, Service, ServiceDetails, ServiceId, }; use super::query; fn ensure_correct_deposit(will_deposit: Uint128, deposit_required: Uint128) -> Result<()> { match will_deposit.cmp(&deposit_required) { - std::cmp::Ordering::Less => Err(ContractError::InsufficientDeposit { + std::cmp::Ordering::Less => Err(SpContractError::InsufficientDeposit { funds: will_deposit, deposit_required, }), std::cmp::Ordering::Equal => Ok(()), - std::cmp::Ordering::Greater => Err(ContractError::TooLargeDeposit { + std::cmp::Ordering::Greater => Err(SpContractError::TooLargeDeposit { funds: will_deposit, deposit_required, }), @@ -30,7 +34,7 @@ fn ensure_max_services_per_announcer(deps: Deps, announcer: Addr) -> Result<()> if current_entries.services.len() < MAX_NUMBER_OF_PROVIDERS_PER_ANNOUNCER as usize { Ok(()) } else { - Err(ContractError::ReachedMaxProvidersForAdmin { + Err(SpContractError::ReachedMaxProvidersForAdmin { max_providers: MAX_NUMBER_OF_PROVIDERS_PER_ANNOUNCER, announcer, }) @@ -42,7 +46,7 @@ fn ensure_max_aliases_per_nym_address(deps: Deps, nym_address: NymAddress) -> Re if current_entries.services.len() < MAX_NUMBER_OF_ALIASES_FOR_NYM_ADDRESS as usize { Ok(()) } else { - Err(ContractError::ReachedMaxAliasesForNymAddress { + Err(SpContractError::ReachedMaxAliasesForNymAddress { max_aliases: MAX_NUMBER_OF_ALIASES_FOR_NYM_ADDRESS, nym_address, }) @@ -50,10 +54,10 @@ fn ensure_max_aliases_per_nym_address(deps: Deps, nym_address: NymAddress) -> Re } fn ensure_service_exists(deps: Deps, service_id: ServiceId) -> Result<()> { - if state::services::has_service(deps.storage, service_id) { + if state::has_service(deps.storage, service_id) { Ok(()) } else { - Err(ContractError::NotFound { service_id }) + Err(SpContractError::NotFound { service_id }) } } @@ -61,7 +65,7 @@ fn ensure_sender_authorized(info: MessageInfo, service: &Service) -> Result<()> if info.sender == service.announcer { Ok(()) } else { - Err(ContractError::Unauthorized { + Err(SpContractError::Unauthorized { sender: info.sender, }) } @@ -74,31 +78,82 @@ fn return_deposit(service_to_delete: &Service) -> BankMsg { } } +fn verify_announce_signature( + deps: Deps<'_>, + sender: Addr, + deposit: Coin, + service: ServiceDetails, + signature: MessageSignature, +) -> Result<()> { + // recover the public key + let public_key = decode_ed25519_identity_key(&service.identity_key)?; + + // reconstruct the payload + let nonce = state::get_signing_nonce(deps.storage, sender.clone())?; + + let msg = construct_service_provider_announce_sign_payload(nonce, sender, deposit, service); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(SpContractError::InvalidEd25519Signature) + } +} + +fn decode_ed25519_identity_key(encoded: &IdentityKey) -> Result<[u8; 32]> { + let mut public_key = [0u8; 32]; + let used = bs58::decode(encoded) + .into(&mut public_key) + .map_err(|err| SpContractError::MalformedEd25519IdentityKey(err.to_string()))?; + + if used != 32 { + return Err(SpContractError::MalformedEd25519IdentityKey( + "Too few bytes provided for the public key".into(), + )); + } + + Ok(public_key) +} + /// Announce a new service. It will be assigned a new service provider id. pub fn announce( deps: DepsMut, env: Env, info: MessageInfo, - nym_address: NymAddress, - service_type: ServiceType, + service: ServiceDetails, + owner_signature: MessageSignature, ) -> Result { ensure_max_services_per_announcer(deps.as_ref(), info.sender.clone())?; - ensure_max_aliases_per_nym_address(deps.as_ref(), nym_address.clone())?; + ensure_max_aliases_per_nym_address(deps.as_ref(), service.nym_address.clone())?; let deposit_required = state::deposit_required(deps.storage)?; let denom = deposit_required.denom.clone(); let will_deposit = cw_utils::must_pay(&info, &denom) - .map_err(|err| ContractError::DepositRequired { source: err })?; + .map_err(|err| SpContractError::DepositRequired { source: err })?; ensure_correct_deposit(will_deposit, deposit_required.amount)?; + let deposit = Coin::new(will_deposit.u128(), denom); + + // Check that the sender actually owns the service provider by checking the signature + verify_announce_signature( + deps.as_ref(), + info.sender.clone(), + deposit.clone(), + service.clone(), + owner_signature, + )?; + + state::increment_signing_nonce(deps.storage, info.sender.clone())?; + + let service_id = state::next_service_id_counter(deps.storage)?; let new_service = Service { - nym_address, - service_type, + service_id, + service, announcer: info.sender, block_height: env.block.height, - deposit: Coin::new(will_deposit.u128(), denom), + deposit, }; - let service_id = state::services::save(deps.storage, &new_service)?; + state::save(deps.storage, &new_service)?; Ok(Response::new().add_event(new_announce_event(service_id, new_service))) } @@ -106,15 +161,15 @@ pub fn announce( /// Delete an exsisting service. pub fn delete_id(deps: DepsMut, info: MessageInfo, service_id: ServiceId) -> Result { ensure_service_exists(deps.as_ref(), service_id)?; - let service_to_delete = state::services::load_id(deps.storage, service_id)?; + let service_to_delete = state::load_id(deps.storage, service_id)?; ensure_sender_authorized(info, &service_to_delete)?; - state::services::remove(deps.storage, service_id)?; + state::remove(deps.storage, service_id)?; let return_deposit_msg = return_deposit(&service_to_delete); Ok(Response::new() .add_message(return_deposit_msg) - .add_event(new_delete_id_event(service_id, service_to_delete))) + .add_event(new_delete_id_event(service_to_delete))) } /// Delete an existing service by nym address. If there are multiple entries for a given nym @@ -128,15 +183,12 @@ pub(crate) fn delete_nym_address( let services_to_delete = query::query_nym_address(deps.as_ref(), nym_address)?.services; for service_to_delete in services_to_delete { - if info.sender == service_to_delete.service.announcer { - state::services::remove(deps.storage, service_to_delete.service_id)?; - let return_deposit_msg = return_deposit(&service_to_delete.service); + if info.sender == service_to_delete.announcer { + state::remove(deps.storage, service_to_delete.service_id)?; + let return_deposit_msg = return_deposit(&service_to_delete); response = response .add_message(return_deposit_msg) - .add_event(new_delete_id_event( - service_to_delete.service_id, - service_to_delete.service, - )); + .add_event(new_delete_id_event(service_to_delete)); } } Ok(response) diff --git a/contracts/service-provider-directory/src/contract/query.rs b/contracts/service-provider-directory/src/contract/query.rs index 77126a27bf..9aae594474 100644 --- a/contracts/service-provider-directory/src/contract/query.rs +++ b/contracts/service-provider-directory/src/contract/query.rs @@ -1,31 +1,27 @@ use cosmwasm_std::Deps; -use nym_contracts_common::ContractBuildInformation; +use nym_contracts_common::{signing::Nonce, ContractBuildInformation}; use nym_service_provider_directory_common::{ response::{ConfigResponse, PagedServicesListResponse, ServicesListResponse}, - NymAddress, ServiceId, ServiceInfo, + NymAddress, Service, ServiceId, }; use crate::{ - error::Result, - state::{self, services::PagedLoad}, + state::{self, PagedLoad}, + Result, }; -pub fn query_id(deps: Deps, service_id: ServiceId) -> Result { - let service = state::services::load_id(deps.storage, service_id)?; - Ok(ServiceInfo { - service_id, - service, - }) +pub fn query_id(deps: Deps, service_id: ServiceId) -> Result { + state::load_id(deps.storage, service_id) } pub fn query_announcer(deps: Deps, announcer: String) -> Result { let announcer = deps.api.addr_validate(&announcer)?; - let services = state::services::load_announcer(deps.storage, announcer)?; + let services = state::load_announcer(deps.storage, announcer)?; Ok(ServicesListResponse::new(services)) } pub fn query_nym_address(deps: Deps, nym_address: NymAddress) -> Result { - let services = state::services::load_nym_address(deps.storage, nym_address)?; + let services = state::load_nym_address(deps.storage, nym_address)?; Ok(ServicesListResponse::new(services)) } @@ -38,7 +34,7 @@ pub fn query_all_paged( services, limit, start_next_after, - } = state::services::load_all_paged(deps.storage, limit, start_after)?; + } = state::load_all_paged(deps.storage, limit, start_after)?; Ok(PagedServicesListResponse::new( services, limit, @@ -46,6 +42,11 @@ pub fn query_all_paged( )) } +pub fn query_current_signing_nonce(deps: Deps<'_>, address: String) -> Result { + let address = deps.api.addr_validate(&address)?; + state::get_signing_nonce(deps.storage, address) +} + pub fn query_config(deps: Deps) -> Result { let config = state::load_config(deps.storage)?; Ok(config.into()) diff --git a/contracts/service-provider-directory/src/integration_tests.rs b/contracts/service-provider-directory/src/integration_tests.rs deleted file mode 100644 index 13e5145d35..0000000000 --- a/contracts/service-provider-directory/src/integration_tests.rs +++ /dev/null @@ -1,355 +0,0 @@ -//! Integration tests using cw-multi-test. - -use cosmwasm_std::Addr; -use nym_service_provider_directory_common::{ - response::{ConfigResponse, PagedServicesListResponse}, - NymAddress, Service, ServiceInfo, ServiceType, -}; - -use crate::{ - constants::SERVICE_DEFAULT_RETRIEVAL_LIMIT, - error::ContractError, - test_helpers::{fixture::service_info, helpers::nyms, test_setup::TestSetup}, -}; - -#[test] -fn instantiate_contract() { - TestSetup::new(); -} - -#[test] -fn query_config() { - assert_eq!( - TestSetup::new().query_config(), - ConfigResponse { - deposit_required: nyms(100), - } - ); -} - -#[test] -fn announce_and_query_service() { - let mut setup = TestSetup::new(); - assert_eq!( - setup.query_all(), - PagedServicesListResponse { - services: vec![], - per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, - start_next_after: None, - } - ); - - // Announce a first service - let announcer = Addr::unchecked("announcer"); - let nym_address = NymAddress::new("nymAddress"); - assert_eq!(setup.contract_balance(), nyms(0)); - assert_eq!(setup.balance(&announcer), nyms(250)); - setup.announce_net_req(nym_address.clone(), announcer.clone()); - - // Deposit is deposited to contract and deducted from announcers's balance - assert_eq!(setup.contract_balance(), nyms(100)); - assert_eq!(setup.balance(&announcer), nyms(150)); - - // We can query the full service list - assert_eq!( - setup.query_all(), - PagedServicesListResponse { - services: vec![ServiceInfo { - service_id: 1, - service: Service { - nym_address: nym_address.clone(), - service_type: ServiceType::NetworkRequester, - announcer: announcer.clone(), - block_height: 12345, - deposit: nyms(100), - }, - }], - per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, - start_next_after: Some(1), - } - ); - - // ... and we can query by id - assert_eq!( - setup.query_id(1), - ServiceInfo { - service_id: 1, - service: Service { - nym_address: nym_address.clone(), - service_type: ServiceType::NetworkRequester, - announcer: announcer.clone(), - block_height: 12345, - deposit: nyms(100), - }, - } - ); - - // Announce a second service - let announcer2 = Addr::unchecked("announcer2"); - let nym_address2 = NymAddress::new("nymAddress2"); - setup.announce_net_req(nym_address2.clone(), announcer2.clone()); - - assert_eq!(setup.contract_balance(), nyms(200)); - assert_eq!( - setup.query_all(), - PagedServicesListResponse { - services: vec![ - service_info(1, nym_address, announcer), - service_info(2, nym_address2, announcer2) - ], - per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, - start_next_after: Some(2), - } - ); -} - -#[test] -fn delete_service() { - let mut setup = TestSetup::new(); - setup.announce_net_req(NymAddress::new("nymAddress"), Addr::unchecked("announcer")); - assert_eq!(setup.contract_balance(), nyms(100)); - assert_eq!(setup.balance("announcer"), nyms(150)); - assert!(!setup.query_all().services.is_empty()); - setup.delete(1, Addr::unchecked("announcer")); - - // Deleting the service returns the deposit to the announcer - assert_eq!(setup.contract_balance(), nyms(0)); - assert_eq!(setup.balance("announcer"), nyms(250)); - assert!(setup.query_all().services.is_empty()); -} - -#[test] -fn only_announcer_can_delete_service() { - let mut setup = TestSetup::new(); - assert_eq!(setup.contract_balance(), nyms(0)); - setup.announce_net_req(NymAddress::new("nymAddress"), Addr::unchecked("announcer")); - assert_eq!(setup.contract_balance(), nyms(100)); - assert!(!setup.query_all().services.is_empty()); - - let delete_resp: ContractError = setup - .try_delete(1, Addr::unchecked("not_announcer")) - .unwrap_err() - .downcast() - .unwrap(); - - assert_eq!(setup.contract_balance(), nyms(100)); - assert_eq!( - delete_resp, - ContractError::Unauthorized { - sender: Addr::unchecked("not_announcer") - } - ); -} - -#[test] -fn cant_delete_service_that_does_not_exist() { - let mut setup = TestSetup::new(); - setup.announce_net_req(NymAddress::new("nymAddress"), Addr::unchecked("announcer")); - assert_eq!(setup.contract_balance(), nyms(100)); - assert!(!setup.query_all().services.is_empty()); - - let delete_resp: ContractError = setup - .try_delete(0, Addr::unchecked("announcer")) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(setup.contract_balance(), nyms(100)); - assert_eq!(delete_resp, ContractError::NotFound { service_id: 0 }); - - let delete_resp: ContractError = setup - .try_delete(2, Addr::unchecked("announcer")) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(setup.contract_balance(), nyms(100)); - assert_eq!(delete_resp, ContractError::NotFound { service_id: 2 }); - - assert!(!setup.query_all().services.is_empty()); - setup.delete(1, Addr::unchecked("announcer")); - assert_eq!(setup.contract_balance(), nyms(0)); - assert!(setup.query_all().services.is_empty()); -} - -#[test] -fn announce_multiple_services_and_deleting_by_name() { - let mut setup = TestSetup::new(); - let announcer1 = Addr::unchecked("wealthy_announcer_1"); - let announcer2 = Addr::unchecked("wealthy_announcer_2"); - let nym_address1 = NymAddress::new("nymAddress1"); - let nym_address2 = NymAddress::new("nymAddress2"); - - // We announce the same address three times, but with different annoucers - assert_eq!(setup.contract_balance(), nyms(0)); - assert_eq!(setup.balance(&announcer1), nyms(1000)); - setup.announce_net_req(nym_address1.clone(), announcer1.clone()); - setup.announce_net_req(nym_address1.clone(), announcer1.clone()); - setup.announce_net_req(nym_address2.clone(), announcer1.clone()); - setup.announce_net_req(nym_address1.clone(), announcer2.clone()); - setup.announce_net_req(nym_address2.clone(), announcer2.clone()); - - assert_eq!(setup.contract_balance(), nyms(500)); - assert_eq!(setup.balance(&announcer1), nyms(700)); - assert_eq!( - setup.query_all(), - PagedServicesListResponse { - services: vec![ - service_info(1, nym_address1.clone(), announcer1.clone()), - service_info(2, nym_address1.clone(), announcer1.clone()), - service_info(3, nym_address2.clone(), announcer1.clone()), - service_info(4, nym_address1.clone(), announcer2.clone()), - service_info(5, nym_address2.clone(), announcer2.clone()), - ], - per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, - start_next_after: Some(5), - } - ); - - // Even though multiple of them point to the same nym address, we only delete the ones we actually - // own. - setup.delete_nym_address(nym_address1.clone(), announcer1.clone()); - - assert_eq!(setup.contract_balance(), nyms(300)); - assert_eq!(setup.balance(&announcer1), nyms(900)); - assert_eq!( - setup.query_all(), - PagedServicesListResponse { - services: vec![ - service_info(3, nym_address2.clone(), announcer1), - service_info(4, nym_address1, announcer2.clone()), - service_info(5, nym_address2, announcer2), - ], - per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, - start_next_after: Some(5), - } - ); -} - -// add multiple services, then query all but with a paging limit less than the number of services -// added -#[test] -fn paging_works() { - let mut setup = TestSetup::new(); - let announcer1 = Addr::unchecked("wealthy_announcer_1"); - let announcer2 = Addr::unchecked("wealthy_announcer_2"); - let nym_address1 = NymAddress::new("nymAddress1"); - let nym_address2 = NymAddress::new("nymAddress2"); - - // We announce the same address three times, but with different announcers - setup.announce_net_req(nym_address1.clone(), announcer1.clone()); - setup.announce_net_req(nym_address1.clone(), announcer1.clone()); - setup.announce_net_req(nym_address2.clone(), announcer1.clone()); - setup.announce_net_req(nym_address1.clone(), announcer2.clone()); - setup.announce_net_req(nym_address2.clone(), announcer2.clone()); - - assert_eq!( - setup.query_all_with_limit(Some(10), None), - PagedServicesListResponse { - services: vec![ - service_info(1, nym_address1.clone(), announcer1.clone()), - service_info(2, nym_address1.clone(), announcer1.clone()), - service_info(3, nym_address2.clone(), announcer1.clone()), - service_info(4, nym_address1.clone(), announcer2.clone()), - service_info(5, nym_address2.clone(), announcer2.clone()), - ], - per_page: 10, - start_next_after: Some(5), - } - ); - - assert_eq!( - setup.query_all_with_limit(Some(3), None), - PagedServicesListResponse { - services: vec![ - service_info(1, nym_address1.clone(), announcer1.clone()), - service_info(2, nym_address1.clone(), announcer1.clone()), - service_info(3, nym_address2.clone(), announcer1), - ], - per_page: 3, - start_next_after: Some(3), - } - ); - assert_eq!( - setup.query_all_with_limit(Some(3), Some(3)), - PagedServicesListResponse { - services: vec![ - service_info(4, nym_address1, announcer2.clone()), - service_info(5, nym_address2, announcer2), - ], - per_page: 3, - start_next_after: Some(5), - } - ); -} - -#[test] -fn service_id_increases_for_new_services() { - let mut setup = TestSetup::new(); - setup.announce_net_req( - NymAddress::new("nymAddress1"), - Addr::unchecked("announcer1"), - ); - setup.announce_net_req( - NymAddress::new("nymAddress2"), - Addr::unchecked("announcer2"), - ); - - assert_eq!( - setup - .query_all() - .services - .iter() - .map(|s| s.service_id) - .collect::>(), - vec![1, 2], - ); -} - -#[test] -fn service_id_is_not_resused_when_deleting_and_then_adding_a_new_service() { - let mut setup = TestSetup::new(); - setup.announce_net_req( - NymAddress::new("nymAddress1"), - Addr::unchecked("announcer1"), - ); - setup.announce_net_req( - NymAddress::new("nymAddress2"), - Addr::unchecked("announcer2"), - ); - setup.announce_net_req( - NymAddress::new("nymAddress3"), - Addr::unchecked("announcer3"), - ); - - setup.delete(1, Addr::unchecked("announcer1")); - setup.delete(3, Addr::unchecked("announcer3")); - - assert_eq!( - setup.query_all().services, - vec![service_info( - 2, - NymAddress::new("nymAddress2"), - Addr::unchecked("announcer2") - )] - ); - - setup.announce_net_req( - NymAddress::new("nymAddress4"), - Addr::unchecked("announcer4"), - ); - - assert_eq!( - setup.query_all().services, - vec![ - service_info( - 2, - NymAddress::new("nymAddress2"), - Addr::unchecked("announcer2") - ), - service_info( - 4, - NymAddress::new("nymAddress4"), - Addr::unchecked("announcer4") - ) - ] - ); -} diff --git a/contracts/service-provider-directory/src/integration_tests/announce.rs b/contracts/service-provider-directory/src/integration_tests/announce.rs new file mode 100644 index 0000000000..db937f0186 --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/announce.rs @@ -0,0 +1,183 @@ +use cosmwasm_std::Addr; +use nym_service_provider_directory_common::{ + response::PagedServicesListResponse, NymAddress, Service, ServiceDetails, ServiceType, +}; +use rstest::rstest; + +use crate::{ + constants::SERVICE_DEFAULT_RETRIEVAL_LIMIT, + test_helpers::{fixture::new_service, helpers::nyms}, + SpContractError, +}; + +use super::test_setup::TestSetup; + +#[rstest::fixture] +fn setup() -> TestSetup { + TestSetup::new() +} + +#[rstest] +fn basic_announce(mut setup: TestSetup) { + assert_eq!( + setup.query_all(), + PagedServicesListResponse { + services: vec![], + per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: None, + } + ); + + // Announce a first service + let announcer = Addr::unchecked("announcer"); + let nym_address = NymAddress::new("nymAddress"); + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance(&announcer), nyms(250)); + assert_eq!(setup.query_signing_nonce(announcer.to_string()), 0); + + let service = setup.new_service(&nym_address); + let payload = setup.payload_to_sign(&announcer, &nyms(100), &service.service); + let service = service.sign(payload); + setup.announce_net_req(&service, &announcer); + + // Deposit is deposited to contract and deducted from announcers's balance + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(setup.balance(&announcer), nyms(150)); + + // The signing nonce has been incremented + assert_eq!(setup.query_signing_nonce(announcer.to_string()), 1); + + // We can query the full service list + assert_eq!( + setup.query_all(), + PagedServicesListResponse { + services: vec![Service { + service_id: 1, + service: ServiceDetails { + nym_address: nym_address.clone(), + service_type: ServiceType::NetworkRequester, + identity_key: service.identity_key().to_string(), + }, + announcer: announcer.clone(), + block_height: 12345, + deposit: nyms(100), + }], + per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(1), + } + ); + + // ... and we can query by id + assert_eq!( + setup.query_id(1), + Service { + service_id: 1, + service: service.details().clone(), + announcer: announcer.clone(), + block_height: 12345, + deposit: nyms(100), + } + ); + + // Announce a second service + let announcer2 = Addr::unchecked("announcer2"); + let nym_address2 = NymAddress::new("nymAddress2"); + let service2 = setup.new_signed_service(&nym_address2, &announcer2, &nyms(100)); + setup.announce_net_req(&service2, &announcer2); + assert_eq!(setup.query_signing_nonce(announcer2.to_string()), 1); + + assert_eq!(setup.contract_balance(), nyms(200)); + assert_eq!( + setup.query_all(), + PagedServicesListResponse { + services: vec![ + new_service(1, &nym_address, &announcer, service.identity_key()), + new_service(2, &nym_address2, &announcer2, service2.identity_key()) + ], + per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(2), + } + ); +} + +#[rstest] +fn announce_fails_when_announcer_mismatch(mut setup: TestSetup) { + let announcer = Addr::unchecked("steve"); + let nym_address = NymAddress::new("foobar"); + let service = setup.new_signed_service(&nym_address, &announcer, &nyms(100)); + + // A difference announcer tries to announce the service + let announcer2 = Addr::unchecked("timmy"); + + let resp: SpContractError = setup + .try_announce_net_req(&service, &announcer2) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(resp, SpContractError::InvalidEd25519Signature); +} + +#[rstest] +fn signing_nonce_is_increased_when_announcing(mut setup: TestSetup) { + let announcer1 = Addr::unchecked("announcer1"); + let announcer2 = Addr::unchecked("announcer2"); + + assert_eq!(setup.query_signing_nonce(announcer1.to_string()), 0); + assert_eq!(setup.query_signing_nonce(announcer2.to_string()), 0); + + setup.sign_and_announce_net_req(&NymAddress::new("nymAddress1"), &announcer1, &nyms(100)); + + assert_eq!(setup.query_signing_nonce(announcer1.to_string()), 1); + assert_eq!(setup.query_signing_nonce(announcer2.to_string()), 0); + + setup.sign_and_announce_net_req(&NymAddress::new("nymAddress2"), &announcer2, &nyms(100)); + + assert_eq!(setup.query_signing_nonce(announcer1.to_string()), 1); + assert_eq!(setup.query_signing_nonce(announcer2.to_string()), 1); + + setup.sign_and_announce_net_req(&NymAddress::new("nymAddress3"), &announcer2, &nyms(100)); + + assert_eq!(setup.query_signing_nonce(announcer1.to_string()), 1); + assert_eq!(setup.query_signing_nonce(announcer2.to_string()), 2); +} + +#[rstest] +fn creating_two_services_in_a_row_without_announcing_fails(mut setup: TestSetup) { + let announcer = Addr::unchecked("wealthy_announcer_1"); + let nym_address1 = NymAddress::new("nymAddress1"); + let nym_address2 = NymAddress::new("nymAddress2"); + let deposit = nyms(100); + + let s1 = setup.new_signed_service(&nym_address1, &announcer, &deposit); + + // This second service will be signed with the same nonce + let s2 = setup.new_signed_service(&nym_address2, &announcer, &deposit); + + // Announce the first service works, and this increments the nonce + setup.announce_net_req(&s1, &announcer); + + // Now the nonce has been incremented, and the signature will not match + let resp: SpContractError = setup + .try_announce_net_req(&s2, &announcer) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(resp, SpContractError::InvalidEd25519Signature,); +} + +#[rstest] +fn announcing_the_same_service_twice_fails(mut setup: TestSetup) { + let announcer = Addr::unchecked("wealthy_announcer_1"); + let nym_address = NymAddress::new("nymAddress1"); + + let s1 = setup.new_signed_service(&nym_address, &announcer, &nyms(100)); + setup.announce_net_req(&s1, &announcer); + + // Now the nonce has been incremented, and the signature will not match + let resp: SpContractError = setup + .try_announce_net_req(&s1, &announcer) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(resp, SpContractError::InvalidEd25519Signature); +} diff --git a/contracts/service-provider-directory/src/integration_tests/delete.rs b/contracts/service-provider-directory/src/integration_tests/delete.rs new file mode 100644 index 0000000000..3fb7275763 --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/delete.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::Addr; +use nym_service_provider_directory_common::{response::PagedServicesListResponse, NymAddress}; + +use crate::{ + constants::SERVICE_DEFAULT_RETRIEVAL_LIMIT, + test_helpers::{fixture::new_service, helpers::nyms}, + SpContractError, +}; + +use super::test_setup::TestSetup; + +#[test] +fn delete_service() { + let mut setup = TestSetup::new(); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress"), + &Addr::unchecked("announcer"), + &nyms(100), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(setup.balance("announcer"), nyms(150)); + assert!(!setup.query_all().services.is_empty()); + setup.delete(1, &Addr::unchecked("announcer")); + + // Deleting the service returns the deposit to the announcer + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance("announcer"), nyms(250)); + assert!(setup.query_all().services.is_empty()); +} + +#[test] +fn only_announcer_can_delete_service() { + let mut setup = TestSetup::new(); + assert_eq!(setup.contract_balance(), nyms(0)); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress"), + &Addr::unchecked("announcer"), + &nyms(100), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert!(!setup.query_all().services.is_empty()); + + let delete_resp: SpContractError = setup + .try_delete(1, &Addr::unchecked("not_announcer")) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!( + delete_resp, + SpContractError::Unauthorized { + sender: Addr::unchecked("not_announcer") + } + ); +} + +#[test] +fn cant_delete_service_that_does_not_exist() { + let mut setup = TestSetup::new(); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress"), + &Addr::unchecked("announcer"), + &nyms(100), + ); + assert_eq!(setup.contract_balance(), nyms(100)); + assert!(!setup.query_all().services.is_empty()); + + let delete_resp: SpContractError = setup + .try_delete(0, &Addr::unchecked("announcer")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(delete_resp, SpContractError::NotFound { service_id: 0 }); + + let delete_resp: SpContractError = setup + .try_delete(2, &Addr::unchecked("announcer")) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(setup.contract_balance(), nyms(100)); + assert_eq!(delete_resp, SpContractError::NotFound { service_id: 2 }); + + assert!(!setup.query_all().services.is_empty()); + setup.delete(1, &Addr::unchecked("announcer")); + assert_eq!(setup.contract_balance(), nyms(0)); + assert!(setup.query_all().services.is_empty()); +} + +#[test] +fn announce_multiple_services_and_deleting_by_name() { + let mut setup = TestSetup::new(); + let announcer1 = Addr::unchecked("wealthy_announcer_1"); + let announcer2 = Addr::unchecked("wealthy_announcer_2"); + let nym_address1 = NymAddress::new("nymAddress1"); + let nym_address2 = NymAddress::new("nymAddress2"); + let deposit = nyms(100); + + // We announce the same address three times, but with different annoucers + assert_eq!(setup.contract_balance(), nyms(0)); + assert_eq!(setup.balance(&announcer1), nyms(1000)); + let s1 = setup.sign_and_announce_net_req(&nym_address1, &announcer1, &deposit); + let s2 = setup.sign_and_announce_net_req(&nym_address1, &announcer1, &deposit); + let s3 = setup.sign_and_announce_net_req(&nym_address2, &announcer1, &deposit); + let s4 = setup.sign_and_announce_net_req(&nym_address1, &announcer2, &deposit); + let s5 = setup.sign_and_announce_net_req(&nym_address2, &announcer2, &deposit); + + assert_eq!(setup.contract_balance(), nyms(500)); + assert_eq!(setup.balance(&announcer1), nyms(700)); + assert_eq!( + setup.query_all(), + PagedServicesListResponse { + services: vec![ + new_service(1, &nym_address1, &announcer1, s1.identity_key()), + new_service(2, &nym_address1, &announcer1, s2.identity_key()), + new_service(3, &nym_address2, &announcer1, s3.identity_key()), + new_service(4, &nym_address1, &announcer2, s4.identity_key()), + new_service(5, &nym_address2, &announcer2, s5.identity_key()), + ], + per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(5), + } + ); + + // Even though multiple of them point to the same nym address, we only delete the ones we actually + // own. + setup.delete_nym_address(&nym_address1, &announcer1); + + assert_eq!(setup.contract_balance(), nyms(300)); + assert_eq!(setup.balance(&announcer1), nyms(900)); + assert_eq!( + setup.query_all(), + PagedServicesListResponse { + services: vec![ + new_service(3, &nym_address2, &announcer1, s3.identity_key()), + new_service(4, &nym_address1, &announcer2, s4.identity_key()), + new_service(5, &nym_address2, &announcer2, s5.identity_key()), + ], + per_page: SERVICE_DEFAULT_RETRIEVAL_LIMIT as usize, + start_next_after: Some(5), + } + ); +} diff --git a/contracts/service-provider-directory/src/integration_tests/mod.rs b/contracts/service-provider-directory/src/integration_tests/mod.rs new file mode 100644 index 0000000000..d802029012 --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/mod.rs @@ -0,0 +1,13 @@ +//! Integration tests using cw-multi-test. + +mod announce; +mod delete; +mod query; +mod service_id; +mod test_service; +mod test_setup; + +#[test] +fn instantiate_contract() { + test_setup::TestSetup::new(); +} diff --git a/contracts/service-provider-directory/src/integration_tests/query.rs b/contracts/service-provider-directory/src/integration_tests/query.rs new file mode 100644 index 0000000000..c903f2394e --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/query.rs @@ -0,0 +1,77 @@ +use cosmwasm_std::Addr; +use nym_service_provider_directory_common::{ + response::{ConfigResponse, PagedServicesListResponse}, + NymAddress, +}; + +use crate::test_helpers::{fixture::new_service, helpers::nyms}; + +use super::test_setup::TestSetup; + +#[test] +fn query_config() { + assert_eq!( + TestSetup::new().query_config(), + ConfigResponse { + deposit_required: nyms(100), + } + ); +} + +// add multiple services, then query all but with a paging limit less than the number of services +// added +#[test] +fn paging_works() { + let mut setup = TestSetup::new(); + let announcer1 = Addr::unchecked("wealthy_announcer_1"); + let announcer2 = Addr::unchecked("wealthy_announcer_2"); + let nym_address1 = NymAddress::new("nymAddress1"); + let nym_address2 = NymAddress::new("nymAddress2"); + let deposit = nyms(100); + + // We announce the same address three times, but with different announcers + let s1 = setup.sign_and_announce_net_req(&nym_address1, &announcer1, &deposit); + let s2 = setup.sign_and_announce_net_req(&nym_address1, &announcer1, &deposit); + let s3 = setup.sign_and_announce_net_req(&nym_address2, &announcer1, &deposit); + let s4 = setup.sign_and_announce_net_req(&nym_address1, &announcer2, &deposit); + let s5 = setup.sign_and_announce_net_req(&nym_address2, &announcer2, &deposit); + + assert_eq!( + setup.query_all_with_limit(Some(10), None), + PagedServicesListResponse { + services: vec![ + new_service(1, &nym_address1, &announcer1, s1.identity_key()), + new_service(2, &nym_address1, &announcer1, s2.identity_key()), + new_service(3, &nym_address2, &announcer1, s3.identity_key()), + new_service(4, &nym_address1, &announcer2, s4.identity_key()), + new_service(5, &nym_address2, &announcer2, s5.identity_key()), + ], + per_page: 10, + start_next_after: Some(5), + } + ); + + assert_eq!( + setup.query_all_with_limit(Some(3), None), + PagedServicesListResponse { + services: vec![ + new_service(1, &nym_address1, &announcer1, s1.identity_key()), + new_service(2, &nym_address1, &announcer1, s2.identity_key()), + new_service(3, &nym_address2, &announcer1, s3.identity_key()), + ], + per_page: 3, + start_next_after: Some(3), + } + ); + assert_eq!( + setup.query_all_with_limit(Some(3), Some(3)), + PagedServicesListResponse { + services: vec![ + new_service(4, &nym_address1, &announcer2, s4.identity_key()), + new_service(5, &nym_address2, &announcer2, s5.identity_key()), + ], + per_page: 3, + start_next_after: Some(5), + } + ); +} diff --git a/contracts/service-provider-directory/src/integration_tests/service_id.rs b/contracts/service-provider-directory/src/integration_tests/service_id.rs new file mode 100644 index 0000000000..7c5a4324c9 --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/service_id.rs @@ -0,0 +1,89 @@ +use cosmwasm_std::Addr; +use nym_service_provider_directory_common::NymAddress; + +use crate::test_helpers::{fixture::new_service, helpers::nyms}; + +use super::test_setup::TestSetup; + +#[test] +fn service_id_increases_for_new_services() { + let mut setup = TestSetup::new(); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress1"), + &Addr::unchecked("announcer1"), + &nyms(100), + ); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress2"), + &Addr::unchecked("announcer2"), + &nyms(100), + ); + + assert_eq!( + setup + .query_all() + .services + .iter() + .map(|s| s.service_id) + .collect::>(), + vec![1, 2], + ); +} + +#[test] +fn service_id_is_not_resused_when_deleting_and_then_adding_a_new_service() { + let mut setup = TestSetup::new(); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress1"), + &Addr::unchecked("announcer1"), + &nyms(100), + ); + let s2 = setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress2"), + &Addr::unchecked("announcer2"), + &nyms(100), + ); + setup.sign_and_announce_net_req( + &NymAddress::new("nymAddress3"), + &Addr::unchecked("announcer3"), + &nyms(100), + ); + + setup.delete(1, &Addr::unchecked("announcer1")); + setup.delete(3, &Addr::unchecked("announcer3")); + + assert_eq!( + setup.query_all().services, + vec![new_service( + 2, + &NymAddress::new("nymAddress2"), + &Addr::unchecked("announcer2"), + s2.identity_key(), + )] + ); + + let s4 = setup.new_signed_service( + &NymAddress::new("nymAddress4"), + &Addr::unchecked("announcer4"), + &nyms(100), + ); + setup.announce_net_req(&s4, &Addr::unchecked("announcer4")); + + assert_eq!( + setup.query_all().services, + vec![ + new_service( + 2, + &NymAddress::new("nymAddress2"), + &Addr::unchecked("announcer2"), + s2.identity_key(), + ), + new_service( + 4, + &NymAddress::new("nymAddress4"), + &Addr::unchecked("announcer4"), + s4.identity_key(), + ) + ] + ); +} diff --git a/contracts/service-provider-directory/src/integration_tests/test_service.rs b/contracts/service-provider-directory/src/integration_tests/test_service.rs new file mode 100644 index 0000000000..d0652dee75 --- /dev/null +++ b/contracts/service-provider-directory/src/integration_tests/test_service.rs @@ -0,0 +1,75 @@ +use nym_contracts_common::{signing::MessageSignature, IdentityKey}; +use nym_crypto::asymmetric::identity; +use nym_service_provider_directory_common::{ + signing_types::SignableServiceProviderAnnounceMsg, NymAddress, ServiceDetails, ServiceType, +}; +use rand_chacha::ChaCha20Rng; + +use crate::test_helpers::signing::ed25519_sign_message; + +pub struct TestService { + pub service: ServiceDetails, + pub keys: identity::KeyPair, + pub rng: ChaCha20Rng, +} + +impl TestService { + pub fn new(rng: &mut ChaCha20Rng, nym_address: NymAddress) -> Self { + let keys = identity::KeyPair::new(rng); + let service = ServiceDetails { + nym_address, + service_type: ServiceType::NetworkRequester, + identity_key: keys.public_key().to_base58_string(), + }; + Self { + service, + keys, + rng: rng.clone(), + } + } + + pub fn identity_key(&self) -> &IdentityKey { + &self.service.identity_key + } + + pub fn details(&self) -> &ServiceDetails { + &self.service + } + + pub fn sign(self, payload: SignableServiceProviderAnnounceMsg) -> SignedTestService { + let owner_signature = ed25519_sign_message(payload, self.keys.private_key()); + SignedTestService { + service: self.service, + keys: self.keys, + owner_signature, + } + } +} + +impl From for ServiceDetails { + fn from(test_service: TestService) -> Self { + test_service.service + } +} + +pub struct SignedTestService { + pub service: ServiceDetails, + pub keys: identity::KeyPair, + pub owner_signature: MessageSignature, +} + +impl SignedTestService { + pub fn identity_key(&self) -> &IdentityKey { + &self.service.identity_key + } + + pub fn details(&self) -> &ServiceDetails { + &self.service + } +} + +impl From for ServiceDetails { + fn from(signed_service: SignedTestService) -> Self { + signed_service.service + } +} diff --git a/contracts/service-provider-directory/src/test_helpers/test_setup.rs b/contracts/service-provider-directory/src/integration_tests/test_setup.rs similarity index 51% rename from contracts/service-provider-directory/src/test_helpers/test_setup.rs rename to contracts/service-provider-directory/src/integration_tests/test_setup.rs index 7aa31fbad3..3438691b8e 100644 --- a/contracts/service-provider-directory/src/test_helpers/test_setup.rs +++ b/contracts/service-provider-directory/src/integration_tests/test_setup.rs @@ -1,14 +1,21 @@ use anyhow::Result; use cosmwasm_std::{coins, Addr, Coin, Uint128}; use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; +use nym_contracts_common::signing::Nonce; use nym_service_provider_directory_common::{ msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, response::{ConfigResponse, PagedServicesListResponse}, - NymAddress, ServiceId, ServiceInfo, ServiceType, + signing_types::{ + construct_service_provider_announce_sign_payload, SignableServiceProviderAnnounceMsg, + }, + NymAddress, Service, ServiceDetails, ServiceId, }; +use rand_chacha::ChaCha20Rng; use serde::de::DeserializeOwned; -use crate::test_helpers::helpers::get_app_attribute; +use crate::test_helpers::helpers::{get_app_attribute, test_rng}; + +use super::test_service::{SignedTestService, TestService}; const DENOM: &str = "unym"; const ADDRESSES: &[&str] = &[ @@ -19,6 +26,8 @@ const ADDRESSES: &[&str] = &[ "announcer2", "announcer3", "announcer4", + "steve", + "timmy", ]; const WEALTHY_ADDRESSES: &[&str] = &["wealthy_announcer_1", "wealthy_announcer_2"]; @@ -26,6 +35,7 @@ const WEALTHY_ADDRESSES: &[&str] = &["wealthy_announcer_1", "wealthy_announcer_2 pub struct TestSetup { app: App, addr: Addr, + rng: ChaCha20Rng, } impl Default for TestSetup { @@ -51,7 +61,8 @@ impl TestSetup { let code = ContractWrapper::new(crate::execute, crate::instantiate, crate::query); let code_id = app.store_code(Box::new(code)); let addr = Self::instantiate(&mut app, code_id); - TestSetup { app, addr } + let rng = test_rng(); + TestSetup { app, addr, rng } } fn instantiate(app: &mut App, code_id: u64) -> Addr { @@ -68,11 +79,6 @@ impl TestSetup { .unwrap() } - #[allow(unused)] - pub fn address(&self) -> &Addr { - &self.addr - } - pub fn contract_balance(&self) -> Coin { self.app.wrap().query_balance(&self.addr, DENOM).unwrap() } @@ -88,7 +94,7 @@ impl TestSetup { self.query(&QueryMsg::Config {}) } - pub fn query_id(&self, service_id: ServiceId) -> ServiceInfo { + pub fn query_id(&self, service_id: ServiceId) -> Service { self.query(&QueryMsg::ServiceId { service_id }) } @@ -104,22 +110,69 @@ impl TestSetup { self.query(&QueryMsg::All { limit, start_after }) } - pub fn announce_net_req(&mut self, address: NymAddress, announcer: Addr) -> AppResponse { - let resp = self - .app - .execute_contract( - announcer, - self.addr.clone(), - &ExecuteMsg::Announce { - nym_address: address, - service_type: ServiceType::NetworkRequester, - }, - &[Coin { - denom: DENOM.to_string(), - amount: Uint128::new(100), - }], - ) - .unwrap(); + pub fn query_signing_nonce(&self, address: String) -> Nonce { + self.query(&QueryMsg::SigningNonce { address }) + } + + // Create a new service, together with its signing keypair + pub fn new_service(&mut self, nym_address: &NymAddress) -> TestService { + TestService::new(&mut self.rng, nym_address.clone()) + } + + // Create payload for the service operator to sign + pub fn payload_to_sign( + &mut self, + announcer: &Addr, + deposit: &Coin, + service: &ServiceDetails, + ) -> SignableServiceProviderAnnounceMsg { + let nonce = self.query_signing_nonce(announcer.to_string()); + construct_service_provider_announce_sign_payload( + nonce, + announcer.clone(), + deposit.clone(), + service.clone(), + ) + } + + // Convenience function for creating a new service and signing it. + pub fn new_signed_service( + &mut self, + nym_address: &NymAddress, + announcer: &Addr, + deposit: &Coin, + ) -> SignedTestService { + let service = self.new_service(nym_address); + let payload = self.payload_to_sign(announcer, deposit, service.details()); + service.sign(payload) + } + + // Announce a new service + pub fn try_announce_net_req( + &mut self, + service: &SignedTestService, + announcer: &Addr, + ) -> Result { + self.app.execute_contract( + announcer.clone(), + self.addr.clone(), + &ExecuteMsg::Announce { + service: service.service.clone(), + owner_signature: service.owner_signature.clone(), + }, + &[Coin { + denom: DENOM.to_string(), + amount: Uint128::new(100), + }], + ) + } + + pub fn announce_net_req( + &mut self, + service: &SignedTestService, + announcer: &Addr, + ) -> AppResponse { + let resp = self.try_announce_net_req(service, announcer).unwrap(); assert_eq!( get_app_attribute(&resp, "wasm-announce", "action"), "announce" @@ -127,16 +180,28 @@ impl TestSetup { resp } - pub fn try_delete(&mut self, service_id: ServiceId, announcer: Addr) -> Result { + // Convenience function for create a new signed service and announcing it + pub fn sign_and_announce_net_req( + &mut self, + nym_address: &NymAddress, + announcer: &Addr, + deposit: &Coin, + ) -> SignedTestService { + let service = self.new_signed_service(nym_address, announcer, deposit); + let _ = self.announce_net_req(&service, announcer); + service + } + + pub fn try_delete(&mut self, service_id: ServiceId, announcer: &Addr) -> Result { self.app.execute_contract( - announcer, + announcer.clone(), self.addr.clone(), &ExecuteMsg::DeleteId { service_id }, &[], ) } - pub fn delete(&mut self, service_id: ServiceId, announcer: Addr) -> AppResponse { + pub fn delete(&mut self, service_id: ServiceId, announcer: &Addr) -> AppResponse { let delete_resp = self.try_delete(service_id, announcer).unwrap(); assert_eq!( get_app_attribute(&delete_resp, "wasm-delete_id", "action"), @@ -145,12 +210,18 @@ impl TestSetup { delete_resp } - pub fn delete_nym_address(&mut self, nym_address: NymAddress, announcer: Addr) -> AppResponse { + pub fn delete_nym_address( + &mut self, + nym_address: &NymAddress, + announcer: &Addr, + ) -> AppResponse { self.app .execute_contract( - announcer, + announcer.clone(), self.addr.clone(), - &ExecuteMsg::DeleteNymAddress { nym_address }, + &ExecuteMsg::DeleteNymAddress { + nym_address: nym_address.clone(), + }, &[], ) .unwrap() diff --git a/contracts/service-provider-directory/src/lib.rs b/contracts/service-provider-directory/src/lib.rs index b93f00ccfc..58ba4f9aa8 100644 --- a/contracts/service-provider-directory/src/lib.rs +++ b/contracts/service-provider-directory/src/lib.rs @@ -4,7 +4,8 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] -use crate::error::Result; +pub use nym_service_provider_directory_common::error::{Result, SpContractError}; + use nym_service_provider_directory_common::msg::{ ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, }; @@ -12,14 +13,11 @@ use nym_service_provider_directory_common::msg::{ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; -use error::ContractError; +mod constants; mod contract; -mod error; mod state; -pub mod constants; - #[cfg(test)] mod integration_tests; #[cfg(test)] @@ -38,7 +36,7 @@ pub fn instantiate( /// Contract entry point for migrations. #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { contract::migrate(deps, env, msg) } @@ -49,7 +47,7 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result { contract::execute(deps, env, info, msg) } diff --git a/contracts/service-provider-directory/src/state/admin.rs b/contracts/service-provider-directory/src/state/admin.rs index e0ec915951..026e353626 100644 --- a/contracts/service-provider-directory/src/state/admin.rs +++ b/contracts/service-provider-directory/src/state/admin.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Deps, DepsMut}; use cw_controllers::Admin; -use crate::{constants::ADMIN_KEY, error::Result}; +use crate::{constants::ADMIN_KEY, Result}; const ADMIN: Admin = Admin::new(ADMIN_KEY); diff --git a/contracts/service-provider-directory/src/state/config.rs b/contracts/service-provider-directory/src/state/config.rs index b54c78f872..093edbffc2 100644 --- a/contracts/service-provider-directory/src/state/config.rs +++ b/contracts/service-provider-directory/src/state/config.rs @@ -3,7 +3,7 @@ use cw_storage_plus::Item; use nym_service_provider_directory_common::response::ConfigResponse; use serde::{Deserialize, Serialize}; -use crate::{constants::CONFIG_KEY, error::Result}; +use crate::{constants::CONFIG_KEY, Result}; const CONFIG: Item = Item::new(CONFIG_KEY); diff --git a/contracts/service-provider-directory/src/state/mod.rs b/contracts/service-provider-directory/src/state/mod.rs index 7c3a34403d..a36fbb1248 100644 --- a/contracts/service-provider-directory/src/state/mod.rs +++ b/contracts/service-provider-directory/src/state/mod.rs @@ -1,8 +1,13 @@ -pub mod admin; -pub mod config; -pub mod service_id_counter; -pub mod services; +mod admin; +mod config; +mod nonce; +mod service_id_counter; +mod services; pub(crate) use admin::{assert_admin, set_admin}; pub(crate) use config::{deposit_required, load_config, save_config, Config}; +pub(crate) use nonce::{get_signing_nonce, increment_signing_nonce}; pub(crate) use service_id_counter::next_service_id_counter; +pub(crate) use services::{ + has_service, load_all_paged, load_announcer, load_id, load_nym_address, remove, save, PagedLoad, +}; diff --git a/contracts/service-provider-directory/src/state/nonce.rs b/contracts/service-provider-directory/src/state/nonce.rs new file mode 100644 index 0000000000..bf865af82a --- /dev/null +++ b/contracts/service-provider-directory/src/state/nonce.rs @@ -0,0 +1,71 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{constants::SIGNING_NONCES_NAMESPACE, Result}; + +use cosmwasm_std::{Addr, Storage}; +use cw_storage_plus::Map; +use nym_contracts_common::signing::Nonce; + +pub const NONCES: Map<'_, Addr, Nonce> = Map::new(SIGNING_NONCES_NAMESPACE); + +pub fn get_signing_nonce(storage: &dyn Storage, address: Addr) -> Result { + let nonce = NONCES.may_load(storage, address)?.unwrap_or(0); + Ok(nonce) +} + +fn update_signing_nonce(storage: &mut dyn Storage, address: Addr, value: Nonce) -> Result<()> { + NONCES + .save(storage, address, &value) + .map_err(|err| err.into()) +} + +pub fn increment_signing_nonce(storage: &mut dyn Storage, address: Addr) -> Result<()> { + // get the current nonce + let nonce = get_signing_nonce(storage, address.clone())?; + + // increment it for the next use + update_signing_nonce(storage, address, nonce + 1) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helpers::transactions::instantiate_test_contract; + use cosmwasm_std::{ + testing::{MockApi, MockQuerier}, + MemoryStorage, OwnedDeps, + }; + use rstest::rstest; + + type TestDeps = OwnedDeps; + + #[rstest::fixture] + fn deps() -> TestDeps { + instantiate_test_contract() + } + + fn addr(s: &str) -> Addr { + Addr::unchecked(s) + } + + #[rstest] + fn getting_signing_nonce_doesnt_increment_it(deps: TestDeps) { + assert_eq!(get_signing_nonce(&deps.storage, addr("gunnar")).unwrap(), 0); + assert_eq!(get_signing_nonce(&deps.storage, addr("gunnar")).unwrap(), 0); + } + + #[rstest] + fn increment_works(mut deps: TestDeps) { + assert_eq!(get_signing_nonce(&deps.storage, addr("gunnar")).unwrap(), 0); + increment_signing_nonce(&mut deps.storage, addr("gunnar")).unwrap(); + assert_eq!(get_signing_nonce(&deps.storage, addr("gunnar")).unwrap(), 1); + } + + #[rstest] + fn incrementing_is_independent(mut deps: TestDeps) { + increment_signing_nonce(&mut deps.storage, addr("gunnar")).unwrap(); + assert_eq!(get_signing_nonce(&deps.storage, addr("gunnar")).unwrap(), 1); + assert_eq!(get_signing_nonce(&deps.storage, addr("bjorn")).unwrap(), 0); + } +} diff --git a/contracts/service-provider-directory/src/state/service_id_counter.rs b/contracts/service-provider-directory/src/state/service_id_counter.rs index d8a805db51..3a1ae1f2eb 100644 --- a/contracts/service-provider-directory/src/state/service_id_counter.rs +++ b/contracts/service-provider-directory/src/state/service_id_counter.rs @@ -2,7 +2,7 @@ use cosmwasm_std::Storage; use cw_storage_plus::Item; use nym_service_provider_directory_common::ServiceId; -use crate::{constants::SERVICE_ID_COUNTER_KEY, error::Result}; +use crate::{constants::SERVICE_ID_COUNTER_KEY, Result}; const SERVICE_ID_COUNTER: Item = Item::new(SERVICE_ID_COUNTER_KEY); @@ -17,29 +17,50 @@ pub(crate) fn next_service_id_counter(store: &mut dyn Storage) -> Result { @@ -26,7 +26,7 @@ impl<'a> IndexList for ServiceIndex<'a> { fn services<'a>() -> IndexedMap<'a, ServiceId, Service, ServiceIndex<'a>> { let indexes = ServiceIndex { nym_address: MultiIndex::new( - |d| d.nym_address.to_string(), + |d| d.service.nym_address.to_string(), SERVICES_PK_NAMESPACE, SERVICES_NYM_ADDRESS_IDX_NAMESPACE, ), @@ -39,10 +39,10 @@ fn services<'a>() -> IndexedMap<'a, ServiceId, Service, ServiceIndex<'a>> { IndexedMap::new(SERVICES_PK_NAMESPACE, indexes) } -pub fn save(store: &mut dyn Storage, new_service: &Service) -> Result { - let service_id = super::next_service_id_counter(store)?; +pub fn save(store: &mut dyn Storage, new_service: &Service) -> Result<()> { + let service_id = new_service.service_id; services().save(store, service_id, new_service)?; - Ok(service_id) + Ok(()) } pub fn remove(store: &mut dyn Storage, service_id: ServiceId) -> Result<()> { @@ -55,39 +55,38 @@ pub fn has_service(store: &dyn Storage, service_id: ServiceId) -> bool { pub fn load_id(store: &dyn Storage, service_id: ServiceId) -> Result { services().load(store, service_id).map_err(|err| match err { - StdError::NotFound { .. } => ContractError::NotFound { service_id }, + StdError::NotFound { .. } => SpContractError::NotFound { service_id }, err => err.into(), }) } -pub fn load_announcer(store: &dyn Storage, announcer: Addr) -> Result> { +pub fn load_announcer(store: &dyn Storage, announcer: Addr) -> Result> { let services = services() .idx .announcer .prefix(announcer) .range(store, None, None, Order::Ascending) .take(MAX_NUMBER_OF_PROVIDERS_PER_ANNOUNCER as usize) + .map(|res| res.map(|item| item.1)) .collect::>>()?; Ok(services) } -pub fn load_nym_address( - store: &dyn Storage, - nym_address: NymAddress, -) -> Result> { +pub fn load_nym_address(store: &dyn Storage, nym_address: NymAddress) -> Result> { let services = services() .idx .nym_address .prefix(nym_address.to_string()) .range(store, None, None, Order::Ascending) .take(MAX_NUMBER_OF_ALIASES_FOR_NYM_ADDRESS as usize) + .map(|res| res.map(|item| item.1)) .collect::>>()?; Ok(services) } #[derive(Debug, PartialEq)] pub struct PagedLoad { - pub services: Vec<(ServiceId, Service)>, + pub services: Vec, pub limit: usize, pub start_next_after: Option, } @@ -106,9 +105,10 @@ pub fn load_all_paged( let services = services() .range(store, start, None, Order::Ascending) .take(limit) - .collect::>>()?; + .map(|res| res.map(|item| item.1)) + .collect::>>()?; - let start_next_after = services.last().map(|service| service.0); + let start_next_after = services.last().map(|service| service.service_id); Ok(PagedLoad { services, @@ -119,122 +119,128 @@ pub fn load_all_paged( #[cfg(test)] mod tests { + use cosmwasm_std::{ + testing::{MockApi, MockQuerier}, + MemoryStorage, OwnedDeps, + }; + use rstest::rstest; + use crate::{ - error::ContractError, test_helpers::{ fixture::{service_fixture, service_fixture_with_address}, - helpers::instantiate_test_contract, + transactions::instantiate_test_contract, }, + SpContractError, }; use super::*; - #[test] - fn save_works() { - let mut deps = instantiate_test_contract(); + type TestDeps = OwnedDeps; + + #[rstest::fixture] + fn deps() -> TestDeps { + instantiate_test_contract() + } + + #[rstest] + fn save_works(mut deps: TestDeps) { assert!(!has_service(&deps.storage, 1)); - save(deps.as_mut().storage, &service_fixture()).unwrap(); + save(deps.as_mut().storage, &service_fixture(1)).unwrap(); assert!(has_service(&deps.storage, 1)); } - #[test] - fn save_and_check_incorrect_id_fails() { - let mut deps = instantiate_test_contract(); + #[rstest] + fn save_and_check_incorrect_id_fails(mut deps: TestDeps) { assert!(!has_service(&deps.storage, 2)); - save(deps.as_mut().storage, &service_fixture()).unwrap(); + save(deps.as_mut().storage, &service_fixture(1)).unwrap(); assert!(!has_service(&deps.storage, 2)); } - #[test] - fn remove_works() { - let mut deps = instantiate_test_contract(); - let id = save(deps.as_mut().storage, &service_fixture()).unwrap(); + #[rstest] + fn remove_works(mut deps: TestDeps) { + let id = 1; + save(deps.as_mut().storage, &service_fixture(id)).unwrap(); assert!(has_service(&deps.storage, id)); remove(deps.as_mut().storage, id).unwrap(); assert!(!has_service(&deps.storage, id)); } - #[test] - fn load_by_id_works() { - let mut deps = instantiate_test_contract(); - let id = save(deps.as_mut().storage, &service_fixture()).unwrap(); + #[rstest] + fn load_by_id_works(mut deps: TestDeps) { + let id = 1; + save(deps.as_mut().storage, &service_fixture(id)).unwrap(); let service = load_id(deps.as_ref().storage, id).unwrap(); - assert_eq!(service, service_fixture()); + assert_eq!(service, service_fixture(id)); } - #[test] - fn load_by_wrong_id_returns_not_found() { - let mut deps = instantiate_test_contract(); - let id = save(deps.as_mut().storage, &service_fixture()).unwrap(); + #[rstest] + fn load_by_wrong_id_returns_not_found(mut deps: TestDeps) { + let id = 1; + save(deps.as_mut().storage, &service_fixture(id)).unwrap(); assert_eq!( load_id(deps.as_ref().storage, id + 1).unwrap_err(), - ContractError::NotFound { service_id: id + 1 } + SpContractError::NotFound { service_id: id + 1 } ); } - #[test] - fn load_by_announcer_works() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("c")).unwrap(); + #[rstest] + fn load_by_announcer_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(3, "c")).unwrap(); assert_eq!( load_announcer(&deps.storage, Addr::unchecked("steve")).unwrap(), vec![ - (1, service_fixture_with_address("a")), - (2, service_fixture_with_address("b")), - (3, service_fixture_with_address("c")), + service_fixture_with_address(1, "a"), + service_fixture_with_address(2, "b"), + service_fixture_with_address(3, "c"), ] ); } - #[test] - fn load_by_wrong_announcer_returns_empty() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("c")).unwrap(); + #[rstest] + fn load_by_wrong_announcer_returns_empty(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(3, "c")).unwrap(); assert_eq!( load_announcer(&deps.storage, Addr::unchecked("timmy")).unwrap(), vec![] ); } - #[test] - fn load_by_nym_address_works() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("c")).unwrap(); + #[rstest] + fn load_by_nym_address_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(3, "c")).unwrap(); assert_eq!( load_nym_address(&deps.storage, NymAddress::new("b")).unwrap(), - vec![(2, service_fixture_with_address("b"))] + vec![service_fixture_with_address(2, "b")] ); } - #[test] - fn load_by_wrong_nym_address_returns_empty() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("c")).unwrap(); + #[rstest] + fn load_by_wrong_nym_address_returns_empty(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(3, "c")).unwrap(); assert_eq!( load_nym_address(&deps.storage, NymAddress::new("d")).unwrap(), vec![] ); } - #[test] - fn load_all_paged_with_no_limit_works() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); + #[rstest] + fn load_all_paged_with_no_limit_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); assert_eq!( load_all_paged(&deps.storage, None, None).unwrap(), PagedLoad { services: vec![ - (1, service_fixture_with_address("a")), - (2, service_fixture_with_address("b")) + service_fixture_with_address(1, "a"), + service_fixture_with_address(2, "b") ], start_next_after: Some(2), limit: 100, @@ -242,20 +248,19 @@ mod tests { ); } - #[test] - fn load_all_paged_with_limit_works() { - let mut deps = instantiate_test_contract(); - save(deps.as_mut().storage, &service_fixture_with_address("a")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("b")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("c")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("d")).unwrap(); - save(deps.as_mut().storage, &service_fixture_with_address("e")).unwrap(); + #[rstest] + fn load_all_paged_with_limit_works(mut deps: TestDeps) { + save(deps.as_mut().storage, &service_fixture_with_address(1, "a")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(2, "b")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(3, "c")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(4, "d")).unwrap(); + save(deps.as_mut().storage, &service_fixture_with_address(5, "e")).unwrap(); assert_eq!( load_all_paged(&deps.storage, Some(2), None).unwrap(), PagedLoad { services: vec![ - (1, service_fixture_with_address("a")), - (2, service_fixture_with_address("b")) + service_fixture_with_address(1, "a"), + service_fixture_with_address(2, "b") ], limit: 2, start_next_after: Some(2), @@ -265,8 +270,8 @@ mod tests { load_all_paged(&deps.storage, Some(2), Some(2)).unwrap(), PagedLoad { services: vec![ - (3, service_fixture_with_address("c")), - (4, service_fixture_with_address("d")) + service_fixture_with_address(3, "c"), + service_fixture_with_address(4, "d") ], limit: 2, start_next_after: Some(4), @@ -275,7 +280,7 @@ mod tests { assert_eq!( load_all_paged(&deps.storage, Some(2), Some(4)).unwrap(), PagedLoad { - services: vec![(5, service_fixture_with_address("e")),], + services: vec![service_fixture_with_address(5, "e")], start_next_after: Some(5), limit: 2, } diff --git a/contracts/service-provider-directory/src/test_helpers/assert.rs b/contracts/service-provider-directory/src/test_helpers/assert.rs index 44b8bfaa2f..392babc34d 100644 --- a/contracts/service-provider-directory/src/test_helpers/assert.rs +++ b/contracts/service-provider-directory/src/test_helpers/assert.rs @@ -1,11 +1,12 @@ use cosmwasm_std::{from_binary, testing::mock_env, Addr, Coin, Deps}; +use nym_contracts_common::signing::Nonce; use nym_service_provider_directory_common::{ msg::QueryMsg, response::{ConfigResponse, PagedServicesListResponse}, - ServiceId, ServiceInfo, + Service, ServiceId, }; -use crate::{constants::SERVICE_DEFAULT_RETRIEVAL_LIMIT, error::ContractError}; +use crate::{constants::SERVICE_DEFAULT_RETRIEVAL_LIMIT, SpContractError}; pub fn assert_config(deps: Deps, admin: &Addr, deposit_required: Coin) { crate::state::assert_admin(deps, admin).unwrap(); @@ -14,7 +15,7 @@ pub fn assert_config(deps: Deps, admin: &Addr, deposit_required: Coin) { assert_eq!(config, ConfigResponse { deposit_required }); } -pub fn assert_services(deps: Deps, expected_services: &[ServiceInfo]) { +pub fn assert_services(deps: Deps, expected_services: &[Service]) { let res = crate::contract::query(deps, mock_env(), QueryMsg::all()).unwrap(); let services: PagedServicesListResponse = from_binary(&res).unwrap(); let start_next_after = expected_services.iter().last().map(|s| s.service_id); @@ -28,7 +29,7 @@ pub fn assert_services(deps: Deps, expected_services: &[ServiceInfo]) { ); } -pub fn assert_service(deps: Deps, expected_service: &ServiceInfo) { +pub fn assert_service(deps: Deps, expected_service: &Service) { let res = crate::contract::query( deps, mock_env(), @@ -37,7 +38,7 @@ pub fn assert_service(deps: Deps, expected_service: &ServiceInfo) { }, ) .unwrap(); - let services: ServiceInfo = from_binary(&res).unwrap(); + let services: Service = from_binary(&res).unwrap(); assert_eq!(&services, expected_service); } @@ -58,8 +59,21 @@ pub fn assert_not_found(deps: Deps, expected_id: ServiceId) { .unwrap_err(); assert!(matches!( res, - ContractError::NotFound { + SpContractError::NotFound { service_id: _expected_id } )); } + +pub fn assert_current_nonce(deps: Deps, address: &Addr, expected_nonce: Nonce) { + let res = crate::contract::query( + deps, + mock_env(), + QueryMsg::SigningNonce { + address: address.to_string(), + }, + ) + .unwrap(); + let nonce: Nonce = from_binary(&res).unwrap(); + assert_eq!(nonce, expected_nonce); +} diff --git a/contracts/service-provider-directory/src/test_helpers/fixture.rs b/contracts/service-provider-directory/src/test_helpers/fixture.rs index 6fcdc41610..0da1ae861c 100644 --- a/contracts/service-provider-directory/src/test_helpers/fixture.rs +++ b/contracts/service-provider-directory/src/test_helpers/fixture.rs @@ -1,43 +1,87 @@ -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Coin, DepsMut}; +use nym_contracts_common::{signing::MessageSignature, IdentityKeyRef}; +use nym_crypto::asymmetric::identity; use nym_service_provider_directory_common::{ - NymAddress, Service, ServiceId, ServiceInfo, ServiceType, + NymAddress, Service, ServiceDetails, ServiceId, ServiceType, +}; +use rand_chacha::rand_core::{CryptoRng, RngCore}; + +use super::{ + helpers::nyms, + signing::{ed25519_sign_message, service_provider_announce_sign_payload}, }; -use super::helpers::nyms; - -pub fn service_fixture() -> Service { - Service { - nym_address: NymAddress::new("nym"), - service_type: ServiceType::NetworkRequester, - announcer: Addr::unchecked("steve"), - block_height: 12345, - deposit: nyms(100), - } -} - -pub fn service_fixture_with_address(nym_address: &str) -> Service { - Service { - nym_address: NymAddress::new(nym_address), - service_type: ServiceType::NetworkRequester, - announcer: Addr::unchecked("steve"), - block_height: 12345, - deposit: nyms(100), - } -} - -pub fn service_info( +pub fn new_service( service_id: ServiceId, - nym_address: NymAddress, - announcer: Addr, -) -> ServiceInfo { - ServiceInfo { + nym_address: &NymAddress, + announcer: &Addr, + identity_key: IdentityKeyRef, +) -> Service { + Service { service_id, - service: Service { - nym_address, + service: ServiceDetails { + nym_address: nym_address.clone(), service_type: ServiceType::NetworkRequester, - announcer, - block_height: 12345, - deposit: nyms(100), + identity_key: identity_key.to_string(), }, + announcer: announcer.clone(), + block_height: 12345, + deposit: nyms(100), } } + +pub fn service_fixture(service_id: ServiceId) -> Service { + new_service( + service_id, + &NymAddress::new("nym"), + &Addr::unchecked("steve"), + "identity", + ) +} + +pub fn service_fixture_with_address(service_id: ServiceId, nym_address: &str) -> Service { + new_service( + service_id, + &NymAddress::new(nym_address), + &Addr::unchecked("steve"), + "identity", + ) +} + +// Create a new service, using a correctly generted identity key +pub fn new_service_details(rng: &mut R, nym_address: &str) -> (ServiceDetails, identity::KeyPair) +where + R: RngCore + CryptoRng, +{ + let keypair = identity::KeyPair::new(rng); + ( + ServiceDetails { + nym_address: NymAddress::new(nym_address), + service_type: ServiceType::NetworkRequester, + identity_key: keypair.public_key().to_base58_string(), + }, + keypair, + ) +} + +// Create a new service, with a correctly generated identity key, and sign it +pub fn new_service_details_with_sign( + deps: DepsMut<'_>, + rng: &mut R, + nym_address: &str, + announcer: &str, + deposit: Coin, +) -> (ServiceDetails, MessageSignature) +where + R: RngCore + CryptoRng, +{ + // Service + let (service, keypair) = new_service_details(rng, nym_address); + + // Sign + let sign_msg = + service_provider_announce_sign_payload(deps.as_ref(), announcer, service.clone(), deposit); + let owner_signature = ed25519_sign_message(sign_msg, keypair.private_key()); + + (service, owner_signature) +} diff --git a/contracts/service-provider-directory/src/test_helpers/helpers.rs b/contracts/service-provider-directory/src/test_helpers/helpers.rs index c34749d633..145802e061 100644 --- a/contracts/service-provider-directory/src/test_helpers/helpers.rs +++ b/contracts/service-provider-directory/src/test_helpers/helpers.rs @@ -1,19 +1,16 @@ -use cosmwasm_std::{ - coin, coins, - testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}, - Coin, DepsMut, Event, MemoryStorage, OwnedDeps, Response, -}; +use cosmwasm_std::{Coin, Event, Response}; use cw_multi_test::AppResponse; -use nym_service_provider_directory_common::{ - events::{ServiceProviderEventType, SERVICE_ID}, - msg::{ExecuteMsg, InstantiateMsg}, - Service, ServiceId, -}; +use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; pub fn nyms(amount: u64) -> Coin { Coin::new(amount.into(), "unym") } +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + ChaCha20Rng::from_seed(dummy_seed) +} + pub fn get_event_types(response: &Response, event_type: &str) -> Vec { response .events @@ -55,50 +52,3 @@ pub fn get_app_attribute(response: &AppResponse, event_type: &str, key: &str) -> .value .clone() } - -#[allow(dead_code)] -pub fn get_app_attributes(response: &AppResponse, event_type: &str, key: &str) -> Vec { - get_app_event_types(response, event_type) - .iter() - .map(|ev| { - ev.attributes - .iter() - .find(|attr| attr.key == key) - .unwrap() - .value - .clone() - }) - .collect::>() -} - -pub fn instantiate_test_contract() -> OwnedDeps { - let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - deposit_required: coin(100, "unym"), - }; - let env = mock_env(); - let info = mock_info("creator", &[]); - let res = crate::instantiate(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.messages.len(), 0); - deps -} - -pub fn announce_service(deps: DepsMut<'_>, service: &Service) -> ServiceId { - let msg: ExecuteMsg = service.clone().into(); - let info = mock_info(service.announcer.as_str(), &coins(100, "unym")); - let res = crate::execute(deps, mock_env(), info, msg).unwrap(); - let service_id: ServiceId = get_attribute( - &res, - &ServiceProviderEventType::Announce.to_string(), - SERVICE_ID, - ) - .parse() - .unwrap(); - service_id -} - -pub fn delete_service(deps: DepsMut<'_>, service_id: ServiceId, announcer: &str) { - let msg = ExecuteMsg::DeleteId { service_id }; - let info = mock_info(announcer, &[]); - crate::execute(deps, mock_env(), info, msg).unwrap(); -} diff --git a/contracts/service-provider-directory/src/test_helpers/mod.rs b/contracts/service-provider-directory/src/test_helpers/mod.rs index b1e4a18e01..ee0717f4e4 100644 --- a/contracts/service-provider-directory/src/test_helpers/mod.rs +++ b/contracts/service-provider-directory/src/test_helpers/mod.rs @@ -1,4 +1,5 @@ pub mod assert; pub mod fixture; pub mod helpers; -pub mod test_setup; +pub mod signing; +pub mod transactions; diff --git a/contracts/service-provider-directory/src/test_helpers/signing.rs b/contracts/service-provider-directory/src/test_helpers/signing.rs new file mode 100644 index 0000000000..4bd1708bff --- /dev/null +++ b/contracts/service-provider-directory/src/test_helpers/signing.rs @@ -0,0 +1,41 @@ +use cosmwasm_std::{Addr, Coin, Deps}; +use nym_contracts_common::signing::{ + MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, +}; +use nym_crypto::asymmetric::identity; +use nym_service_provider_directory_common::{ + signing_types::{ + construct_service_provider_announce_sign_payload, SignableServiceProviderAnnounceMsg, + }, + ServiceDetails, +}; +use serde::Serialize; + +use crate::state; + +pub fn service_provider_announce_sign_payload( + deps: Deps<'_>, + owner: &str, + service: ServiceDetails, + deposit: Coin, +) -> SignableServiceProviderAnnounceMsg { + let owner = Addr::unchecked(owner); + let nonce = state::get_signing_nonce(deps.storage, owner.clone()).unwrap(); + construct_service_provider_announce_sign_payload(nonce, owner, deposit, service) +} + +pub fn ed25519_sign_message( + message: SignableMessage, + private_key: &identity::PrivateKey, +) -> MessageSignature { + match message.algorithm { + SigningAlgorithm::Ed25519 => { + let plaintext = message.to_plaintext().unwrap(); + let signature = private_key.sign(&plaintext); + MessageSignature::from(signature.to_bytes().as_ref()) + } + SigningAlgorithm::Secp256k1 => { + unimplemented!() + } + } +} diff --git a/contracts/service-provider-directory/src/test_helpers/transactions.rs b/contracts/service-provider-directory/src/test_helpers/transactions.rs new file mode 100644 index 0000000000..9a28a0c93b --- /dev/null +++ b/contracts/service-provider-directory/src/test_helpers/transactions.rs @@ -0,0 +1,69 @@ +use cosmwasm_std::{ + coin, + testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}, + DepsMut, MemoryStorage, OwnedDeps, +}; + +use nym_service_provider_directory_common::{ + events::{ServiceProviderEventType, SERVICE_ID}, + msg::{ExecuteMsg, InstantiateMsg}, + ServiceDetails, ServiceId, +}; +use rand_chacha::rand_core::{CryptoRng, RngCore}; + +use super::helpers::{get_attribute, nyms}; + +pub fn instantiate_test_contract() -> OwnedDeps { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + deposit_required: coin(100, "unym"), + }; + let env = mock_env(); + let info = mock_info("creator", &[]); + let res = crate::instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + deps +} + +pub fn announce_service( + mut deps: DepsMut<'_>, + rng: &mut R, + nym_address: &str, + announcer: &str, +) -> (ServiceId, ServiceDetails) +where + R: RngCore + CryptoRng, +{ + let deposit = nyms(100); + let (service, owner_signature) = super::fixture::new_service_details_with_sign( + deps.branch(), + rng, + nym_address, + announcer, + deposit.clone(), + ); + + // Announce + let msg = ExecuteMsg::Announce { + service: service.clone(), + owner_signature, + }; + let info = mock_info(announcer, &[deposit]); + let res = crate::execute(deps, mock_env(), info, msg).unwrap(); + + let service_id: ServiceId = get_attribute( + &res, + &ServiceProviderEventType::Announce.to_string(), + SERVICE_ID, + ) + .parse() + .unwrap(); + + (service_id, service) +} + +pub fn delete_service(deps: DepsMut<'_>, service_id: ServiceId, announcer: &str) { + let msg = ExecuteMsg::DeleteId { service_id }; + let info = mock_info(announcer, &[]); + crate::execute(deps, mock_env(), info, msg).unwrap(); +} diff --git a/envs/qa-qwerty.env b/envs/qa-qwerty.env index ce4be6e28a..2a6a5da732 100644 --- a/envs/qa-qwerty.env +++ b/envs/qa-qwerty.env @@ -17,7 +17,7 @@ GROUP_CONTRACT_ADDRESS=n1fqquzw4mk0pkamgr2ywt2v7h2j9nuyjjn4gvpy8zlpp6xn0uyuzqfm2 MULTISIG_CONTRACT_ADDRESS=n1gaq3666chd5348apj8cka8t2mckv7azp9espyr7wgpxyuzur5d0sazpysy COCONUT_DKG_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy -SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ryt076cufyddallg5x0gz3qjz0pd3wg0m4cwkg9njhmlnp6u88qq6nczgj +SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1qsn2655eflc0nx2uwqtwyv5kad5dwm4c0gn72yr4q4de5r3jaz2slvqjgt NAME_SERVICE_CONTRACT_ADDRESS=n1cm2u5vfjd3zalfw0p65xyh4tcrw3hjlm0960gzhewga449h4mgas77mjkl STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://qwerty-validator.qa.nymte.ch/" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index b762bf34f5..457a30070a 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -17,6 +17,7 @@ rust-version = "1.56" [dependencies] anyhow = "1.0.53" async-trait = { workspace = true } +atty = "0.2" bip39 = { workspace = true } bs58 = "0.4.0" clap = { version = "4.0", features = ["cargo", "derive"] } @@ -32,20 +33,11 @@ once_cell = "1.7.2" pretty_env_logger = "0.4" rand = "0.7" serde = { workspace = true, features = ["derive"] } -sqlx = { version = "0.5", features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", -] } +serde_json = { workspace = true } +sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", ] } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" -tokio = { version = "1.24.1", features = [ - "rt-multi-thread", - "net", - "signal", - "fs", -] } +tokio = { version = "1.24.1", features = [ "rt-multi-thread", "net", "signal", "fs", ] } tokio-stream = { version = "0.1.11", features = ["fs"] } tokio-tungstenite = "0.14" tokio-util = { version = "0.7.4", features = ["codec"] } @@ -53,27 +45,22 @@ url = { version = "2.2", features = ["serde"] } zeroize = { workspace = true } # internal -nym-coconut-interface = { path = "../common/coconut-interface" } -nym-credentials = { path = "../common/credentials" } -nym-config = { path = "../common/config" } -nym-crypto = { path = "../common/crypto" } +nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-coconut-interface = { path = "../common/coconut-interface" } +nym-config = { path = "../common/config" } +nym-credentials = { path = "../common/credentials" } +nym-crypto = { path = "../common/crypto" } nym-gateway-requests = { path = "gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } -nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } +nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } -nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-task = { path = "../common/task" } -nym-validator-client = { path = "../common/client-libs/validator-client", features = [ - "nyxd-client", -] } nym-types = { path = "../common/types" } -serde_json = { workspace = true } -atty = "0.2" - +nym-validator-client = { path = "../common/client-libs/validator-client", features = [ "nyxd-client" ] } [build-dependencies] tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index adde788bab..235afdb630 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -7,7 +7,7 @@ use nym_mixnet_contract_common::{ RewardingParams, }; use nym_name_service_common::NameEntry; -use nym_service_provider_directory_common::ServiceInfo; +use nym_service_provider_directory_common::Service; use std::collections::HashSet; pub(crate) struct ValidatorCacheData { @@ -25,7 +25,7 @@ pub(crate) struct ValidatorCacheData { pub(crate) mix_to_family: Cache>, - pub(crate) service_providers: Cache>, + pub(crate) service_providers: Cache>, pub(crate) registered_names: Cache>, } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 73b9699c6a..13e2783e47 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -6,7 +6,7 @@ use nym_mixnet_contract_common::{ RewardingParams, }; use nym_name_service_common::NameEntry; -use nym_service_provider_directory_common::ServiceInfo; +use nym_service_provider_directory_common::Service; use rocket::fairing::AdHoc; use std::{ collections::HashSet, @@ -52,7 +52,7 @@ impl NymContractCache { rewarding_params: RewardingParams, current_interval: Interval, mix_to_family: Vec<(IdentityKey, FamilyHead)>, - services: Option>, + services: Option>, names: Option>, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { @@ -293,7 +293,7 @@ impl NymContractCache { self.mixnode_details(mix_id).await.1 } - pub(crate) async fn services(&self) -> Cache> { + pub(crate) async fn services(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.service_providers.clone(), Err(err) => { diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 09f7b01640..d77febfb3d 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -1053,6 +1053,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-controllers" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-storage-plus" version = "0.13.4" @@ -3598,8 +3612,12 @@ name = "nym-service-provider-directory-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs b/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs index 0e58035274..a518dc29b1 100644 --- a/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs +++ b/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs @@ -42,7 +42,7 @@ pub async fn get_services() -> Result> { let mut filtered: Vec = vec![]; for service in &services_res { - let items: _ = service + let items = service .items .clone() .into_iter() diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 4b27a3d74c..3ec89c141f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -934,6 +934,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-controllers" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-storage-plus" version = "0.13.4" @@ -3012,8 +3026,12 @@ name = "nym-service-provider-directory-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 0ca9f50631..9e98516df5 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -12,6 +12,7 @@ rust-version = "1.65" [dependencies] async-trait = { workspace = true } +bs58 = "0.4.0" clap = {version = "4.0", features = ["cargo", "derive"]} dirs = "4.0" futures = "0.3.24" @@ -24,6 +25,7 @@ publicsuffix = "1.5" # Can't update this until bip updates to support newer idna rand = "0.7.3" reqwest = { version = "0.11.11", features = ["json"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "chrono"]} tap = { workspace = true } thiserror = "1.0" @@ -47,6 +49,7 @@ nym-service-providers-common = { path = "../common" } nym-socks5-requests = { path = "../../common/socks5/requests" } nym-statistics-common = { path = "../../common/statistics" } nym-task = { path = "../../common/task" } +nym-types = { path = "../../common/types" } nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" } [dev-dependencies] diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index f7d287c4bb..b2fdda09ee 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -5,6 +5,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use log::info; use nym_bin_common::build_information::BinaryBuildInformation; use nym_bin_common::completions::{fig_generate, ArgShell}; +use nym_bin_common::version_checker; use nym_config::NymConfig; use crate::config::old_config_v1_1_13::OldConfigV1_1_13; @@ -15,6 +16,7 @@ use crate::{ mod init; mod run; +mod sign; lazy_static::lazy_static! { pub static ref PRETTY_BUILD_INFORMATION: String = @@ -47,6 +49,9 @@ pub(crate) enum Commands { /// parameters. Run(run::Run), + /// Sign to prove ownership of this network requester + Sign(sign::Sign), + /// Generate shell completions Completions(ArgShell), @@ -91,6 +96,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { match &args.command { Commands::Init(m) => init::execute(m).await?, Commands::Run(m) => run::execute(m).await?, + Commands::Sign(m) => sign::execute(m).await?, Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), } @@ -111,6 +117,36 @@ fn try_upgrade_v1_1_13_config(id: &str) -> std::io::Result<()> { updated.save_to_file(None) } +// this only checks compatibility between config the binary. It does not take into consideration +// network version. It might do so in the future. +fn version_check(cfg: &Config) -> bool { + let binary_version = env!("CARGO_PKG_VERSION"); + let config_version = cfg.get_base().get_version(); + if binary_version == config_version { + true + } else { + log::warn!( + "The native-client binary has different version than what is specified \ + in config file! {} and {}", + binary_version, + config_version + ); + if version_checker::is_minor_version_compatible(binary_version, config_version) { + log::info!( + "but they are still semver compatible. \ + However, consider running the `upgrade` command" + ); + true + } else { + log::error!( + "and they are semver incompatible! - \ + please run the `upgrade` command before attempting `run` again" + ); + false + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index bacbd792f0..cde999e83b 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -1,14 +1,13 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cli::try_upgrade_v1_1_13_config; +use crate::cli::{try_upgrade_v1_1_13_config, version_check}; use crate::{ cli::{override_config, OverrideConfig}, config::Config, error::NetworkRequesterError, }; use clap::Args; -use nym_bin_common::version_checker; use nym_config::NymConfig; use nym_sphinx::addressing::clients::Recipient; @@ -61,36 +60,6 @@ impl From for OverrideConfig { } } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = cfg.get_base().get_version(); - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {} and {}", - binary_version, - config_version - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} - pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { if args.open_proxy { println!( @@ -145,7 +114,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { return Err(NetworkRequesterError::FailedLocalVersionCheck); } - // TODO: consider incorporating statistics_recipient, open_proxuy and enable_statistics in + // TODO: consider incorporating statistics_recipient, open_proxy and enable_statistics in // `Config`. let stats_provider_addr = args diff --git a/service-providers/network-requester/src/cli/sign.rs b/service-providers/network-requester/src/cli/sign.rs new file mode 100644 index 0000000000..c5bfd94578 --- /dev/null +++ b/service-providers/network-requester/src/cli/sign.rs @@ -0,0 +1,96 @@ +use clap::Args; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; +use nym_config::NymConfig; +use nym_crypto::asymmetric::identity; +use nym_types::helpers::ConsoleSigningOutput; + +use crate::{config::Config, error::NetworkRequesterError}; + +use super::version_check; + +#[derive(Args, Clone)] +pub(crate) struct Sign { + /// The id of the mixnode you want to sign with + #[clap(long)] + id: String, + + /// Signs a transaction-specific payload, that is going to be sent to the smart contract, with your identity key + #[clap(long)] + contract_msg: String, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +fn print_signed_contract_msg( + private_key: &identity::PrivateKey, + raw_msg: &str, + output: OutputFormat, +) { + let trimmed = raw_msg.trim(); + eprintln!(">>> attempting to sign {trimmed}"); + + let Ok(decoded) = bs58::decode(trimmed).into_vec() else { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + eprintln!(">>> decoding the message..."); + + // we don't really care about what particular information is embedded inside of it, + // we just want to know if user correctly copied the string, i.e. whether it's a valid bs58 encoded json + if serde_json::from_slice::(&decoded).is_err() { + println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); + return; + }; + + // if this is a valid json, it MUST be a valid string + let decoded_string = String::from_utf8(decoded.clone()).unwrap(); + let signature = private_key.sign(&decoded).to_base58_string(); + + let sign_output = ConsoleSigningOutput::new(decoded_string, signature); + println!("{}", output.format(&sign_output)); +} + +pub(crate) async fn execute(args: &Sign) -> Result<(), NetworkRequesterError> { + let id = &args.id; + + let mut config = match Config::load_from_file(id) { + Ok(cfg) => cfg, + Err(err) => { + log::error!( + "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {err})", + id, + ); + return Err(NetworkRequesterError::FailedToLoadConfig(id.to_string())); + } + }; + + if !config.validate() { + return Err(NetworkRequesterError::ConfigValidationFailure); + } + + if config.get_base_mut().set_empty_fields_to_defaults() { + log::warn!( + "Some of the core config options were left unset. \ + The default values are going to get used instead." + ); + } + + if !version_check(&config) { + log::error!("Failed the local version check"); + return Err(NetworkRequesterError::FailedLocalVersionCheck); + } + + let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base()); + let identity_keypair = nym_client_core::init::load_identity_keys(&pathfinder)?; + + print_signed_contract_msg( + identity_keypair.private_key(), + &args.contract_msg, + args.output, + ); + + Ok(()) +} diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 9e3dc81f23..a5acae77b9 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -54,6 +54,7 @@ pub(crate) struct Cli { command: Commands, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub(crate) enum Commands { /// Query and manage Nyx blockchain accounts diff --git a/tools/nym-cli/src/validator/mixnet/operators/services/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/services/mod.rs index 09df9b6a55..1ee172d7c4 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/services/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/services/mod.rs @@ -9,6 +9,7 @@ pub(crate) async fn execute( match service.command { nym_cli_commands::validator::mixnet::operators::service::MixnetOperatorsServiceCommands::Announce(announce) => nym_cli_commands::validator::mixnet::operators::service::announce::announce(announce, create_signing_client(global_args, network_details)?).await, nym_cli_commands::validator::mixnet::operators::service::MixnetOperatorsServiceCommands::Delete(delete) => nym_cli_commands::validator::mixnet::operators::service::delete::delete(delete, create_signing_client(global_args, network_details)?).await, + nym_cli_commands::validator::mixnet::operators::service::MixnetOperatorsServiceCommands::CreateServiceAnnounceSignPayload(args) => nym_cli_commands::validator::mixnet::operators::service::announce_sign_payload::create_payload(args, create_signing_client(global_args, network_details)?).await, } Ok(()) }