From e24c32ed8b5a1cdcc2c4f9e47c7cce35981cef44 Mon Sep 17 00:00:00 2001 From: renauter Date: Fri, 25 Nov 2022 09:07:51 -0300 Subject: [PATCH 01/12] feat: full service contract changes extracted from #513 --- .../pallets/pallet-smart-contract/src/cost.rs | 32 +- .../pallets/pallet-smart-contract/src/lib.rs | 488 ++++++++++++- .../pallet-smart-contract/src/tests.rs | 650 +++++++++++++++++- .../pallet-smart-contract/src/types.rs | 38 +- 4 files changed, 1200 insertions(+), 8 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index dce6560db..aad5565c1 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -2,7 +2,7 @@ use crate::pallet; use crate::pallet::BalanceOf; use crate::pallet::Error; use crate::types; -use crate::types::{Contract, ContractBillingInformation}; +use crate::types::{Contract, ContractBillingInformation, ServiceContract, ServiceContractBill}; use crate::Config; use frame_support::dispatch::DispatchErrorWithPostInfo; use pallet_tfgrid::types as pallet_tfgrid_types; @@ -88,6 +88,36 @@ impl Contract { } } +impl ServiceContract { + pub fn calculate_bill_cost_tft( + &self, + service_bill: ServiceContractBill, + ) -> Result, DispatchErrorWithPostInfo> { + // Calculate the cost in mUSD for service contract bill + let total_cost = self.calculate_bill_cost(service_bill); + + if total_cost == 0 { + return Ok(BalanceOf::::saturated_from(0 as u128)); + } + + // Calculate the cost in TFT for service contract + let total_cost_tft_64 = calculate_cost_in_tft_from_musd::(total_cost)?; + + // convert to balance object + let amount_due: BalanceOf = BalanceOf::::saturated_from(total_cost_tft_64); + + return Ok(amount_due); + } + + pub fn calculate_bill_cost(&self, service_bill: ServiceContractBill) -> u64 { + // bill user for service usage for elpased usage (window) in seconds + let contract_cost = U64F64::from_num(self.base_fee) * U64F64::from_num(service_bill.window) + / 3600 + + U64F64::from_num(service_bill.variable_amount); + contract_cost.round().to_num::() + } +} + // Calculates the total cost of a node contract. pub fn calculate_resources_cost( resources: &Resources, diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 9943323c6..1b44fa2e7 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_std::prelude::*; - +use core::marker::PhantomData; use frame_support::{ dispatch::DispatchErrorWithPostInfo, ensure, @@ -30,8 +29,8 @@ use sp_runtime::{ traits::{CheckedSub, SaturatedConversion}, Perbill, }; +use sp_std::prelude::*; use substrate_fixed::types::U64F64; - use tfchain_support::{ resources::Resources, traits::{ChangeNode, Tfgrid}, @@ -222,6 +221,15 @@ pub mod pallet { #[pallet::getter(fn deployment_id)] pub type DeploymentID = StorageValue<_, u64, ValueQuery>; + #[pallet::storage] + #[pallet::getter(fn service_contracts)] + pub type ServiceContracts = + StorageMap<_, Blake2_128Concat, u64, ServiceContract, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn service_contract_id)] + pub type ServiceContractID = StorageValue<_, u64, ValueQuery>; + #[pallet::config] pub trait Config: CreateSignedTransaction> @@ -350,6 +358,19 @@ pub mod pallet { DeploymentCanceled { deployment_id: u64, }, + /// A Service contract is created + ServiceContractCreated { + service_contract_id: u64, + }, + /// A Service contract is approved + ServiceContractApproved { + service_contract_id: u64, + }, + /// A Service contract is canceled + ServiceContractCanceled { + service_contract_id: u64, + cause: types::Cause, + }, } #[pallet::error] @@ -396,6 +417,15 @@ pub mod pallet { NotEnoughResourcesInCapacityReservation, DeploymentNotExists, TwinNotAuthorized, + ServiceContractNotExists, + ServiceContractCreationNotAllowed, + ServiceContractModificationNotAllowed, + ServiceContractApprovalNotAllowed, + ServiceContractRejectionNotAllowed, + ServiceContractBillingNotAllowed, + ServiceContractBillMetadataTooLong, + ServiceContractMetadataTooLong, + ServiceContractNotEnoughFundsToPayBill, } #[pallet::genesis_config] @@ -567,6 +597,84 @@ pub mod pallet { let _account_id = ensure_signed(origin)?; Self::bill_contract(contract_id) } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_create( + origin: OriginFor, + service_account: T::AccountId, + consumer_account: T::AccountId, + ) -> DispatchResultWithPostInfo { + let caller_account = ensure_signed(origin)?; + Self::_service_contract_create(caller_account, service_account, consumer_account) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_set_metadata( + origin: OriginFor, + service_contract_id: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_set_metadata(account_id, service_contract_id, metadata) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_set_fees( + origin: OriginFor, + service_contract_id: u64, + base_fee: u64, + variable_fee: u64, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_set_fees( + account_id, + service_contract_id, + base_fee, + variable_fee, + ) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_approve( + origin: OriginFor, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_approve(account_id, service_contract_id) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_reject( + origin: OriginFor, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_reject(account_id, service_contract_id) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_cancel( + origin: OriginFor, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_cancel( + account_id, + service_contract_id, + types::Cause::CanceledByUser, + ) + } + + #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] + pub fn service_contract_bill( + origin: OriginFor, + service_contract_id: u64, + variable_amount: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_service_contract_bill(account_id, service_contract_id, variable_amount, metadata) + } } #[pallet::hooks] @@ -2201,6 +2309,380 @@ impl Pallet { let now = >::block_number().saturated_into::(); now % BillingFrequency::::get() } + + pub fn _service_contract_create( + caller: T::AccountId, + service: T::AccountId, + consumer: T::AccountId, + ) -> DispatchResultWithPostInfo { + let caller_twin_id = + pallet_tfgrid::TwinIdByAccountID::::get(&caller).ok_or(Error::::TwinNotExists)?; + + let service_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&service) + .ok_or(Error::::TwinNotExists)?; + + let consumer_twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&consumer) + .ok_or(Error::::TwinNotExists)?; + + // Only service or consumer can create contract + ensure!( + caller_twin_id == service_twin_id || caller_twin_id == consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Service twin and consumer twin can not be the same + ensure!( + service_twin_id != consumer_twin_id, + Error::::ServiceContractCreationNotAllowed, + ); + + // Create service contract + let service_contract = types::ServiceContract { + service_twin_id, + consumer_twin_id, + base_fee: 0, + variable_fee: 0, + metadata: vec![].try_into().unwrap(), + accepted_by_service: false, + accepted_by_consumer: false, + last_bill: 0, + state: types::ServiceContractState::Created, + phantom: PhantomData, + }; + + // Get the service contract ID map and increment + let mut id = ServiceContractID::::get(); + id = id + 1; + + // Insert into service contract map + ServiceContracts::::insert(id, &service_contract); + + // Update Contract ID + ServiceContractID::::put(id); + + // Trigger event for service contract creation + Self::deposit_event(Event::ServiceContractCreated { + service_contract_id: id, + }); + + Ok(().into()) + } + + pub fn _service_contract_set_metadata( + account_id: T::AccountId, + service_contract_id: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service or consumer can set metadata + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Only allow to modify metadata if contract still not approved by both parties + ensure!( + !matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractModificationNotAllowed, + ); + + service_contract.metadata = BoundedVec::try_from(metadata) + .map_err(|_| Error::::ServiceContractMetadataTooLong)?; + + // If base_fee is set and non-zero (mandatory) + if service_contract.base_fee != 0 { + service_contract.state = types::ServiceContractState::AgreementReady; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract); + + Ok(().into()) + } + + pub fn _service_contract_set_fees( + account_id: T::AccountId, + service_contract_id: u64, + base_fee: u64, + variable_fee: u64, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service can set fees + ensure!( + twin_id == service_contract.service_twin_id, + Error::::TwinNotAuthorized, + ); + + // Only allow to modify fees if contract still not approved by both parties + ensure!( + !matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractModificationNotAllowed, + ); + + service_contract.base_fee = base_fee; + service_contract.variable_fee = variable_fee; + + // If metadata is filled and not empty (mandatory) + if !service_contract.metadata.is_empty() { + service_contract.state = types::ServiceContractState::AgreementReady; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract); + + Ok(().into()) + } + + pub fn _service_contract_approve( + account_id: T::AccountId, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Allow to approve contract only if agreement is ready + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::AgreementReady + ), + Error::::ServiceContractApprovalNotAllowed, + ); + + // Only service or consumer can accept agreement + if twin_id == service_contract.service_twin_id { + service_contract.accepted_by_service = true; + } else if twin_id == service_contract.consumer_twin_id { + service_contract.accepted_by_consumer = true + } else { + return Err(DispatchErrorWithPostInfo::from( + Error::::TwinNotAuthorized, + )); + } + + // If both parties (service and consumer) accept then contract is approved and can be billed + if service_contract.accepted_by_service && service_contract.accepted_by_consumer { + // Change contract state to approved and emit event + service_contract.state = types::ServiceContractState::ApprovedByBoth; + Self::deposit_event(Event::ServiceContractApproved { + service_contract_id, + }); + // Initialize billing time + let now = >::get().saturated_into::() / 1000; + service_contract.last_bill = now; + } + + // Update service contract in map after modification + ServiceContracts::::insert(service_contract_id, service_contract); + + Ok(().into()) + } + + pub fn _service_contract_reject( + account_id: T::AccountId, + service_contract_id: u64, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Allow to reject contract only if agreement is ready + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::AgreementReady + ), + Error::::ServiceContractRejectionNotAllowed, + ); + + // Only service or consumer can reject agreement + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // If one party (service or consumer) rejects agreement + // then contract is canceled and removed from service contract map + Self::_service_contract_cancel( + account_id, + service_contract_id, + types::Cause::CanceledByUser, + )?; + + Ok(().into()) + } + + pub fn _service_contract_cancel( + account_id: T::AccountId, + service_contract_id: u64, + cause: types::Cause, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service or consumer can cancel contract + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + + // Remove contract from service contract map + // Can be done at any state of contract + // so no need to handle state validation + ServiceContracts::::remove(service_contract_id); + Self::deposit_event(Event::ServiceContractCanceled { + service_contract_id, + cause, + }); + + log::info!( + "successfully removed service contract with id {:?}", + service_contract_id, + ); + + Ok(().into()) + } + + #[transactional] + pub fn _service_contract_bill( + account_id: T::AccountId, + service_contract_id: u64, + variable_amount: u64, + metadata: Vec, + ) -> DispatchResultWithPostInfo { + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + + let mut service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + + // Only service can bill consumer for service contract + ensure!( + twin_id == service_contract.service_twin_id, + Error::::TwinNotAuthorized, + ); + + // Allow to bill contract only if approved by both + ensure!( + matches!( + service_contract.state, + types::ServiceContractState::ApprovedByBoth + ), + Error::::ServiceContractBillingNotAllowed, + ); + + // Get elapsed time (in seconds) to bill for service + let now = >::get().saturated_into::() / 1000; + let elapsed_seconds_since_last_bill = now - service_contract.last_bill; + + // Billing time (window) is max 1h by design + // So extra time will not be billed + // It is the service responsability to bill on right frequency + let window = elapsed_seconds_since_last_bill.min(3600); + + // Billing variable amount is bounded by contract variable fee + ensure!( + variable_amount + <= ((U64F64::from_num(window) / 3600) + * U64F64::from_num(service_contract.variable_fee)) + .round() + .to_num::(), + Error::::ServiceContractBillingNotAllowed, + ); + + let bill_metadata = BoundedVec::try_from(metadata) + .map_err(|_| Error::::ServiceContractBillMetadataTooLong)?; + + // Create service contract bill + let service_contract_bill = types::ServiceContractBill { + variable_amount, + window, + metadata: bill_metadata, + }; + + // Make consumer pay for service contract bill + Self::_service_contract_pay_bill(service_contract_id, service_contract_bill)?; + + // Update contract in list after modification + service_contract.last_bill = now; + ServiceContracts::::insert(service_contract_id, service_contract); + + Ok(().into()) + } + + // Pay a service contract bill + // Calculates how much TFT is due by the consumer and pay the amount to the service + fn _service_contract_pay_bill( + service_contract_id: u64, + bill: types::ServiceContractBill, + ) -> DispatchResultWithPostInfo { + let service_contract = ServiceContracts::::get(service_contract_id) + .ok_or(Error::::ServiceContractNotExists)?; + let amount_due = service_contract.calculate_bill_cost_tft(bill)?; + + let service_twin = pallet_tfgrid::Twins::::get(service_contract.service_twin_id) + .ok_or(Error::::TwinNotExists)?; + + let consumer_twin = pallet_tfgrid::Twins::::get(service_contract.consumer_twin_id) + .ok_or(Error::::TwinNotExists)?; + + let usable_balance = Self::get_usable_balance(&consumer_twin.account_id); + + // If consumer is out of funds then contract is canceled + // by service and removed from service contract map + if usable_balance < amount_due { + Self::_service_contract_cancel( + service_twin.account_id, + service_contract_id, + types::Cause::OutOfFunds, + )?; + return Err(DispatchErrorWithPostInfo::from( + Error::::ServiceContractNotEnoughFundsToPayBill, + )); + } + + // Transfer amount due from consumer account to service account + ::Currency::transfer( + &consumer_twin.account_id, + &service_twin.account_id, + amount_due, + ExistenceRequirement::KeepAlive, + )?; + + log::info!( + "bill successfully payed by consumer for service contract with id {:?}", + service_contract_id, + ); + + Ok(().into()) + } } impl ChangeNode, PubConfigOf, InterfaceOf, SerialNumberOf> diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 3387c1863..9e98e9e33 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -15,7 +15,10 @@ use pallet_tfgrid::{ }; use sp_core::H256; use sp_runtime::{assert_eq_error_rate, traits::SaturatedConversion, Perbill, Percent}; -use sp_std::convert::{TryFrom, TryInto}; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, +}; use substrate_fixed::types::U64F64; use tfchain_support::types::{ CapacityReservationPolicy, ConsumableResources, NodeFeatures, PowerState, PowerTarget, @@ -24,6 +27,9 @@ use tfchain_support::types::{ use tfchain_support::types::{FarmCertification, NodeCertification, PublicIP}; const GIGABYTE: u64 = 1024 * 1024 * 1024; +const BASE_FEE: u64 = 1000; +const VARIABLE_FEE: u64 = 1000; +const VARIABLE_AMOUNT: u64 = 100; // GROUP TESTS // // -------------------- // @@ -3459,6 +3465,583 @@ fn test_capacity_reservation_contract_create_with_solution_provider_fails_if_not }); } +// SERVICE CONTRACT TESTS // +// ---------------------- // + +#[test] +fn test_service_contract_create_works() { + new_test_ext().execute_with(|| { + run_to_block(1, None); + create_service_consumer_contract(); + + assert_eq!( + get_service_contract(), + SmartContractModule::service_contracts(1).unwrap(), + ); + + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule( + SmartContractEvent::ServiceContractCreated { + service_contract_id: 1, + } + )), + ); + }); +} + +#[test] +fn test_service_contract_create_by_anyone_fails() { + new_test_ext().execute_with(|| { + create_twin(alice()); + create_twin(bob()); + create_twin(charlie()); + + assert_noop!( + SmartContractModule::service_contract_create( + Origin::signed(charlie()), + alice(), + bob(), + ), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_create_same_account_fails() { + new_test_ext().execute_with(|| { + create_twin(alice()); + + assert_noop!( + SmartContractModule::service_contract_create( + Origin::signed(alice()), + alice(), + alice(), + ), + Error::::ServiceContractCreationNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_set_metadata_works() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_ok!(SmartContractModule::service_contract_set_metadata( + Origin::signed(alice()), + 1, + b"some_metadata".to_vec(), + )); + + let mut service_contract = get_service_contract(); + service_contract.metadata = BoundedVec::try_from(b"some_metadata".to_vec()).unwrap(); + assert_eq!( + service_contract, + SmartContractModule::service_contracts(1).unwrap(), + ); + }); +} + +#[test] +fn test_service_contract_set_metadata_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + create_twin(charlie()); + + assert_noop!( + SmartContractModule::service_contract_set_metadata( + Origin::signed(charlie()), + 1, + b"some_metadata".to_vec(), + ), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_set_metadata_already_approved_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_set_metadata( + Origin::signed(alice()), + 1, + b"some_metadata".to_vec(), + ), + Error::::ServiceContractModificationNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_set_metadata_too_long_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_set_metadata( + Origin::signed(alice()), + 1, + b"very_loooooooooooooooooooooooooooooooooooooooooooooooooong_metadata".to_vec(), + ), + Error::::ServiceContractMetadataTooLong + ); + }); +} + +#[test] +fn test_service_contract_set_fees_works() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_ok!(SmartContractModule::service_contract_set_fees( + Origin::signed(alice()), + 1, + BASE_FEE, + VARIABLE_FEE, + )); + + let mut service_contract = get_service_contract(); + service_contract.base_fee = BASE_FEE; + service_contract.variable_fee = VARIABLE_FEE; + assert_eq!( + service_contract, + SmartContractModule::service_contracts(1).unwrap(), + ); + }); +} + +#[test] +fn test_service_contract_set_fees_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_set_fees( + Origin::signed(bob()), + 1, + BASE_FEE, + VARIABLE_FEE, + ), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_set_fees_already_approved_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_set_fees( + Origin::signed(alice()), + 1, + BASE_FEE, + VARIABLE_FEE, + ), + Error::::ServiceContractModificationNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_approve_works() { + new_test_ext().execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + + let mut service_contract = get_service_contract(); + service_contract.base_fee = BASE_FEE; + service_contract.variable_fee = VARIABLE_FEE; + service_contract.metadata = BoundedVec::try_from(b"some_metadata".to_vec()).unwrap(); + service_contract.state = types::ServiceContractState::AgreementReady; + assert_eq!( + service_contract, + SmartContractModule::service_contracts(1).unwrap(), + ); + + // Service approves + assert_ok!(SmartContractModule::service_contract_approve( + Origin::signed(alice()), + 1, + )); + + service_contract.accepted_by_service = true; + assert_eq!( + service_contract, + SmartContractModule::service_contracts(1).unwrap(), + ); + + // Consumer approves + assert_ok!(SmartContractModule::service_contract_approve( + Origin::signed(bob()), + 1, + )); + + service_contract.accepted_by_consumer = true; + service_contract.last_bill = get_timestamp_in_seconds_for_block(1); + service_contract.state = types::ServiceContractState::ApprovedByBoth; + assert_eq!( + service_contract, + SmartContractModule::service_contracts(1).unwrap(), + ); + + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule(SmartContractEvent::< + TestRuntime, + >::ServiceContractApproved { + service_contract_id: 1, + })), + ); + }); +} + +#[test] +fn test_service_contract_approve_agreement_not_ready_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_approve(Origin::signed(alice()), 1,), + Error::::ServiceContractApprovalNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_approve_already_approved_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_approve(Origin::signed(alice()), 1,), + Error::::ServiceContractApprovalNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_approve_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + create_twin(charlie()); + + assert_noop!( + SmartContractModule::service_contract_approve(Origin::signed(charlie()), 1,), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_reject_works() { + new_test_ext().execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + + assert_ok!(SmartContractModule::service_contract_reject( + Origin::signed(alice()), + 1, + )); + + assert_eq!(SmartContractModule::service_contracts(1).is_none(), true); + + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule(SmartContractEvent::< + TestRuntime, + >::ServiceContractCanceled { + service_contract_id: 1, + cause: types::Cause::CanceledByUser, + })), + ); + }); +} + +#[test] +fn test_service_contract_reject_agreement_not_ready_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_reject(Origin::signed(alice()), 1,), + Error::::ServiceContractRejectionNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_reject_already_approved_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_reject(Origin::signed(alice()), 1,), + Error::::ServiceContractRejectionNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_reject_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + create_twin(charlie()); + + assert_noop!( + SmartContractModule::service_contract_reject(Origin::signed(charlie()), 1,), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_cancel_works() { + new_test_ext().execute_with(|| { + run_to_block(1, None); + create_service_consumer_contract(); + + assert_ok!(SmartContractModule::service_contract_cancel( + Origin::signed(alice()), + 1, + )); + + assert_eq!(SmartContractModule::service_contracts(1).is_none(), true); + + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule(SmartContractEvent::< + TestRuntime, + >::ServiceContractCanceled { + service_contract_id: 1, + cause: types::Cause::CanceledByUser, + })), + ); + }); +} + +#[test] +fn test_service_contract_cancel_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + create_service_consumer_contract(); + create_twin(charlie()); + + assert_noop!( + SmartContractModule::service_contract_cancel(Origin::signed(charlie()), 1,), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_bill_works() { + let (mut ext, mut pool_state) = new_test_ext_with_pool_state(0); + ext.execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + + let service_contract = SmartContractModule::service_contracts(1).unwrap(); + assert_eq!(service_contract.last_bill, 0); + + approve_service_consumer_contract(); + + let service_contract = SmartContractModule::service_contracts(1).unwrap(); + assert_eq!( + service_contract.last_bill, + get_timestamp_in_seconds_for_block(1) + ); + + let consumer_twin = TfgridModule::twins(2).unwrap(); + let consumer_balance = Balances::free_balance(&consumer_twin.account_id); + assert_eq!(consumer_balance, 2500000000); + + // Bill 20 min after contract approval + run_to_block(201, Some(&mut pool_state)); + assert_ok!(SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + VARIABLE_AMOUNT, + b"bill_metadata".to_vec(), + )); + + let service_contract = SmartContractModule::service_contracts(1).unwrap(); + assert_eq!( + service_contract.last_bill, + get_timestamp_in_seconds_for_block(201) + ); + + let consumer_balance = Balances::free_balance(&consumer_twin.account_id); + let window = + get_timestamp_in_seconds_for_block(201) - get_timestamp_in_seconds_for_block(1); + let bill = types::ServiceContractBill { + variable_amount: VARIABLE_AMOUNT, + window, + metadata: bounded_vec![], + }; + let billed_amount_1 = service_contract.calculate_bill_cost_tft(bill).unwrap(); + + assert_eq!(2500000000 - consumer_balance, billed_amount_1); + + // Bill a second time, 1h10min after first billing + run_to_block(901, Some(&mut pool_state)); + assert_ok!(SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + VARIABLE_AMOUNT, + b"bill_metadata".to_vec(), + )); + + let service_contract = SmartContractModule::service_contracts(1).unwrap(); + assert_eq!( + service_contract.last_bill, + get_timestamp_in_seconds_for_block(901) + ); + + let consumer_balance = Balances::free_balance(&consumer_twin.account_id); + let bill = types::ServiceContractBill { + variable_amount: VARIABLE_AMOUNT, + window: 3600, // force a 1h bill here + metadata: bounded_vec![], + }; + let billed_amount_2 = service_contract.calculate_bill_cost_tft(bill).unwrap(); + + // Check that second billing was equivalent to a 1h + // billing even if window is greater than 1h + assert_eq!( + 2500000000 - consumer_balance - billed_amount_1, + billed_amount_2 + ); + }); +} + +#[test] +fn test_service_contract_bill_by_unauthorized_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_bill( + Origin::signed(bob()), + 1, + VARIABLE_AMOUNT, + b"bill_metadata".to_vec(), + ), + Error::::TwinNotAuthorized + ); + }); +} + +#[test] +fn test_service_contract_bill_not_approved_fails() { + new_test_ext().execute_with(|| { + prepare_service_consumer_contract(); + + assert_noop!( + SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + VARIABLE_AMOUNT, + b"bill_metadata".to_vec(), + ), + Error::::ServiceContractBillingNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_bill_variable_amount_too_high_fails() { + let (mut ext, mut pool_state) = new_test_ext_with_pool_state(0); + ext.execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + // Bill 1h after contract approval + run_to_block(601, Some(&mut pool_state)); + // set variable amount a bit higher than variable fee to trigger error + let variable_amount = VARIABLE_FEE + 1; + assert_noop!( + SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + variable_amount, + b"bill_metadata".to_vec(), + ), + Error::::ServiceContractBillingNotAllowed + ); + }); +} + +#[test] +fn test_service_contract_bill_metadata_too_long_fails() { + let (mut ext, mut pool_state) = new_test_ext_with_pool_state(0); + ext.execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + // Bill 1h after contract approval + run_to_block(601, Some(&mut pool_state)); + assert_noop!( + SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + VARIABLE_AMOUNT, + b"very_loooooooooooooooooooooooooooooooooooooooooooooooooong_metadata".to_vec(), + ), + Error::::ServiceContractBillMetadataTooLong + ); + }); +} + +#[test] +fn test_service_contract_bill_out_of_funds_fails() { + let (mut ext, mut pool_state) = new_test_ext_with_pool_state(0); + ext.execute_with(|| { + run_to_block(1, None); + prepare_service_consumer_contract(); + approve_service_consumer_contract(); + + // Drain consumer account + let consumer_twin = TfgridModule::twins(2).unwrap(); + let consumer_balance = Balances::free_balance(&consumer_twin.account_id); + Balances::transfer(Origin::signed(bob()), alice(), consumer_balance).unwrap(); + let consumer_balance = Balances::free_balance(&consumer_twin.account_id); + assert_eq!(consumer_balance, 0); + + // Bill 1h after contract approval + run_to_block(601, Some(&mut pool_state)); + assert_noop!( + SmartContractModule::service_contract_bill( + Origin::signed(alice()), + 1, + VARIABLE_AMOUNT, + b"bill_metadata".to_vec(), + ), + Error::::ServiceContractNotEnoughFundsToPayBill, + ); + }); +} + // MODULE FUNCTION TESTS // // ---------------------- // @@ -3601,7 +4184,7 @@ fn push_nru_report_for_contract(contract_id: u64, block_number: u64) { consumption_reports.push(super::types::NruConsumption { contract_id, nru: 3 * gigabyte, - timestamp: 1628082000 + (6 * block_number), + timestamp: get_timestamp_in_seconds_for_block(block_number), window: 6 * block_number, }); @@ -3621,7 +4204,7 @@ fn check_report_cost( let contract_bill_event = types::ContractBill { contract_id, - timestamp: 1628082000 + (6 * block_number), + timestamp: get_timestamp_in_seconds_for_block(block_number), discount_level, amount_billed: amount_billed as u128, }; @@ -4276,3 +4859,64 @@ fn prepare_farm_three_nodes_three_capacity_reservation_contracts() { assert_eq!(SmartContractModule::active_node_contracts(1).len(), 2); assert_eq!(SmartContractModule::active_node_contracts(2).len(), 1); } + +fn create_service_consumer_contract() { + create_twin(alice()); + create_twin(bob()); + + // create contract between service (Alice) and consumer (Bob) + assert_ok!(SmartContractModule::service_contract_create( + Origin::signed(alice()), + alice(), + bob(), + )); +} + +fn prepare_service_consumer_contract() { + create_service_consumer_contract(); + + assert_ok!(SmartContractModule::service_contract_set_metadata( + Origin::signed(alice()), + 1, + b"some_metadata".to_vec(), + )); + + assert_ok!(SmartContractModule::service_contract_set_fees( + Origin::signed(alice()), + 1, + BASE_FEE, + VARIABLE_FEE, + )); +} + +fn approve_service_consumer_contract() { + // Service approves + assert_ok!(SmartContractModule::service_contract_approve( + Origin::signed(alice()), + 1, + )); + // Consumer approves + assert_ok!(SmartContractModule::service_contract_approve( + Origin::signed(bob()), + 1, + )); +} + +fn get_service_contract() -> types::ServiceContract { + types::ServiceContract:: { + service_twin_id: 1, //Alice + consumer_twin_id: 2, //Bob + base_fee: 0, + variable_fee: 0, + metadata: bounded_vec![], + accepted_by_service: false, + accepted_by_consumer: false, + last_bill: 0, + state: types::ServiceContractState::Created, + phantom: PhantomData, + } +} + +fn get_timestamp_in_seconds_for_block(block_number: u64) -> u64 { + 1628082000 + (6 * block_number) +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index b899dd95f..697dceb2a 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -3,7 +3,8 @@ use crate::pallet::{ }; use crate::Config; use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::{BoundedVec, RuntimeDebugNoBound}; +use core::marker::PhantomData; +use frame_support::{pallet_prelude::ConstU32, BoundedVec, RuntimeDebugNoBound}; use scale_info::TypeInfo; use sp_std::prelude::*; use substrate_fixed::types::U64F64; @@ -252,3 +253,38 @@ pub struct Provider { pub who: AccountId, pub take: u8, } + +pub const MAX_METADATA_LENGTH: u32 = 64; // limited to 64 bytes (2 public keys) +pub const MAX_BILL_METADATA_LENGTH: u32 = 50; // limited to 50 bytes for now + +#[derive(Clone, Eq, PartialEq, RuntimeDebugNoBound, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound())] +pub struct ServiceContract { + pub service_twin_id: u32, + pub consumer_twin_id: u32, + pub base_fee: u64, + pub variable_fee: u64, + pub metadata: BoundedVec>, + pub accepted_by_service: bool, + pub accepted_by_consumer: bool, + pub last_bill: u64, + pub state: ServiceContractState, + pub phantom: PhantomData, +} + +#[derive( + PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, Debug, TypeInfo, MaxEncodedLen, +)] +pub struct ServiceContractBill { + pub variable_amount: u64, // variable amount which is billed + pub window: u64, // amount of time (in seconds) covered since last bill + pub metadata: BoundedVec>, +} + +#[derive(Clone, Eq, PartialEq, RuntimeDebugNoBound, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum ServiceContractState { + Created, + AgreementReady, + ApprovedByBoth, +} From d5bc0244ae7aa1e569e63d1d5137a3541755de24 Mon Sep 17 00:00:00 2001 From: renauter Date: Tue, 29 Nov 2022 19:41:58 -0300 Subject: [PATCH 02/12] feat: add twin id to service contract created event --- substrate-node/pallets/pallet-smart-contract/src/lib.rs | 2 ++ substrate-node/pallets/pallet-smart-contract/src/tests.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 1b44fa2e7..fbcac77a8 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -361,6 +361,7 @@ pub mod pallet { /// A Service contract is created ServiceContractCreated { service_contract_id: u64, + twin_id: u32, }, /// A Service contract is approved ServiceContractApproved { @@ -2363,6 +2364,7 @@ impl Pallet { // Trigger event for service contract creation Self::deposit_event(Event::ServiceContractCreated { service_contract_id: id, + twin_id: caller_twin_id, }); Ok(().into()) diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 9e98e9e33..03ec8bfbe 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -3486,6 +3486,7 @@ fn test_service_contract_create_works() { &record(MockEvent::SmartContractModule( SmartContractEvent::ServiceContractCreated { service_contract_id: 1, + twin_id: 1, } )), ); From 04e8ddfc9a703428cd74af495f39169e1c4ac71e Mon Sep 17 00:00:00 2001 From: renauter Date: Thu, 15 Dec 2022 13:34:08 -0300 Subject: [PATCH 03/12] fix: resolve conflicts --- substrate-node/pallets/pallet-smart-contract/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 5bc74ec8b..d8f4c8127 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -10,6 +10,7 @@ use frame_support::{ Currency, EnsureOrigin, ExistenceRequirement, ExistenceRequirement::KeepAlive, Get, LockableCurrency, OnUnbalanced, WithdrawReasons, }, + transactional, weights::Pays, BoundedVec, }; From 0feaae1df0763d5491eb067d731a50a4bc7ad45a Mon Sep 17 00:00:00 2001 From: renauter Date: Thu, 15 Dec 2022 15:31:28 -0300 Subject: [PATCH 04/12] chore: review --- .../pallets/pallet-smart-contract/src/cost.rs | 13 +++-- .../pallets/pallet-smart-contract/src/lib.rs | 52 +++++++++---------- .../pallet-smart-contract/src/tests.rs | 14 ++--- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index 037e39692..826e0e21b 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -101,7 +101,8 @@ impl Contract { // Calculate total cost for a name contract types::ContractData::NameContract(_) => { // bill user for name usage for number of seconds elapsed - let total_cost_u64f64 = (U64F64::from_num(pricing_policy.unique_name.value) / 3600) + let total_cost_u64f64 = (U64F64::from_num(pricing_policy.unique_name.value) + / U64F64::from_num(crate::SECS_PER_HOUR)) * U64F64::from_num(seconds_elapsed); total_cost_u64f64.to_num::() } @@ -135,7 +136,7 @@ impl ServiceContract { pub fn calculate_bill_cost(&self, service_bill: ServiceContractBill) -> u64 { // bill user for service usage for elpased usage (window) in seconds let contract_cost = U64F64::from_num(self.base_fee) * U64F64::from_num(service_bill.window) - / 3600 + / U64F64::from_num(crate::SECS_PER_HOUR) + U64F64::from_num(service_bill.variable_amount); contract_cost.round().to_num::() } @@ -160,14 +161,16 @@ pub fn calculate_resources_cost( let su_used = hru / 1200 + sru / 200; // the pricing policy su cost value is expressed in 1 hours or 3600 seconds. // we bill every 3600 seconds but here we need to calculate the cost per second and multiply it by the seconds elapsed. - let su_cost = (U64F64::from_num(pricing_policy.su.value) / 3600) + let su_cost = (U64F64::from_num(pricing_policy.su.value) + / U64F64::from_num(crate::SECS_PER_HOUR)) * U64F64::from_num(seconds_elapsed) * su_used; log::debug!("su cost: {:?}", su_cost); let cu = calculate_cu(cru, mru); - let cu_cost = (U64F64::from_num(pricing_policy.cu.value) / 3600) + let cu_cost = (U64F64::from_num(pricing_policy.cu.value) + / U64F64::from_num(crate::SECS_PER_HOUR)) * U64F64::from_num(seconds_elapsed) * cu; log::debug!("cu cost: {:?}", cu_cost); @@ -176,7 +179,7 @@ pub fn calculate_resources_cost( if ipu > 0 { let total_ip_cost = U64F64::from_num(ipu) - * (U64F64::from_num(pricing_policy.ipu.value) / 3600) + * (U64F64::from_num(pricing_policy.ipu.value) / U64F64::from_num(crate::SECS_PER_HOUR)) * U64F64::from_num(seconds_elapsed); log::debug!("ip cost: {:?}", total_ip_cost); total_cost += total_ip_cost; diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index d8f4c8127..cf6fb9999 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -36,6 +36,7 @@ use tfchain_support::{ }; pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"smct"); +pub const SECS_PER_HOUR: u64 = 36000; #[cfg(test)] mod mock; @@ -372,7 +373,8 @@ pub mod pallet { ServiceContractModificationNotAllowed, ServiceContractApprovalNotAllowed, ServiceContractRejectionNotAllowed, - ServiceContractBillingNotAllowed, + ServiceContractBillingNotApprovedByBoth, + ServiceContractBillingVariableAmountTooHigh, ServiceContractBillMetadataTooLong, ServiceContractMetadataTooLong, ServiceContractNotEnoughFundsToPayBill, @@ -579,8 +581,12 @@ pub mod pallet { service_contract_id: u64, ) -> DispatchResultWithPostInfo { let account_id = ensure_signed(origin)?; + + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + Self::_service_contract_cancel( - account_id, + twin_id, service_contract_id, types::Cause::CanceledByUser, ) @@ -1019,7 +1025,7 @@ impl Pallet { // calculate NRU used and the cost let used_nru = U64F64::from_num(report.nru) / pricing_policy.nu.factor_base_1000(); let nu_cost = used_nru - * (U64F64::from_num(pricing_policy.nu.value) / 3600) + * (U64F64::from_num(pricing_policy.nu.value) / U64F64::from_num(SECS_PER_HOUR)) * U64F64::from_num(seconds_elapsed); log::info!("nu cost: {:?}", nu_cost); @@ -2016,6 +2022,13 @@ impl Pallet { let service_contract = ServiceContracts::::get(service_contract_id) .ok_or(Error::::ServiceContractNotExists)?; + // Only service or consumer can reject agreement + ensure!( + twin_id == service_contract.service_twin_id + || twin_id == service_contract.consumer_twin_id, + Error::::TwinNotAuthorized, + ); + // Allow to reject contract only if agreement is ready ensure!( matches!( @@ -2025,32 +2038,18 @@ impl Pallet { Error::::ServiceContractRejectionNotAllowed, ); - // Only service or consumer can reject agreement - ensure!( - twin_id == service_contract.service_twin_id - || twin_id == service_contract.consumer_twin_id, - Error::::TwinNotAuthorized, - ); - // If one party (service or consumer) rejects agreement // then contract is canceled and removed from service contract map - Self::_service_contract_cancel( - account_id, - service_contract_id, - types::Cause::CanceledByUser, - )?; + Self::_service_contract_cancel(twin_id, service_contract_id, types::Cause::CanceledByUser)?; Ok(().into()) } pub fn _service_contract_cancel( - account_id: T::AccountId, + twin_id: u32, service_contract_id: u64, cause: types::Cause, ) -> DispatchResultWithPostInfo { - let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) - .ok_or(Error::::TwinNotExists)?; - let service_contract = ServiceContracts::::get(service_contract_id) .ok_or(Error::::ServiceContractNotExists)?; @@ -2103,7 +2102,7 @@ impl Pallet { service_contract.state, types::ServiceContractState::ApprovedByBoth ), - Error::::ServiceContractBillingNotAllowed, + Error::::ServiceContractBillingNotApprovedByBoth, ); // Get elapsed time (in seconds) to bill for service @@ -2113,16 +2112,16 @@ impl Pallet { // Billing time (window) is max 1h by design // So extra time will not be billed // It is the service responsability to bill on right frequency - let window = elapsed_seconds_since_last_bill.min(3600); + let window = elapsed_seconds_since_last_bill.min(SECS_PER_HOUR); // Billing variable amount is bounded by contract variable fee ensure!( variable_amount - <= ((U64F64::from_num(window) / 3600) + <= ((U64F64::from_num(window) / U64F64::from_num(SECS_PER_HOUR)) * U64F64::from_num(service_contract.variable_fee)) .round() .to_num::(), - Error::::ServiceContractBillingNotAllowed, + Error::::ServiceContractBillingVariableAmountTooHigh, ); let bill_metadata = BoundedVec::try_from(metadata) @@ -2155,8 +2154,9 @@ impl Pallet { .ok_or(Error::::ServiceContractNotExists)?; let amount_due = service_contract.calculate_bill_cost_tft(bill)?; - let service_twin = pallet_tfgrid::Twins::::get(service_contract.service_twin_id) - .ok_or(Error::::TwinNotExists)?; + let service_twin_id = service_contract.service_twin_id; + let service_twin = + pallet_tfgrid::Twins::::get(service_twin_id).ok_or(Error::::TwinNotExists)?; let consumer_twin = pallet_tfgrid::Twins::::get(service_contract.consumer_twin_id) .ok_or(Error::::TwinNotExists)?; @@ -2167,7 +2167,7 @@ impl Pallet { // by service and removed from service contract map if usable_balance < amount_due { Self::_service_contract_cancel( - service_twin.account_id, + service_twin_id, service_contract_id, types::Cause::OutOfFunds, )?; diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 4dfb63af1..6555509c0 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -77,14 +77,14 @@ fn test_create_node_contract_with_public_ips_works() { assert_eq!(c.public_ips, 2); let pub_ip = PublicIP { - ip: get_public_ip_ip_input(b"185.206.122.33/24"), - gateway: get_public_ip_gw_input(b"185.206.122.1"), + ip: get_public_ip_ip_input(b"185.206.122.33/24"), + gateway: get_public_ip_gw_input(b"185.206.122.1"), contract_id: 1, }; let pub_ip_2 = PublicIP { - ip: get_public_ip_ip_input(b"185.206.122.34/24"), - gateway: get_public_ip_gw_input(b"185.206.122.1"), + ip: get_public_ip_ip_input(b"185.206.122.34/24"), + gateway: get_public_ip_gw_input(b"185.206.122.1"), contract_id: 1, }; assert_eq!(c.public_ips_list[0], pub_ip); @@ -2829,7 +2829,7 @@ fn test_service_contract_bill_works() { let consumer_balance = Balances::free_balance(&consumer_twin.account_id); let bill = types::ServiceContractBill { variable_amount: VARIABLE_AMOUNT, - window: 3600, // force a 1h bill here + window: crate::SECS_PER_HOUR, // force a 1h bill here metadata: bounded_vec![], }; let billed_amount_2 = service_contract.calculate_bill_cost_tft(bill).unwrap(); @@ -2873,7 +2873,7 @@ fn test_service_contract_bill_not_approved_fails() { VARIABLE_AMOUNT, b"bill_metadata".to_vec(), ), - Error::::ServiceContractBillingNotAllowed + Error::::ServiceContractBillingNotApprovedByBoth ); }); } @@ -2897,7 +2897,7 @@ fn test_service_contract_bill_variable_amount_too_high_fails() { variable_amount, b"bill_metadata".to_vec(), ), - Error::::ServiceContractBillingNotAllowed + Error::::ServiceContractBillingVariableAmountTooHigh ); }); } From 5ffdb12d9e462e002eb2e4b07d52d89620ce86f5 Mon Sep 17 00:00:00 2001 From: renauter Date: Thu, 15 Dec 2022 16:20:37 -0300 Subject: [PATCH 05/12] fix: 1h = 3600 sec --- substrate-node/pallets/pallet-smart-contract/src/lib.rs | 2 +- substrate-node/pallets/pallet-smart-contract/src/tests.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index cf6fb9999..f774b224e 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -36,7 +36,7 @@ use tfchain_support::{ }; pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"smct"); -pub const SECS_PER_HOUR: u64 = 36000; +pub const SECS_PER_HOUR: u64 = 3600; #[cfg(test)] mod mock; diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 6555509c0..09f8cb71a 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -2275,6 +2275,7 @@ fn test_rent_contract_and_node_contract_canceled_when_node_is_deleted_works() { // SOLUTION PROVIDER TESTS // // ------------------------ // + #[test] fn test_create_solution_provider_works() { new_test_ext().execute_with(|| { From 00cfbb30bc06f7cc50c248fc0b2441311410b21d Mon Sep 17 00:00:00 2001 From: renauter Date: Fri, 16 Dec 2022 13:15:31 -0300 Subject: [PATCH 06/12] chore: move generic from service contract struct to function --- .../pallets/pallet-smart-contract/src/cost.rs | 4 ++-- .../pallets/pallet-smart-contract/src/lib.rs | 6 ++---- .../pallet-smart-contract/src/tests.rs | 21 +++++++++---------- .../pallet-smart-contract/src/types.rs | 9 ++------ 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index 826e0e21b..c37a78f41 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -112,8 +112,8 @@ impl Contract { } } -impl ServiceContract { - pub fn calculate_bill_cost_tft( +impl ServiceContract { + pub fn calculate_bill_cost_tft( &self, service_bill: ServiceContractBill, ) -> Result, DispatchErrorWithPostInfo> { diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index f774b224e..237f5e6c1 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -1,6 +1,5 @@ #![cfg_attr(not(feature = "std"), no_std)] -use core::marker::PhantomData; use frame_support::{ dispatch::DispatchErrorWithPostInfo, ensure, @@ -204,7 +203,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn service_contracts)] pub type ServiceContracts = - StorageMap<_, Blake2_128Concat, u64, ServiceContract, OptionQuery>; + StorageMap<_, Blake2_128Concat, u64, ServiceContract, OptionQuery>; #[pallet::storage] #[pallet::getter(fn service_contract_id)] @@ -1860,7 +1859,6 @@ impl Pallet { accepted_by_consumer: false, last_bill: 0, state: types::ServiceContractState::Created, - phantom: PhantomData, }; // Get the service contract ID map and increment @@ -2152,7 +2150,7 @@ impl Pallet { ) -> DispatchResultWithPostInfo { let service_contract = ServiceContracts::::get(service_contract_id) .ok_or(Error::::ServiceContractNotExists)?; - let amount_due = service_contract.calculate_bill_cost_tft(bill)?; + let amount_due = service_contract.calculate_bill_cost_tft::(bill)?; let service_twin_id = service_contract.service_twin_id; let service_twin = diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 09f8cb71a..cfa0232b9 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -16,15 +16,11 @@ use pallet_tfgrid::{ }; use sp_core::H256; use sp_runtime::{assert_eq_error_rate, traits::SaturatedConversion, Perbill, Percent}; -use sp_std::{ - convert::{TryFrom, TryInto}, - marker::PhantomData, -}; +use sp_std::convert::{TryFrom, TryInto}; use substrate_fixed::types::U64F64; -use tfchain_support::types::IP4; use tfchain_support::{ resources::Resources, - types::{FarmCertification, NodeCertification, PublicIP}, + types::{FarmCertification, NodeCertification, PublicIP, IP4}, }; const GIGABYTE: u64 = 1024 * 1024 * 1024; @@ -2808,7 +2804,9 @@ fn test_service_contract_bill_works() { window, metadata: bounded_vec![], }; - let billed_amount_1 = service_contract.calculate_bill_cost_tft(bill).unwrap(); + let billed_amount_1 = service_contract + .calculate_bill_cost_tft::(bill) + .unwrap(); assert_eq!(2500000000 - consumer_balance, billed_amount_1); @@ -2833,7 +2831,9 @@ fn test_service_contract_bill_works() { window: crate::SECS_PER_HOUR, // force a 1h bill here metadata: bounded_vec![], }; - let billed_amount_2 = service_contract.calculate_bill_cost_tft(bill).unwrap(); + let billed_amount_2 = service_contract + .calculate_bill_cost_tft::(bill) + .unwrap(); // Check that second billing was equivalent to a 1h // billing even if window is greater than 1h @@ -3458,8 +3458,8 @@ fn approve_service_consumer_contract() { )); } -fn get_service_contract() -> types::ServiceContract { - types::ServiceContract:: { +fn get_service_contract() -> types::ServiceContract { + types::ServiceContract { service_twin_id: 1, //Alice consumer_twin_id: 2, //Bob base_fee: 0, @@ -3469,7 +3469,6 @@ fn get_service_contract() -> types::ServiceContract { accepted_by_consumer: false, last_bill: 0, state: types::ServiceContractState::Created, - phantom: PhantomData, } } diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index 2c231e618..d58ef4d32 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -1,15 +1,11 @@ use crate::pallet::{MaxDeploymentDataLength, MaxNodeContractPublicIPs}; use crate::Config; use codec::{Decode, Encode, MaxEncodedLen}; -use core::marker::PhantomData; use frame_support::{pallet_prelude::ConstU32, BoundedVec, RuntimeDebugNoBound}; use scale_info::TypeInfo; use sp_std::prelude::*; use substrate_fixed::types::U64F64; -use tfchain_support::{ - resources::Resources, - types::PublicIP, -}; +use tfchain_support::{resources::Resources, types::PublicIP}; pub type BlockNumber = u64; @@ -237,7 +233,7 @@ pub const MAX_BILL_METADATA_LENGTH: u32 = 50; // limited to 50 bytes for now #[derive(Clone, Eq, PartialEq, RuntimeDebugNoBound, Encode, Decode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] #[codec(mel_bound())] -pub struct ServiceContract { +pub struct ServiceContract { pub service_twin_id: u32, pub consumer_twin_id: u32, pub base_fee: u64, @@ -247,7 +243,6 @@ pub struct ServiceContract { pub accepted_by_consumer: bool, pub last_bill: u64, pub state: ServiceContractState, - pub phantom: PhantomData, } #[derive( From 74bda342e1b468b67d064bd04c8f48cfe407ad39 Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 09:31:40 -0300 Subject: [PATCH 07/12] fix: resolve conflicts --- substrate-node/pallets/pallet-smart-contract/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index dc682729f..0544e1b7f 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -609,6 +609,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let account_id = ensure_signed(origin)?; Self::_service_contract_bill(account_id, service_contract_id, variable_amount, metadata) + } #[pallet::weight(10_000 + T::DbWeight::get().writes(1) + T::DbWeight::get().reads(1))] pub fn change_billing_frequency( @@ -2207,6 +2208,9 @@ impl Pallet { service_contract_id, ); + Ok(().into()) + } + pub fn _change_billing_frequency(frequency: u64) -> DispatchResultWithPostInfo { let billing_frequency = BillingFrequency::::get(); ensure!( From 0da42a4617309ceac7a4b528fb991c1fad211b3c Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 09:42:00 -0300 Subject: [PATCH 08/12] feat: emit the full service contract object on creation --- .../pallets/pallet-smart-contract/src/lib.rs | 10 ++-------- .../pallets/pallet-smart-contract/src/tests.rs | 8 +++----- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 0544e1b7f..8b9ff5868 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -320,10 +320,7 @@ pub mod pallet { SolutionProviderCreated(types::SolutionProvider), SolutionProviderApproved(u64, bool), /// A Service contract is created - ServiceContractCreated { - service_contract_id: u64, - twin_id: u32, - }, + ServiceContractCreated(types::ServiceContract), /// A Service contract is approved ServiceContractApproved { service_contract_id: u64, @@ -1893,10 +1890,7 @@ impl Pallet { ServiceContractID::::put(id); // Trigger event for service contract creation - Self::deposit_event(Event::ServiceContractCreated { - service_contract_id: id, - twin_id: caller_twin_id, - }); + Self::deposit_event(Event::ServiceContractCreated(service_contract)); Ok(().into()) } diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index bc52e56c6..4663f969b 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -2381,8 +2381,9 @@ fn test_service_contract_create_works() { run_to_block(1, None); create_service_consumer_contract(); + let service_contract = get_service_contract(); assert_eq!( - get_service_contract(), + service_contract, SmartContractModule::service_contracts(1).unwrap(), ); @@ -2391,10 +2392,7 @@ fn test_service_contract_create_works() { assert_eq!( our_events.last().unwrap(), &record(MockEvent::SmartContractModule( - SmartContractEvent::ServiceContractCreated { - service_contract_id: 1, - twin_id: 1, - } + SmartContractEvent::ServiceContractCreated(service_contract) )), ); }); From 1fe130e9e7a69f0bb7e7d67ce1df7f67bb8cb3d1 Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 10:41:24 -0300 Subject: [PATCH 09/12] feat: add event for service contract billing --- .../pallets/pallet-smart-contract/src/lib.rs | 23 +++++++-- .../pallet-smart-contract/src/tests.rs | 50 +++++++++++++++---- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 8b9ff5868..d582e05d4 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -330,6 +330,12 @@ pub mod pallet { service_contract_id: u64, cause: types::Cause, }, + /// A Service contract is billed + ServiceContractBilled { + service_contract_id: u64, + bill: types::ServiceContractBill, + amount: BalanceOf, + }, BillingFrequencyChanged(u64), } @@ -2011,6 +2017,8 @@ impl Pallet { if service_contract.accepted_by_service && service_contract.accepted_by_consumer { // Change contract state to approved and emit event service_contract.state = types::ServiceContractState::ApprovedByBoth; + + // Trigger event for service contract approval Self::deposit_event(Event::ServiceContractApproved { service_contract_id, }); @@ -2077,6 +2085,8 @@ impl Pallet { // Can be done at any state of contract // so no need to handle state validation ServiceContracts::::remove(service_contract_id); + + // Trigger event for service contract cancelation Self::deposit_event(Event::ServiceContractCanceled { service_contract_id, cause, @@ -2165,7 +2175,7 @@ impl Pallet { ) -> DispatchResultWithPostInfo { let service_contract = ServiceContracts::::get(service_contract_id) .ok_or(Error::::ServiceContractNotExists)?; - let amount_due = service_contract.calculate_bill_cost_tft::(bill)?; + let amount = service_contract.calculate_bill_cost_tft::(bill.clone())?; let service_twin_id = service_contract.service_twin_id; let service_twin = @@ -2178,7 +2188,7 @@ impl Pallet { // If consumer is out of funds then contract is canceled // by service and removed from service contract map - if usable_balance < amount_due { + if usable_balance < amount { Self::_service_contract_cancel( service_twin_id, service_contract_id, @@ -2193,10 +2203,17 @@ impl Pallet { ::Currency::transfer( &consumer_twin.account_id, &service_twin.account_id, - amount_due, + amount, ExistenceRequirement::KeepAlive, )?; + // Trigger event for service contract billing + Self::deposit_event(Event::ServiceContractBilled { + service_contract_id, + bill, + amount, + }); + log::info!( "bill successfully payed by consumer for service contract with id {:?}", service_contract_id, diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 4663f969b..5e78e3688 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -2785,7 +2785,7 @@ fn test_service_contract_bill_works() { Origin::signed(alice()), 1, VARIABLE_AMOUNT, - b"bill_metadata".to_vec(), + b"bill_metadata_1".to_vec(), )); let service_contract = SmartContractModule::service_contracts(1).unwrap(); @@ -2794,27 +2794,41 @@ fn test_service_contract_bill_works() { get_timestamp_in_seconds_for_block(201) ); + // Check consumer balance after first billing let consumer_balance = Balances::free_balance(&consumer_twin.account_id); let window = get_timestamp_in_seconds_for_block(201) - get_timestamp_in_seconds_for_block(1); - let bill = types::ServiceContractBill { + let bill_1 = types::ServiceContractBill { variable_amount: VARIABLE_AMOUNT, window, - metadata: bounded_vec![], + metadata: BoundedVec::try_from(b"bill_metadata_1".to_vec()).unwrap(), }; let billed_amount_1 = service_contract - .calculate_bill_cost_tft::(bill) + .calculate_bill_cost_tft::(bill_1.clone()) .unwrap(); - assert_eq!(2500000000 - consumer_balance, billed_amount_1); + // Check event triggering + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule(SmartContractEvent::< + TestRuntime, + >::ServiceContractBilled { + service_contract_id: 1, + bill: bill_1, + amount: billed_amount_1, + })), + ); + // Bill a second time, 1h10min after first billing run_to_block(901, Some(&mut pool_state)); assert_ok!(SmartContractModule::service_contract_bill( Origin::signed(alice()), 1, VARIABLE_AMOUNT, - b"bill_metadata".to_vec(), + b"bill_metadata_2".to_vec(), )); let service_contract = SmartContractModule::service_contracts(1).unwrap(); @@ -2823,22 +2837,36 @@ fn test_service_contract_bill_works() { get_timestamp_in_seconds_for_block(901) ); + // Check consumer balance after second billing let consumer_balance = Balances::free_balance(&consumer_twin.account_id); - let bill = types::ServiceContractBill { + let bill_2 = types::ServiceContractBill { variable_amount: VARIABLE_AMOUNT, window: crate::SECS_PER_HOUR, // force a 1h bill here - metadata: bounded_vec![], + metadata: BoundedVec::try_from(b"bill_metadata_2".to_vec()).unwrap(), }; let billed_amount_2 = service_contract - .calculate_bill_cost_tft::(bill) + .calculate_bill_cost_tft::(bill_2.clone()) .unwrap(); - - // Check that second billing was equivalent to a 1h + // Second billing was equivalent to a 1h // billing even if window is greater than 1h assert_eq!( 2500000000 - consumer_balance - billed_amount_1, billed_amount_2 ); + + // Check event triggering + let our_events = System::events(); + assert_eq!(!our_events.is_empty(), true); + assert_eq!( + our_events.last().unwrap(), + &record(MockEvent::SmartContractModule(SmartContractEvent::< + TestRuntime, + >::ServiceContractBilled { + service_contract_id: 1, + bill: bill_2, + amount: billed_amount_2, + })), + ); }); } From 43550f3dd74679037021d70976cd1b9cb6bd9a5d Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 12:21:38 -0300 Subject: [PATCH 10/12] feat: add service contract ADR and specs --- .../0003-third-party-service-contract.md | 25 +++++++++ .../third-party_service_contract.md | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 docs/architecture/0003-third-party-service-contract.md create mode 100644 substrate-node/pallets/pallet-smart-contract/third-party_service_contract.md diff --git a/docs/architecture/0003-third-party-service-contract.md b/docs/architecture/0003-third-party-service-contract.md new file mode 100644 index 000000000..6329d0078 --- /dev/null +++ b/docs/architecture/0003-third-party-service-contract.md @@ -0,0 +1,25 @@ +# 3. Third party service contract + +Date: 2022-10-17 + +## Status + +Accepted + +## Context + +See [here](https://github.com/threefoldtech/tfchain/issues/445) for more details. + +## Decision + +The third party service contract flow is described [here](../../substrate-node/pallets/pallet-smart-contract/third-party_service_contract.md#flow). + +## Consequences + +### the good + +- It is now possible to create generic contract between two `TFChain` users (without restriction of account type) for some service and bill for it. + +### the worrying + +- Keep eyes on potential abuses and be prepared to handle all the malicious cases that can show up. \ No newline at end of file diff --git a/substrate-node/pallets/pallet-smart-contract/third-party_service_contract.md b/substrate-node/pallets/pallet-smart-contract/third-party_service_contract.md new file mode 100644 index 000000000..01889170b --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/third-party_service_contract.md @@ -0,0 +1,56 @@ +# Third party service contract + +Since we don't want to do work on the chain for every possible 3rd party service, we will keep this as generic as possible. While custom implementations for services might offer small advantages in the flow, the extra effort to develop and most importantly maintain these implementations is not worth it compared to a proper generic flow which would technically also be reusable. + +## Contract structure + +A contract will work simple client - server principle (i.e. the "buyer" and the "seller", a "consumer" of a service and one who "offers" the service). Both parties are identified by a twin id to fit in the current flow (we could use a generic address as well here). Contract is laid out as such + +- consumer twin id +- service twin id +- base fee, this is the fixed amount which will be billed hourly +- variable fee, this is the maximum amount which can be billed on top of the base fee (for variable consumption metrics, to be defined by the service) +- metadata, a field which just holds some bytes. The service can use this any way it likes (including having stuff set by the user). We limit this field to some size now, suggested 64 bytes (2 public keys generally) + +Additionally, we also keep track of some metadata, which will be: + +- accepted by consumer +- accepted by service +- last bill received (we keep track of this to make sure the service does not send overlapping bills) + +## Billing + +Once a contract is accepted by both the consumer and the service, the chain can start accepting "bill reports" from the service for the contract. Only the twin of the service can send these, as specified in the contract. The bill contains the following: + +- Variable amount which is billed. The chain checks that this is less than or equal to the variable amount as specified in the contract, if it is higher the bill is rejected for overcharging. Technically the service could send a report every second to drain the user. To protect against this, the max variable amount in the contract is interpreted as being "per hour", and the value set in the contract is divided by 3600, multiplied by window size, to find the actual maximum which can be billed by the contract. +- Window, this is the amount of time (in seconds) covered since last contract. The chain verifies that `current time - window >= contract.last_bill`, such that no bills overlap to avoid overcharging. Combined with the previous limit to variable amount this prevents the service from overcharging the user. +- Some optional metadata, this will again just be some bytes (the service decides how this can be interpreted). For now we'll limit this to 50 bytes or so. + +## Chain calls + +### Callable by anyone + +- `create_contract(consumer twin ID, service twin ID)`: Creates the contract and sets the id's. Base fee and variable fee are left at 0 + +### Callable by consumer or service + +- `set_metadata(data)`: Sets the custom metadata on the contract. This can be done by either the client of the service, depending on how it is interpreted (as specified by the service). For now, we will assume that setting metadata is a one off operation. As a result, if metadata is already present when this is called, an error is thrown (i.e. only the first call of this function can succeed). + +### Callable by service + +- `set_fees(base, variable)`: Sets the base fee and variable fee on the contract +- `reject_by_service()`: Rejects the contract, deleting it. +- `approve_by_service()`: Sets the `service_accepted` flag on the contract. After this, no more modifications to fees or metadata can be done + +### Callable by user + +- `reject_by_consumer()`: Rejects the contract, deleting it. +- `approve_by_consumer()`: Sets the `consumer_accepted` flag on the contract. After this, no more modifications to fees or metadata can be done + +## Flow + +We start of by creating a contract. This can technically be done by anyone, but in practice will likely end up being done by either the service or the consumer (depending on what the service expects). This will be followed by the service or consumer setting the metadata (again depending on how the service expects things to be), and the service setting a base fee + variable fee. Note that part of the communication can and should be off chain, the contract is only the finalized agreement. When the fees and metadata are set, both the consumer and service need to explicitly approve the contract, setting the appropriate flag on the contract. Note that as soon as either party accepted (i.e. either flag is set), the fees and metadata cannot be changed anymore. It is technically possible for consumers to accept a contract as soon as it is created, thereby not giving the service a chance to set the fees. Though this basically means the contract is invalid and the service should just outright reject it. + +Once the contract is accepted by both the consumer and the service, it can be billed (i.e. bills send before both flags are set must be rejected). Because a service should not charge the user if it doesn't work, we will require that bills be send every hour, by limiting the window size to 3600. Anything with a bigger window is rejected. This way if the service is down (for some longer period), it for sure can't bill for the time it was down. When the bill is received, the chain calculates `contract.base_fee * bill.window / 3600 + variable fee` (keeping in mind the constraint for variable fee as outlined above), and this amount is transferred from the consumer twin account to the service twin account. + +We will not implement a grace period for this right now, as the service should define on an individual basis how this is handled. If needed in the future this can of course change. From e2a5be1d3676bec361698d2e77115d49e2fb6be2 Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 17:54:22 -0300 Subject: [PATCH 11/12] feat: add service contract id field into struct --- substrate-node/pallets/pallet-smart-contract/src/lib.rs | 9 +++++---- .../pallets/pallet-smart-contract/src/tests.rs | 1 + .../pallets/pallet-smart-contract/src/types.rs | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index c5fdb3cb1..d674d21ec 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -1873,8 +1873,13 @@ impl Pallet { Error::::ServiceContractCreationNotAllowed, ); + // Get the service contract ID map and increment + let mut id = ServiceContractID::::get(); + id = id + 1; + // Create service contract let service_contract = types::ServiceContract { + service_contract_id: id, service_twin_id, consumer_twin_id, base_fee: 0, @@ -1886,10 +1891,6 @@ impl Pallet { state: types::ServiceContractState::Created, }; - // Get the service contract ID map and increment - let mut id = ServiceContractID::::get(); - id = id + 1; - // Insert into service contract map ServiceContracts::::insert(id, &service_contract); diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 5e78e3688..c2b023570 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -3525,6 +3525,7 @@ fn approve_service_consumer_contract() { fn get_service_contract() -> types::ServiceContract { types::ServiceContract { + service_contract_id: 1, service_twin_id: 1, //Alice consumer_twin_id: 2, //Bob base_fee: 0, diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index d58ef4d32..e7f2fb351 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -234,6 +234,7 @@ pub const MAX_BILL_METADATA_LENGTH: u32 = 50; // limited to 50 bytes for now #[scale_info(skip_type_params(T))] #[codec(mel_bound())] pub struct ServiceContract { + pub service_contract_id: u64, pub service_twin_id: u32, pub consumer_twin_id: u32, pub base_fee: u64, From 70bfb6007789076d5e4287acfcb919d7a94ec22a Mon Sep 17 00:00:00 2001 From: renauter Date: Mon, 19 Dec 2022 19:55:57 -0300 Subject: [PATCH 12/12] feat: add BillingReferencePeriod config parameter type --- .../pallets/pallet-smart-contract/src/cost.rs | 20 +++++++++---------- .../pallets/pallet-smart-contract/src/lib.rs | 11 ++++++---- .../pallets/pallet-smart-contract/src/mock.rs | 20 ++++++++++--------- .../pallet-smart-contract/src/tests.rs | 3 ++- substrate-node/runtime/src/constants.rs | 20 ++----------------- substrate-node/runtime/src/lib.rs | 18 +++-------------- substrate-node/support/src/constants.rs | 9 ++++++++- 7 files changed, 43 insertions(+), 58 deletions(-) diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index c37a78f41..d7270801e 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -4,10 +4,9 @@ use crate::pallet::Error; use crate::types; use crate::types::{Contract, ContractBillingInformation, ServiceContract, ServiceContractBill}; use crate::Config; -use frame_support::dispatch::DispatchErrorWithPostInfo; +use frame_support::{dispatch::DispatchErrorWithPostInfo, traits::Get}; use pallet_tfgrid::types as pallet_tfgrid_types; -use sp_runtime::Percent; -use sp_runtime::SaturatedConversion; +use sp_runtime::{Percent, SaturatedConversion}; use substrate_fixed::types::U64F64; use tfchain_support::{resources::Resources, types::NodeCertification}; @@ -102,7 +101,7 @@ impl Contract { types::ContractData::NameContract(_) => { // bill user for name usage for number of seconds elapsed let total_cost_u64f64 = (U64F64::from_num(pricing_policy.unique_name.value) - / U64F64::from_num(crate::SECS_PER_HOUR)) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed); total_cost_u64f64.to_num::() } @@ -118,7 +117,7 @@ impl ServiceContract { service_bill: ServiceContractBill, ) -> Result, DispatchErrorWithPostInfo> { // Calculate the cost in mUSD for service contract bill - let total_cost = self.calculate_bill_cost(service_bill); + let total_cost = self.calculate_bill_cost::(service_bill); if total_cost == 0 { return Ok(BalanceOf::::saturated_from(0 as u128)); @@ -133,10 +132,10 @@ impl ServiceContract { return Ok(amount_due); } - pub fn calculate_bill_cost(&self, service_bill: ServiceContractBill) -> u64 { + pub fn calculate_bill_cost(&self, service_bill: ServiceContractBill) -> u64 { // bill user for service usage for elpased usage (window) in seconds let contract_cost = U64F64::from_num(self.base_fee) * U64F64::from_num(service_bill.window) - / U64F64::from_num(crate::SECS_PER_HOUR) + / U64F64::from_num(T::BillingReferencePeriod::get()) + U64F64::from_num(service_bill.variable_amount); contract_cost.round().to_num::() } @@ -162,7 +161,7 @@ pub fn calculate_resources_cost( // the pricing policy su cost value is expressed in 1 hours or 3600 seconds. // we bill every 3600 seconds but here we need to calculate the cost per second and multiply it by the seconds elapsed. let su_cost = (U64F64::from_num(pricing_policy.su.value) - / U64F64::from_num(crate::SECS_PER_HOUR)) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed) * su_used; log::debug!("su cost: {:?}", su_cost); @@ -170,7 +169,7 @@ pub fn calculate_resources_cost( let cu = calculate_cu(cru, mru); let cu_cost = (U64F64::from_num(pricing_policy.cu.value) - / U64F64::from_num(crate::SECS_PER_HOUR)) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed) * cu; log::debug!("cu cost: {:?}", cu_cost); @@ -179,7 +178,8 @@ pub fn calculate_resources_cost( if ipu > 0 { let total_ip_cost = U64F64::from_num(ipu) - * (U64F64::from_num(pricing_policy.ipu.value) / U64F64::from_num(crate::SECS_PER_HOUR)) + * (U64F64::from_num(pricing_policy.ipu.value) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed); log::debug!("ip cost: {:?}", total_ip_cost); total_cost += total_ip_cost; diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index d674d21ec..1fd8ce53b 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -38,7 +38,6 @@ use tfchain_support::{ }; pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); -pub const SECS_PER_HOUR: u64 = 3600; #[cfg(test)] mod mock; @@ -122,6 +121,7 @@ pub mod pallet { // Version constant that referenced the struct version pub const CONTRACT_VERSION: u32 = 4; + pub type BillingReferencePeriod = ::BillingReferencePeriod; pub type MaxNodeContractPublicIPs = ::MaxNodeContractPublicIps; pub type MaxDeploymentDataLength = ::MaxDeploymentDataLength; pub type DeploymentDataInput = BoundedVec>; @@ -228,6 +228,7 @@ pub mod pallet { type Burn: OnUnbalanced>; type StakingPoolAccount: Get; type BillingFrequency: Get; + type BillingReferencePeriod: Get; type DistributionFrequency: Get; type GracePeriod: Get; type WeightInfo: WeightInfo; @@ -1045,7 +1046,8 @@ impl Pallet { // calculate NRU used and the cost let used_nru = U64F64::from_num(report.nru) / pricing_policy.nu.factor_base_1000(); let nu_cost = used_nru - * (U64F64::from_num(pricing_policy.nu.value) / U64F64::from_num(SECS_PER_HOUR)) + * (U64F64::from_num(pricing_policy.nu.value) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed); log::info!("nu cost: {:?}", nu_cost); @@ -2137,12 +2139,13 @@ impl Pallet { // Billing time (window) is max 1h by design // So extra time will not be billed // It is the service responsability to bill on right frequency - let window = elapsed_seconds_since_last_bill.min(SECS_PER_HOUR); + let window = elapsed_seconds_since_last_bill.min(T::BillingReferencePeriod::get()); // Billing variable amount is bounded by contract variable fee ensure!( variable_amount - <= ((U64F64::from_num(window) / U64F64::from_num(SECS_PER_HOUR)) + <= ((U64F64::from_num(window) + / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(service_contract.variable_fee)) .round() .to_num::(), diff --git a/substrate-node/pallets/pallet-smart-contract/src/mock.rs b/substrate-node/pallets/pallet-smart-contract/src/mock.rs index 36302361e..e5be6e72a 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/mock.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/mock.rs @@ -1,6 +1,4 @@ #![cfg(test)] -use std::{panic, thread}; - use super::*; use crate::name_contract::NameContractName; use crate::{self as pallet_smart_contract}; @@ -12,17 +10,15 @@ use frame_support::{ BoundedVec, }; use frame_system::EnsureRoot; -use pallet_tfgrid::node::{CityName, CountryName}; use pallet_tfgrid::{ farm::FarmName, interface::{InterfaceIp, InterfaceMac, InterfaceName}, + node::{CityName, CountryName}, node::{Location, SerialNumber}, terms_cond::TermsAndConditions, twin::TwinIp, - DocumentHashInput, DocumentLinkInput, TwinIpInput, -}; -use pallet_tfgrid::{ - CityNameInput, CountryNameInput, Gw4Input, Ip4Input, LatitudeInput, LongitudeInput, + CityNameInput, CountryNameInput, DocumentHashInput, DocumentLinkInput, Gw4Input, Ip4Input, + LatitudeInput, LongitudeInput, TwinIpInput, }; use parking_lot::RwLock; use sp_core::{ @@ -45,8 +41,11 @@ use sp_runtime::{ AccountId32, MultiSignature, }; use sp_std::convert::{TryFrom, TryInto}; -use std::cell::RefCell; -use tfchain_support::{constants::time::MINUTES, traits::ChangeNode}; +use std::{cell::RefCell, panic, thread}; +use tfchain_support::{ + constants::time::{MINUTES, SECS_PER_HOUR}, + traits::ChangeNode, +}; impl_opaque_keys! { pub struct MockSessionKeys { @@ -249,11 +248,13 @@ impl pallet_timestamp::Config for TestRuntime { parameter_types! { pub const BillingFrequency: u64 = 10; + pub const BillingReferencePeriod: u64 = SECS_PER_HOUR; pub const GracePeriod: u64 = 100; pub const DistributionFrequency: u16 = 24; pub const MaxNameContractNameLength: u32 = 64; pub const MaxNodeContractPublicIPs: u32 = 512; pub const MaxDeploymentDataLength: u32 = 512; + pub const SecondsPerHour: u64 = 3600; } pub(crate) type TestNameContractName = NameContractName; @@ -265,6 +266,7 @@ impl pallet_smart_contract::Config for TestRuntime { type Burn = (); type StakingPoolAccount = StakingPoolAccount; type BillingFrequency = BillingFrequency; + type BillingReferencePeriod = BillingReferencePeriod; type DistributionFrequency = DistributionFrequency; type GracePeriod = GracePeriod; type WeightInfo = weights::SubstrateWeight; diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index c2b023570..a39c9cad0 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -18,6 +18,7 @@ use sp_core::H256; use sp_runtime::{assert_eq_error_rate, traits::SaturatedConversion, Perbill, Percent}; use sp_std::convert::{TryFrom, TryInto}; use substrate_fixed::types::U64F64; +use tfchain_support::constants::time::SECS_PER_HOUR; use tfchain_support::{ resources::Resources, types::{FarmCertification, NodeCertification, PublicIP, IP4}, @@ -2841,7 +2842,7 @@ fn test_service_contract_bill_works() { let consumer_balance = Balances::free_balance(&consumer_twin.account_id); let bill_2 = types::ServiceContractBill { variable_amount: VARIABLE_AMOUNT, - window: crate::SECS_PER_HOUR, // force a 1h bill here + window: SECS_PER_HOUR, // force a 1h bill here metadata: BoundedVec::try_from(b"bill_metadata_2".to_vec()).unwrap(), }; let billed_amount_2 = service_contract diff --git a/substrate-node/runtime/src/constants.rs b/substrate-node/runtime/src/constants.rs index 663311f44..2f64f9c7e 100644 --- a/substrate-node/runtime/src/constants.rs +++ b/substrate-node/runtime/src/constants.rs @@ -28,22 +28,6 @@ pub mod currency { } } -/// Time and blocks. -pub mod time { - use crate::BlockNumber; - pub const MILLISECS_PER_BLOCK: u64 = 6000; - pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; - pub const EPOCH_DURATION_IN_BLOCKS: BlockNumber = 1 * HOURS; - - // These time units are defined in number of blocks. - pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); - pub const HOURS: BlockNumber = MINUTES * 60; - pub const DAYS: BlockNumber = HOURS * 24; - - // 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks. - pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); -} - /// Fee-related. pub mod fee { use crate::Balance; @@ -103,8 +87,8 @@ mod tests { let max_block_weight: u128 = (MaximumBlockWeight::get() as u128) * precision; let ext_base_weight: u128 = ExtrinsicBaseWeight::get() as u128; let x = WeightToFeeStruct::weight_to_fee(&MaximumBlockWeight::get()); - let cost_extrinsic:u128 = WeightToFeeStruct::weight_to_fee(&ExtrinsicBaseWeight::get()); - let y:u128 = (cost_extrinsic * (max_block_weight/ext_base_weight)) / precision; + let cost_extrinsic: u128 = WeightToFeeStruct::weight_to_fee(&ExtrinsicBaseWeight::get()); + let y: u128 = (cost_extrinsic * (max_block_weight / ext_base_weight)) / precision; // Difference should be less then the cost of an extrinsic devided by 2 as we can execute Z amount of extrinsics and Z was calculated by deviding // the max amount of weight per block by the weight of one extrinsic. That operation results in a loss of precision (from float to integer). assert!(x.max(y) - x.min(y) < cost_extrinsic / 2); diff --git a/substrate-node/runtime/src/lib.rs b/substrate-node/runtime/src/lib.rs index 97fadd145..8c2f92c51 100644 --- a/substrate-node/runtime/src/lib.rs +++ b/substrate-node/runtime/src/lib.rs @@ -27,6 +27,7 @@ use sp_std::{cmp::Ordering, prelude::*}; use sp_version::NativeVersion; use sp_version::RuntimeVersion; use tfchain_support::{ + constants::time::*, traits::{ChangeNode, PublicIpModifier}, types::PublicIP, }; @@ -149,21 +150,6 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { state_version: 0, }; -/// This determines the average expected block time that we are targeting. -/// Blocks will be produced at a minimum duration defined by `SLOT_DURATION`. -/// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked -/// up by `pallet_aura` to implement `fn slot_duration()`. -/// -/// Change this to adjust the block time. -pub const MILLISECS_PER_BLOCK: u64 = 6000; - -pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; - -// Time is measured by number of blocks. -pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); -pub const HOURS: BlockNumber = MINUTES * 60; -pub const DAYS: BlockNumber = HOURS * 24; - /// The version information used to identify this runtime when compiled natively. #[cfg(feature = "std")] pub fn native_version() -> NativeVersion { @@ -381,6 +367,7 @@ impl pallet_tfgrid::Config for Runtime { parameter_types! { pub StakingPoolAccount: AccountId = get_staking_pool_account(); pub BillingFrequency: u64 = 600; + pub BillingReferencePeriod: u64 = SECS_PER_HOUR; pub GracePeriod: u64 = (14 * DAYS).into(); pub DistributionFrequency: u16 = 24; pub RetryInterval: u32 = 20; @@ -401,6 +388,7 @@ impl pallet_smart_contract::Config for Runtime { type Currency = Balances; type StakingPoolAccount = StakingPoolAccount; type BillingFrequency = BillingFrequency; + type BillingReferencePeriod = BillingReferencePeriod; type DistributionFrequency = DistributionFrequency; type GracePeriod = GracePeriod; type WeightInfo = pallet_smart_contract::weights::SubstrateWeight; diff --git a/substrate-node/support/src/constants.rs b/substrate-node/support/src/constants.rs index 42aa0b462..709165c04 100644 --- a/substrate-node/support/src/constants.rs +++ b/substrate-node/support/src/constants.rs @@ -1,12 +1,19 @@ pub type BlockNumber = u32; /// Time and blocks. pub mod time { + /// This determines the average expected block time that we are targeting. + /// Blocks will be produced at a minimum duration defined by `SLOT_DURATION`. + /// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked + /// up by `pallet_aura` to implement `fn slot_duration()`. + /// + /// Change this to adjust the block time. pub const MILLISECS_PER_BLOCK: u64 = 6000; pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; + pub const SECS_PER_HOUR: u64 = 3600; pub const EPOCH_DURATION_IN_BLOCKS: super::BlockNumber = 1 * HOURS; // These time units are defined in number of blocks. pub const MINUTES: super::BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as super::BlockNumber); pub const HOURS: super::BlockNumber = MINUTES * 60; pub const DAYS: super::BlockNumber = HOURS * 24; -} \ No newline at end of file +}