diff --git a/packages/rs-dpp/src/errors/consensus/fee.rs b/packages/rs-dpp/src/errors/consensus/fee.rs index 8ac40f0e920..94db1a656c7 100644 --- a/packages/rs-dpp/src/errors/consensus/fee.rs +++ b/packages/rs-dpp/src/errors/consensus/fee.rs @@ -1,7 +1,9 @@ use thiserror::Error; +use crate::state_transition::fee::Credits; + #[derive(Error, Debug)] pub enum FeeError { #[error("Current credits balance {balance} is not enough to pay {fee} fee")] - BalanceIsNotEnoughError { balance: u64, fee: i64 }, + BalanceIsNotEnoughError { balance: Credits, fee: Credits }, } diff --git a/packages/rs-dpp/src/state_transition/abstract_state_transition.rs b/packages/rs-dpp/src/state_transition/abstract_state_transition.rs index d17bcbe9219..74c3296c4d6 100644 --- a/packages/rs-dpp/src/state_transition/abstract_state_transition.rs +++ b/packages/rs-dpp/src/state_transition/abstract_state_transition.rs @@ -19,7 +19,6 @@ use crate::{ }; use super::{ - fee::calculate_state_transition_fee::calculate_state_transition_fee, state_transition_execution_context::StateTransitionExecutionContext, StateTransition, StateTransitionType, }; @@ -54,10 +53,6 @@ pub trait StateTransitionLike: fn get_signature(&self) -> &BinaryData; /// set a new signature fn set_signature(&mut self, signature: BinaryData); - /// Calculates the ST fee in credits - fn calculate_fee(&self) -> i64 { - calculate_state_transition_fee(self) - } /// get modified ids list fn get_modified_data_ids(&self) -> Vec; diff --git a/packages/rs-dpp/src/state_transition/fee/calculate_operation_fees.rs b/packages/rs-dpp/src/state_transition/fee/calculate_operation_fees.rs new file mode 100644 index 00000000000..d6fe927df83 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/fee/calculate_operation_fees.rs @@ -0,0 +1,45 @@ +use super::{ + operations::{Operation, OperationLike}, + DummyFeesResult, Refunds, +}; + +pub fn calculate_operation_fees(operations: &[Operation]) -> DummyFeesResult { + let mut storage_fee = 0; + let mut processing_fee = 0; + let mut fee_refunds: Vec = Vec::new(); + + for operation in operations { + storage_fee += operation.get_storage_cost(); + processing_fee += operation.get_processing_cost(); + + // Merge refunds + if let Some(operation_refunds) = operation.get_refunds() { + for identity_refunds in operation_refunds { + let mut existing_identity_refunds = fee_refunds + .iter_mut() + .find(|refund| refund.identifier == identity_refunds.identifier); + + if existing_identity_refunds.is_none() { + fee_refunds.push(identity_refunds.clone()); + continue; + } + + for (epoch_index, credits) in identity_refunds.credits_per_epoch.iter() { + if let Some(ref mut refunds) = existing_identity_refunds { + let epoch = refunds + .credits_per_epoch + .entry(epoch_index.to_string()) + .or_default(); + *epoch += credits + } + } + } + } + } + + DummyFeesResult { + storage: storage_fee, + processing: processing_fee, + fee_refunds, + } +} diff --git a/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_factory.rs b/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_factory.rs new file mode 100644 index 00000000000..01d0583746c --- /dev/null +++ b/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_factory.rs @@ -0,0 +1,15 @@ +use crate::state_transition::{ + fee::calculate_state_transition_fee_from_operations_factory::calculate_state_transition_fee_from_operations, + StateTransition, StateTransitionLike, +}; + +use super::FeeResult; + +pub fn calculate_state_transition_fee(state_transition: &StateTransition) -> FeeResult { + let execution_context = state_transition.get_execution_context(); + + calculate_state_transition_fee_from_operations( + &execution_context.get_operations(), + state_transition.get_owner_id(), + ) +} diff --git a/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_from_operations_factory.rs b/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_from_operations_factory.rs new file mode 100644 index 00000000000..a5a7adf92f6 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/fee/calculate_state_transition_fee_from_operations_factory.rs @@ -0,0 +1,110 @@ +use crate::prelude::Identifier; + +use super::{ + calculate_operation_fees::calculate_operation_fees, constants::DEFAULT_USER_TIP, + operations::Operation, DummyFeesResult, FeeResult, +}; + +pub fn calculate_state_transition_fee_from_operations( + operations: &[Operation], + identity_id: &Identifier, +) -> FeeResult { + calculate_state_transition_fee_from_operations_with_custom_calculator( + operations, + identity_id, + calculate_operation_fees, + ) +} + +fn calculate_state_transition_fee_from_operations_with_custom_calculator( + operations: &[Operation], + identity_id: &Identifier, + calculate_operation_fees_fn: impl FnOnce(&[Operation]) -> DummyFeesResult, +) -> FeeResult { + let calculated_fees = calculate_operation_fees_fn(operations); + + let storage_fee = calculated_fees.storage; + let processing_fee = calculated_fees.processing; + let fee_refunds = calculated_fees.fee_refunds; + + let mut total_refunds = 0; + + let owner_refunds = fee_refunds + .iter() + .find(|refunds| identity_id == &refunds.identifier); + + if let Some(owner_refunds) = owner_refunds { + total_refunds = owner_refunds + .credits_per_epoch + .iter() + .fold(0, |sum, (_, credits)| sum + credits); + } + + let required_amount = (storage_fee - total_refunds) + DEFAULT_USER_TIP; + let desired_amount = (storage_fee + processing_fee - total_refunds) + DEFAULT_USER_TIP; + + FeeResult { + storage_fee, + processing_fee, + fee_refunds, + total_refunds, + required_amount, + desired_amount, + } +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use crate::{ + state_transition::fee::{ + operations::Operation, Credits, DummyFeesResult, FeeResult, Refunds, + }, + tests::utils::generate_random_identifier_struct, + }; + + use super::calculate_state_transition_fee_from_operations_with_custom_calculator; + + #[test] + fn should_calculate_fee_based_on_executed_operations() { + let identifier = generate_random_identifier_struct(); + let storage_fee = 10000; + let processing_fee = 1000; + let total_refunds = 1000 + 500; + let required_amount = storage_fee - total_refunds; + let desired_amount = storage_fee + processing_fee - total_refunds; + + let mut credits_per_epoch: HashMap = Default::default(); + credits_per_epoch.insert("0".to_string(), 1000); + credits_per_epoch.insert("1".to_string(), 500); + + let refunds = Refunds { + identifier, + credits_per_epoch, + }; + + let mock = |_operations: &[Operation]| -> DummyFeesResult { + DummyFeesResult { + storage: storage_fee, + processing: processing_fee, + fee_refunds: vec![refunds.clone()], + } + }; + + let result = calculate_state_transition_fee_from_operations_with_custom_calculator( + &[], + &identifier, + mock, + ); + let expected = FeeResult { + storage_fee, + processing_fee, + desired_amount, + required_amount, + fee_refunds: vec![refunds], + total_refunds: 1500, + }; + assert_eq!(expected, result); + } +} diff --git a/packages/rs-dpp/src/state_transition/fee/constants.rs b/packages/rs-dpp/src/state_transition/fee/constants.rs index 4ee78de7830..6730ac945b4 100644 --- a/packages/rs-dpp/src/state_transition/fee/constants.rs +++ b/packages/rs-dpp/src/state_transition/fee/constants.rs @@ -1,15 +1,17 @@ use crate::identity::KeyType; -pub const BASE_ST_PROCESSING_FEE: i64 = 10000; // 84000 -pub const FEE_MULTIPLIER: i64 = 2; -pub const DEFAULT_USER_TIP: i64 = 0; -pub const STORAGE_CREDIT_PER_BYTE: i64 = 5000; -pub const PROCESSING_CREDIT_PER_BYTE: i64 = 12; -pub const DELETE_BASE_PROCESSING_COST: i64 = 2000; // 20000 -pub const READ_BASE_PROCESSING_COST: i64 = 8400; // 8400 -pub const WRITE_BASE_PROCESSING_COST: i64 = 6000; // 60000 +use super::Credits; -pub const fn signature_verify_cost(key_type: KeyType) -> i64 { +pub const BASE_ST_PROCESSING_FEE: Credits = 10000; // 84000 +pub const FEE_MULTIPLIER: Credits = 2; +pub const DEFAULT_USER_TIP: Credits = 0; +pub const STORAGE_CREDIT_PER_BYTE: Credits = 5000; +pub const PROCESSING_CREDIT_PER_BYTE: Credits = 12; +pub const DELETE_BASE_PROCESSING_COST: Credits = 2000; // 20000 +pub const READ_BASE_PROCESSING_COST: Credits = 8400; // 8400 +pub const WRITE_BASE_PROCESSING_COST: Credits = 6000; // 60000 + +pub const fn signature_verify_cost(key_type: KeyType) -> Credits { match key_type { KeyType::ECDSA_SECP256K1 => 3000, KeyType::BLS12_381 => 6000, diff --git a/packages/rs-dpp/src/state_transition/fee/mod.rs b/packages/rs-dpp/src/state_transition/fee/mod.rs index eac889b595d..0c0c515eb31 100644 --- a/packages/rs-dpp/src/state_transition/fee/mod.rs +++ b/packages/rs-dpp/src/state_transition/fee/mod.rs @@ -1,33 +1,37 @@ -use std::borrow::Borrow; +use std::collections::HashMap; -use self::{ - constants::FEE_MULTIPLIER, - operations::{Operation, OperationLike}, -}; +use serde::{Deserialize, Serialize}; -pub mod calculate_state_transition_fee; +use crate::prelude::Identifier; + +pub mod calculate_operation_fees; +pub mod calculate_state_transition_fee_factory; +pub mod calculate_state_transition_fee_from_operations_factory; pub mod constants; pub mod operations; -#[derive(Default)] -pub struct Fees { - storage: i64, - processing: i64, -} - -pub fn calculate_operations_fees( - operations: impl IntoIterator>, -) -> Fees { - let mut fees = Fees::default(); +pub type Credits = u64; - for operation in operations.into_iter() { - let operation = operation.borrow(); - fees.processing += operation.get_processing_cost(); - fees.storage += operation.get_storage_cost(); - } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct FeeResult { + pub storage_fee: Credits, + pub processing_fee: Credits, + pub fee_refunds: Vec, + pub total_refunds: Credits, + pub desired_amount: Credits, + pub required_amount: Credits, +} - fees.storage *= FEE_MULTIPLIER; - fees.processing *= FEE_MULTIPLIER; +#[derive(Default)] +pub struct DummyFeesResult { + storage: Credits, + processing: Credits, + fee_refunds: Vec, +} - fees +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename = "camelCase")] +pub struct Refunds { + pub identifier: Identifier, + pub credits_per_epoch: HashMap, } diff --git a/packages/rs-dpp/src/state_transition/fee/operations/delete_operation.rs b/packages/rs-dpp/src/state_transition/fee/operations/delete_operation.rs deleted file mode 100644 index 214c19dffc5..00000000000 --- a/packages/rs-dpp/src/state_transition/fee/operations/delete_operation.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::OperationLike; -use crate::state_transition::fee::constants::{ - DELETE_BASE_PROCESSING_COST, PROCESSING_CREDIT_PER_BYTE, STORAGE_CREDIT_PER_BYTE, -}; - -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct DeleteOperation { - pub key_size: usize, - pub value_size: usize, -} - -impl DeleteOperation { - pub fn new(key_size: usize, value_size: usize) -> Self { - Self { - key_size, - value_size, - } - } -} - -impl OperationLike for DeleteOperation { - fn get_processing_cost(&self) -> i64 { - DELETE_BASE_PROCESSING_COST - + ((self.key_size as i64 + self.value_size as i64) * PROCESSING_CREDIT_PER_BYTE) - } - - fn get_storage_cost(&self) -> i64 { - -((self.key_size + self.value_size) as i64 * STORAGE_CREDIT_PER_BYTE) - } -} diff --git a/packages/rs-dpp/src/state_transition/fee/operations/mod.rs b/packages/rs-dpp/src/state_transition/fee/operations/mod.rs index 7085fd22f74..59430db494a 100644 --- a/packages/rs-dpp/src/state_transition/fee/operations/mod.rs +++ b/packages/rs-dpp/src/state_transition/fee/operations/mod.rs @@ -1,47 +1,42 @@ -mod delete_operation; - -pub use delete_operation::*; - mod precalculated_operation; pub use precalculated_operation::*; mod read_operation; pub use read_operation::*; -mod write_operation; use serde::{Deserialize, Serialize}; use serde_json::Value; -pub use write_operation::*; mod signature_verification_operation; pub use signature_verification_operation::*; +use super::{Credits, Refunds}; + pub const STORAGE_CREDIT_PER_BYTE: i64 = 5000; pub const STORAGE_PROCESSING_CREDIT_PER_BYTE: i64 = 5000; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Operation { Read(ReadOperation), - Write(WriteOperation), - Delete(DeleteOperation), PreCalculated(PreCalculatedOperation), SignatureVerification(SignatureVerificationOperation), } pub trait OperationLike { /// Get CPU cost of the operation - fn get_processing_cost(&self) -> i64; + fn get_processing_cost(&self) -> Credits; /// Get storage cost of the operation - fn get_storage_cost(&self) -> i64; + fn get_storage_cost(&self) -> Credits; + + /// Get refunds + fn get_refunds(&self) -> Option<&Vec>; } macro_rules! call_method { ($operation_type:expr, $method:ident ) => { match $operation_type { Operation::Read(op) => op.$method(), - Operation::Write(op) => op.$method(), - Operation::Delete(op) => op.$method(), Operation::PreCalculated(op) => op.$method(), Operation::SignatureVerification(op) => op.$method(), } @@ -49,13 +44,17 @@ macro_rules! call_method { } impl OperationLike for Operation { - fn get_processing_cost(&self) -> i64 { + fn get_processing_cost(&self) -> Credits { call_method!(self, get_processing_cost) } - fn get_storage_cost(&self) -> i64 { + fn get_storage_cost(&self) -> Credits { call_method!(self, get_storage_cost) } + + fn get_refunds(&self) -> Option<&Vec> { + call_method!(self, get_refunds) + } } impl Operation { @@ -82,11 +81,8 @@ impl Operation { #[cfg(test)] mod test { - use super::{ - DeleteOperation, Operation, PreCalculatedOperation, ReadOperation, - SignatureVerificationOperation, - }; - use crate::{identity::KeyType, state_transition::fee::operations::WriteOperation}; + use super::{Operation, PreCalculatedOperation, ReadOperation, SignatureVerificationOperation}; + use crate::identity::KeyType; use serde_json::json; struct TestCase { @@ -110,37 +106,17 @@ mod test { }), operation: Operation::Read(ReadOperation { value_size: 123 }), }, - TestCase { - json_str: json_string!({ - "type": "write", - "keySize" : 65, - "valueSize" : 321, - }), - operation: Operation::Write(WriteOperation { - key_size: 65, - value_size: 321, - }), - }, - TestCase { - json_str: json_string!({ - "type": "delete", - "keySize" : 65, - "valueSize" : 123, - }), - operation: Operation::Delete(DeleteOperation { - key_size: 65, - value_size: 123, - }), - }, TestCase { json_str: json_string!({ "type": "preCalculated", "storageCost" : 12357, "processingCost" : 468910, + "feeRefunds" : [], }), operation: Operation::PreCalculated(PreCalculatedOperation { storage_cost: 12357, processing_cost: 468910, + fee_refunds: vec![], }), }, TestCase { diff --git a/packages/rs-dpp/src/state_transition/fee/operations/precalculated_operation.rs b/packages/rs-dpp/src/state_transition/fee/operations/precalculated_operation.rs index f3d57fc96f6..b7cdc272502 100644 --- a/packages/rs-dpp/src/state_transition/fee/operations/precalculated_operation.rs +++ b/packages/rs-dpp/src/state_transition/fee/operations/precalculated_operation.rs @@ -1,29 +1,49 @@ use serde::{Deserialize, Serialize}; +use crate::state_transition::fee::{Credits, DummyFeesResult, Refunds}; + use super::OperationLike; -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PreCalculatedOperation { - pub storage_cost: i64, - pub processing_cost: i64, + pub storage_cost: Credits, + pub processing_cost: Credits, + pub fee_refunds: Vec, } impl PreCalculatedOperation { - pub fn new(storage_cost: i64, processing_cost: i64) -> Self { + pub fn from_fee(fee: DummyFeesResult) -> Self { + Self { + fee_refunds: fee.fee_refunds, + processing_cost: fee.processing, + storage_cost: fee.storage, + } + } + + pub fn new( + storage_cost: Credits, + processing_cost: Credits, + fee_refunds: impl IntoIterator, + ) -> Self { Self { storage_cost, processing_cost, + fee_refunds: fee_refunds.into_iter().collect(), } } } impl OperationLike for PreCalculatedOperation { - fn get_processing_cost(&self) -> i64 { + fn get_processing_cost(&self) -> Credits { self.processing_cost } - fn get_storage_cost(&self) -> i64 { + fn get_storage_cost(&self) -> Credits { self.storage_cost } + + fn get_refunds(&self) -> Option<&Vec> { + Some(&self.fee_refunds) + } } diff --git a/packages/rs-dpp/src/state_transition/fee/operations/read_operation.rs b/packages/rs-dpp/src/state_transition/fee/operations/read_operation.rs index 5f54ad1a681..007be2e941e 100644 --- a/packages/rs-dpp/src/state_transition/fee/operations/read_operation.rs +++ b/packages/rs-dpp/src/state_transition/fee/operations/read_operation.rs @@ -2,28 +2,33 @@ use serde::{Deserialize, Serialize}; use super::OperationLike; -use crate::state_transition::fee::constants::{ - PROCESSING_CREDIT_PER_BYTE, READ_BASE_PROCESSING_COST, +use crate::state_transition::fee::{ + constants::{PROCESSING_CREDIT_PER_BYTE, READ_BASE_PROCESSING_COST}, + Credits, Refunds, }; #[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ReadOperation { - pub value_size: usize, + pub value_size: Credits, } impl ReadOperation { - pub fn new(value_size: usize) -> Self { + pub fn new(value_size: u64) -> Self { Self { value_size } } } impl OperationLike for ReadOperation { - fn get_processing_cost(&self) -> i64 { - READ_BASE_PROCESSING_COST + (self.value_size as i64 * PROCESSING_CREDIT_PER_BYTE) + fn get_processing_cost(&self) -> Credits { + READ_BASE_PROCESSING_COST + (self.value_size * PROCESSING_CREDIT_PER_BYTE) } - fn get_storage_cost(&self) -> i64 { + fn get_storage_cost(&self) -> Credits { 0 } + + fn get_refunds(&self) -> Option<&Vec> { + None + } } diff --git a/packages/rs-dpp/src/state_transition/fee/operations/signature_verification_operation.rs b/packages/rs-dpp/src/state_transition/fee/operations/signature_verification_operation.rs index 0211b196b2f..59d43afb249 100644 --- a/packages/rs-dpp/src/state_transition/fee/operations/signature_verification_operation.rs +++ b/packages/rs-dpp/src/state_transition/fee/operations/signature_verification_operation.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; use super::OperationLike; -use crate::{identity::KeyType, state_transition::fee::constants::signature_verify_cost}; +use crate::{ + identity::KeyType, + state_transition::fee::{constants::signature_verify_cost, Credits, Refunds}, +}; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -16,11 +19,15 @@ impl SignatureVerificationOperation { } impl OperationLike for SignatureVerificationOperation { - fn get_processing_cost(&self) -> i64 { + fn get_processing_cost(&self) -> Credits { signature_verify_cost(self.signature_type) } - fn get_storage_cost(&self) -> i64 { + fn get_storage_cost(&self) -> Credits { 0 } + + fn get_refunds(&self) -> Option<&Vec> { + None + } } diff --git a/packages/rs-dpp/src/state_transition/fee/operations/write_operation.rs b/packages/rs-dpp/src/state_transition/fee/operations/write_operation.rs deleted file mode 100644 index 65b2088e576..00000000000 --- a/packages/rs-dpp/src/state_transition/fee/operations/write_operation.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::OperationLike; -use crate::state_transition::fee::constants::{ - PROCESSING_CREDIT_PER_BYTE, STORAGE_CREDIT_PER_BYTE, WRITE_BASE_PROCESSING_COST, -}; - -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct WriteOperation { - pub key_size: usize, - pub value_size: usize, -} - -impl WriteOperation { - pub fn new(key_size: usize, value_size: usize) -> Self { - Self { - key_size, - value_size, - } - } -} - -impl OperationLike for WriteOperation { - fn get_processing_cost(&self) -> i64 { - WRITE_BASE_PROCESSING_COST - + ((self.key_size + self.value_size) as i64 * PROCESSING_CREDIT_PER_BYTE) - } - - fn get_storage_cost(&self) -> i64 { - (self.key_size + self.value_size) as i64 * STORAGE_CREDIT_PER_BYTE - } -} diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index baf0711e8ec..784a931bf13 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -15,6 +15,7 @@ use crate::identity::state_transition::identity_create_transition::IdentityCreat use crate::identity::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; use crate::identity::state_transition::identity_topup_transition::IdentityTopUpTransition; use crate::identity::state_transition::identity_update_transition::identity_update_transition::IdentityUpdateTransition; +use crate::prelude::Identifier; mod abstract_state_transition; mod abstract_state_transition_identity_signed; @@ -98,6 +99,10 @@ impl StateTransition { fn binary_property_paths(&self) -> Vec<&'static str> { call_static_method!(self, binary_property_paths) } + + fn get_owner_id(&self) -> &Identifier { + call_method!(self, get_owner_id) + } } impl StateTransitionConvert for StateTransition { diff --git a/packages/rs-dpp/src/state_transition/state_transition_execution_context.rs b/packages/rs-dpp/src/state_transition/state_transition_execution_context.rs index 4ac2f18c1c3..2a524aceb75 100644 --- a/packages/rs-dpp/src/state_transition/state_transition_execution_context.rs +++ b/packages/rs-dpp/src/state_transition/state_transition_execution_context.rs @@ -47,8 +47,10 @@ impl StateTransitionExecutionContext { inner .actual_operations .iter() - .copied() - .chain(inner.dry_run_operations.iter().copied()) + // TODO remove cloning + .cloned() + // TODO remove cloning + .chain(inner.dry_run_operations.iter().cloned()) .collect() } diff --git a/packages/rs-dpp/src/state_transition/validation/validate_state_transition_fee.rs b/packages/rs-dpp/src/state_transition/validation/validate_state_transition_fee.rs index 17c7bd8fcce..0b011208653 100644 --- a/packages/rs-dpp/src/state_transition/validation/validate_state_transition_fee.rs +++ b/packages/rs-dpp/src/state_transition/validation/validate_state_transition_fee.rs @@ -3,6 +3,8 @@ use std::convert::TryInto; use crate::consensus::basic::state_transition::InvalidStateTransitionTypeError; use crate::data_contract::errors::IdentityNotPresentError; +use crate::state_transition::fee::calculate_state_transition_fee_factory::calculate_state_transition_fee; +use crate::state_transition::fee::{Credits, FeeResult}; use crate::state_transition::StateTransitionType; use crate::{ consensus::fee::FeeError, @@ -38,10 +40,21 @@ where pub async fn validate( &self, state_transition: &StateTransition, + ) -> Result { + self.validate_with_custom_calculator(state_transition, calculate_state_transition_fee) + .await + } + + async fn validate_with_custom_calculator( + &self, + state_transition: &StateTransition, + calculate_state_transition_fee_fn: impl Fn(&StateTransition) -> FeeResult, ) -> Result { let mut result = SimpleValidationResult::default(); let execution_context = state_transition.get_execution_context(); + let required_fee = calculate_state_transition_fee_fn(state_transition); + let balance = match state_transition { StateTransition::IdentityCreate(st) => { let output = self @@ -69,13 +82,10 @@ where })?; let balance = convert_satoshi_to_credits(output.value); let identity_id = st.get_owner_id(); - let identity = self + let identity_balance: i64 = self .state_repository - .fetch_identity(identity_id, Some(st.get_execution_context())) + .fetch_identity_balance_with_debt(identity_id, Some(execution_context)) .await? - .map(TryInto::try_into) - .transpose() - .map_err(Into::into)? .ok_or_else(|| { ProtocolError::IdentityNotPresentError(IdentityNotPresentError::new( *identity_id, @@ -85,7 +95,20 @@ where if execution_context.is_dry_run() { return Ok(result); } - balance + identity.get_balance() + + if identity_balance.is_negative() && identity_balance.unsigned_abs() > balance { + result.add_error(FeeError::BalanceIsNotEnoughError { + balance: 0, + fee: required_fee.desired_amount, + }); + return Ok(result); + } + + if identity_balance.is_negative() { + balance - identity_balance.unsigned_abs() + } else { + balance + identity_balance as Credits + } } StateTransition::DataContractCreate(st) => { let balance = self.get_identity_owner_balance(st).await?; @@ -129,10 +152,12 @@ where return Ok(result); } - let fee = state_transition.calculate_fee(); // ? make sure Fee cannot be negative and refunds are handled differently - if (balance as i64) < fee { - result.add_error(FeeError::BalanceIsNotEnoughError { balance, fee }) + if balance < required_fee.desired_amount { + result.add_error(FeeError::BalanceIsNotEnoughError { + balance, + fee: required_fee.desired_amount, + }) } Ok(result) @@ -160,12 +185,18 @@ where #[cfg(test)] mod test { + use crate::tests::fixtures::{ + identity_create_transition_fixture, identity_topup_transition_fixture, + }; use std::sync::Arc; use crate::data_contract::state_transition::data_contract_create_transition::DataContractCreateTransition; + use crate::identity::state_transition::asset_lock_proof::AssetLockProof; + use crate::identity::state_transition::identity_create_transition::IdentityCreateTransition; use crate::identity::state_transition::identity_topup_transition::IdentityTopUpTransition; - use crate::state_transition::StateTransitionLike; - use crate::tests::fixtures::identity_topup_transition_fixture; + use crate::identity::RATIO; + use crate::state_transition::fee::{Credits, FeeResult}; + use crate::state_transition::StateTransition; use crate::ProtocolError; use crate::{ consensus::fee::FeeError, @@ -188,17 +219,32 @@ mod test { use super::StateTransitionFeeValidator; fn execution_context_with_cost( - storage_cost: i64, - processing_cost: i64, + storage_cost: Credits, + processing_cost: Credits, ) -> StateTransitionExecutionContext { let ctx = StateTransitionExecutionContext::default(); ctx.add_operation(Operation::PreCalculated(PreCalculatedOperation::new( storage_cost, processing_cost, + vec![], ))); ctx } + macro_rules! get_output_amount_from_identity_transition { + ($transition:ident) => { + if let AssetLockProof::Instant(lock_proof) = $transition.get_asset_lock_proof() { + let satoshis = lock_proof + .output() + .expect("output must be present in instant lock proof") + .value; + satoshis * RATIO + } else { + panic!("identity must have an instant lock proof") + } + }; + } + #[tokio::test] async fn data_contract_crate_transition_invalid_result_if_balance_is_not_enough() { let mut identity = identity_fixture(); @@ -226,8 +272,7 @@ mod test { let fee_error = get_fee_error_from_result(&result, 0); assert!( matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, fee } if { - *balance == 1 && - *fee == 90 + *balance == 1 }) ); } @@ -237,7 +282,7 @@ mod test { let mut identity = identity_fixture(); let mut state_repository_mock = MockStateRepositoryLike::new(); - identity.balance = 90; + identity.balance = 52; state_repository_mock .expect_fetch_identity() .returning(move |_, _| Ok(Some(identity.clone()))); @@ -287,9 +332,8 @@ mod test { let fee_error = get_fee_error_from_result(&result, 0); assert!( - matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, fee } if { - *balance == 1 && - *fee == 90 + matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, .. } if { + *balance == 1 }) ); } @@ -355,33 +399,114 @@ mod test { assert!(result.is_valid()); } + #[tokio::test] + async fn identity_create_transition_should_return_invalid_result_if_asset_lock_output_amount_is_not_enough( + ) { + let identity_create_transition = + IdentityCreateTransition::new(identity_create_transition_fixture(None)).unwrap(); + let output_amount = get_output_amount_from_identity_transition!(identity_create_transition); + let state_repository_mock = MockStateRepositoryLike::new(); + let calculate_state_transition_fee_mock = |_: &StateTransition| FeeResult { + desired_amount: output_amount + 1, + ..Default::default() + }; + + let validator = StateTransitionFeeValidator::new(Arc::new(state_repository_mock)); + let result = validator + .validate_with_custom_calculator( + &identity_create_transition.into(), + calculate_state_transition_fee_mock, + ) + .await + .expect("the validation result should be returned"); + let fee_error = get_fee_error_from_result(&result, 0); + + assert!( + matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, .. } if { + *balance == output_amount + }) + ); + } + + #[tokio::test] + async fn identity_create_transition_should_return_valid_result() { + let identity_create_transition = + IdentityCreateTransition::new(identity_create_transition_fixture(None)).unwrap(); + let output_amount = get_output_amount_from_identity_transition!(identity_create_transition); + let state_repository_mock = MockStateRepositoryLike::new(); + let calculate_state_transition_fee_mock = |_: &StateTransition| FeeResult { + desired_amount: output_amount, + ..Default::default() + }; + let validator = StateTransitionFeeValidator::new(Arc::new(state_repository_mock)); + let result = validator + .validate_with_custom_calculator( + &identity_create_transition.into(), + calculate_state_transition_fee_mock, + ) + .await + .expect("the validation result should be returned"); + assert!(result.is_valid()) + } + #[tokio::test] async fn identity_top_up_transition_should_return_invalid_result_if_balance_is_not_enough() { - let mut identity = identity_fixture(); let mut state_repository_mock = MockStateRepositoryLike::new(); - identity.balance = 1; state_repository_mock - .expect_fetch_identity() - .returning(move |_, _| Ok(Some(identity.clone()))); + .expect_fetch_identity_balance_with_debt() + .returning(move |_, _| Ok(Some(1))); - let mut identity_topup_transition = + let identity_topup_transition = IdentityTopUpTransition::new(identity_topup_transition_fixture(None)).unwrap(); - identity_topup_transition.set_execution_context(execution_context_with_cost(45000000, 5)); + let output_amount = get_output_amount_from_identity_transition!(identity_topup_transition); + + let calculate_state_transition_fee_mock = |_: &StateTransition| FeeResult { + desired_amount: output_amount + 2, + ..Default::default() + }; let validator = StateTransitionFeeValidator::new(Arc::new(state_repository_mock)); let result = validator - .validate(&identity_topup_transition.into()) + .validate_with_custom_calculator( + &identity_topup_transition.into(), + calculate_state_transition_fee_mock, + ) .await .expect("the validation result should be returned"); + let fee_error = get_fee_error_from_result(&result, 0); assert!( - matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, fee } if { - *balance == 90000001 && - *fee == 90000010 + matches!(fee_error, FeeError::BalanceIsNotEnoughError { balance, .. } if { + *balance == output_amount + 1 }) ); } + #[tokio::test] + async fn identity_top_up_transition_should_return_valid_result() { + let mut state_repository_mock = MockStateRepositoryLike::new(); + state_repository_mock + .expect_fetch_identity_balance_with_debt() + .returning(move |_, _| Ok(Some(41))); + + let identity_topup_transition = + IdentityTopUpTransition::new(identity_topup_transition_fixture(None)).unwrap(); + let output_amount = get_output_amount_from_identity_transition!(identity_topup_transition); + + let calculation_mock = |_: &StateTransition| FeeResult { + desired_amount: output_amount - 1, + ..Default::default() + }; + + let validator = StateTransitionFeeValidator::new(Arc::new(state_repository_mock)); + let result = validator + .validate_with_custom_calculator(&identity_topup_transition.into(), calculation_mock) + .await + .expect("the validation result should be returned"); + + assert!(result.is_valid()) + } + #[tokio::test] async fn should_return_invalid_state_transition_type() { let transition = IdentityCreditWithdrawalTransition::default(); diff --git a/packages/rs-drive/src/drive/test_utils.rs b/packages/rs-drive/src/drive/test_utils.rs index 61240e3d254..f37c4ccf908 100644 --- a/packages/rs-drive/src/drive/test_utils.rs +++ b/packages/rs-drive/src/drive/test_utils.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use dpp::util::entropy_generator::EntropyGenerator; +use dpp::{dashcore::anyhow, util::entropy_generator::EntropyGenerator}; use rand::{rngs::SmallRng, Rng, SeedableRng}; pub(crate) struct TestEntropyGenerator { @@ -16,7 +16,7 @@ impl TestEntropyGenerator { } impl EntropyGenerator for TestEntropyGenerator { - fn generate(&self) -> [u8; 32] { - self.rng.borrow_mut().gen() + fn generate(&self) -> anyhow::Result<[u8; 32]> { + Ok(self.rng.borrow_mut().gen()) } } diff --git a/packages/wasm-dpp/src/document/state_transition/document_batch_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/document_batch_transition/mod.rs index 7e15b529050..ddebf0e65cc 100644 --- a/packages/wasm-dpp/src/document/state_transition/document_batch_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/document_batch_transition/mod.rs @@ -329,11 +329,6 @@ impl DocumentsBatchTransitionWasm { self.0.set_signature(BinaryData::new(signature)) } - #[wasm_bindgen(js_name=calculateFee)] - pub fn calculate_fee(&self) -> i64 { - self.0.calculate_fee() - } - #[wasm_bindgen(js_name=isDocumentStateTransition)] pub fn is_document_state_transition(&self) -> bool { self.0.is_document_state_transition() diff --git a/packages/wasm-dpp/src/errors/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus_error.rs index b067c1808e5..7453d0133f9 100644 --- a/packages/wasm-dpp/src/errors/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus_error.rs @@ -185,7 +185,8 @@ pub fn from_consensus_error_ref(e: &DPPConsensusError) -> JsValue { } DPPConsensusError::FeeError(e) => match e { dpp::consensus::fee::FeeError::BalanceIsNotEnoughError { balance, fee } => { - BalanceIsNotEnoughErrorWasm::new(*balance, *fee, code).into() + // TODO decide about type for Fee + BalanceIsNotEnoughErrorWasm::new(*balance, *fee as i64, code).into() } }, DPPConsensusError::SignatureError(e) => from_signature_error(e),