From c8317da7a60dae74c40d0da9984276f4066e8ffa Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Mon, 30 Sep 2024 02:22:30 +0700 Subject: [PATCH] feat(platform)!: withdrawal limits (#2182) --- Cargo.lock | 21 +- packages/dashpay-contract/Cargo.toml | 2 +- packages/data-contracts/Cargo.toml | 2 +- packages/dpns-contract/Cargo.toml | 2 +- packages/feature-flags-contract/Cargo.toml | 2 +- .../Cargo.toml | 2 +- packages/rs-dapi-client/Cargo.toml | 2 +- ...edit_withdrawal_transition_amount_error.rs | 15 +- packages/rs-dpp/src/identity/core_script.rs | 16 +- packages/rs-dpp/src/util/units.rs | 30 + .../withdrawal/daily_withdrawal_limit/mod.rs | 18 + .../daily_withdrawal_limit/v0/mod.rs | 54 + packages/rs-dpp/src/withdrawal/mod.rs | 2 + packages/rs-drive-abci/Cargo.toml | 2 +- .../initialization/init_chain/v0/mod.rs | 2 +- .../engine/run_block_proposal/mod.rs | 15 +- .../engine/run_block_proposal/v0/mod.rs | 11 + .../platform_events/protocol_upgrade/mod.rs | 1 + .../mod.rs | 66 + .../v0/mod.rs | 73 + .../v0/mod.rs | 2 +- .../mod.rs | 3 +- .../v0/mod.rs | 51 +- .../mod.rs | 58 + .../v0/mod.rs | 69 + .../v0/mod.rs | 6 +- .../platform_events/withdrawals/mod.rs | 1 + .../v0/mod.rs | 68 +- .../v1/mod.rs | 70 +- .../v0/mod.rs | 9 +- .../identity_credit_withdrawal/mod.rs | 2 +- .../structure/v1/mod.rs | 14 +- .../src/platform_types/platform_state/mod.rs | 1 - .../tests/strategy_tests/execution.rs | 1 + .../tests/strategy_tests/main.rs | 24 +- .../tests/strategy_tests/strategy.rs | 38 +- .../tests/strategy_tests/voting_tests.rs | 6 + .../tests/strategy_tests/withdrawal_tests.rs | 2194 +++++++++++++++-- packages/rs-drive/Cargo.toml | 1 + .../calculate_current_withdrawal_limit/mod.rs | 53 + .../v0/mod.rs | 86 + .../mod.rs | 5 +- .../v0/mod.rs | 1 + .../identity/withdrawals/document/mod.rs | 4 +- .../src/drive/identity/withdrawals/mod.rs | 1 + .../src/drive/identity/withdrawals/paths.rs | 34 +- .../mod.rs | 3 + .../v0/mod.rs | 11 + .../withdrawals/transaction/queue/mod.rs | 1 + .../src/drive/initialization/v0/mod.rs | 2 +- .../v0/transformer.rs | 30 +- .../util/batch/drive_op_batch/withdrawals.rs | 42 +- .../batch_delete_items_in_path_query/mod.rs | 56 + .../v0/mod.rs | 588 +++++ .../batch_insert_if_not_exists/mod.rs | 2 +- .../batch_insert_if_not_exists/v0/mod.rs | 2 +- .../mod.rs | 69 + .../v0/mod.rs | 165 ++ .../mod.rs | 58 + .../v0/mod.rs | 220 ++ .../grove_get_sum_tree_total_value/v0/mod.rs | 2 +- .../mod.rs | 54 + .../v0/mod.rs | 30 + .../rs-drive/src/util/grove_operations/mod.rs | 16 +- .../Cargo.toml | 2 +- packages/rs-platform-value/Cargo.toml | 2 +- .../src/version/dpp_versions.rs | 1 + .../src/version/drive_abci_versions.rs | 9 + .../src/version/drive_versions.rs | 6 +- .../rs-platform-version/src/version/limits.rs | 1 + .../src/version/mocks/v2_test.rs | 17 +- .../src/version/mocks/v3_test.rs | 16 +- .../rs-platform-version/src/version/v1.rs | 16 +- .../rs-platform-version/src/version/v2.rs | 16 +- .../rs-platform-version/src/version/v3.rs | 16 +- .../rs-platform-version/src/version/v4.rs | 16 +- packages/rs-sdk/Cargo.toml | 10 +- packages/strategy-tests/src/lib.rs | 18 +- packages/strategy-tests/src/operations.rs | 29 +- packages/strategy-tests/src/transitions.rs | 111 +- packages/withdrawals-contract/Cargo.toml | 2 +- packages/withdrawals-contract/src/lib.rs | 5 + 82 files changed, 4460 insertions(+), 324 deletions(-) create mode 100644 packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/mod.rs create mode 100644 packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/mod.rs create mode 100644 packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/v0/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/v0/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/v0/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/v0/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/mod.rs create mode 100644 packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/v0/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ad59187b72b..2d882e0c93b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -1499,6 +1499,7 @@ name = "drive" version = "1.4.0-dev.1" dependencies = [ "arc-swap", + "assert_matches", "base64 0.22.1", "bincode", "bs58 0.5.1", @@ -5102,18 +5103,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -5188,9 +5189,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -5284,9 +5285,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", diff --git a/packages/dashpay-contract/Cargo.toml b/packages/dashpay-contract/Cargo.toml index cdb3ea808ee..c425b0738a0 100644 --- a/packages/dashpay-contract/Cargo.toml +++ b/packages/dashpay-contract/Cargo.toml @@ -8,6 +8,6 @@ license = "MIT" [dependencies] platform-version = { path = "../rs-platform-version" } -thiserror = "1.0.58" +thiserror = "1.0.64" serde_json = { version = "1.0" } platform-value = { path = "../rs-platform-value" } diff --git a/packages/data-contracts/Cargo.toml b/packages/data-contracts/Cargo.toml index 84cf2284a02..fb0fe7d640f 100644 --- a/packages/data-contracts/Cargo.toml +++ b/packages/data-contracts/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license = "MIT" [dependencies] -thiserror = "1.0.58" +thiserror = "1.0.64" platform-version = { path = "../rs-platform-version" } serde_json = { version = "1.0" } withdrawals-contract = { path = "../withdrawals-contract" } diff --git a/packages/dpns-contract/Cargo.toml b/packages/dpns-contract/Cargo.toml index 8b66395d80c..328a6fa2fa2 100644 --- a/packages/dpns-contract/Cargo.toml +++ b/packages/dpns-contract/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license = "MIT" [dependencies] -thiserror = "1.0.58" +thiserror = "1.0.64" platform-version = { path = "../rs-platform-version" } serde_json = { version = "1.0" } platform-value = { path = "../rs-platform-value" } diff --git a/packages/feature-flags-contract/Cargo.toml b/packages/feature-flags-contract/Cargo.toml index 2bfb4c00875..04abaeacfaf 100644 --- a/packages/feature-flags-contract/Cargo.toml +++ b/packages/feature-flags-contract/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license = "MIT" [dependencies] -thiserror = "1.0.58" +thiserror = "1.0.64" platform-version = { path = "../rs-platform-version" } serde_json = { version = "1.0" } platform-value = { path = "../rs-platform-value" } diff --git a/packages/masternode-reward-shares-contract/Cargo.toml b/packages/masternode-reward-shares-contract/Cargo.toml index 49c81c24dbb..8eb18072879 100644 --- a/packages/masternode-reward-shares-contract/Cargo.toml +++ b/packages/masternode-reward-shares-contract/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license = "MIT" [dependencies] -thiserror = "1.0.58" +thiserror = "1.0.64" platform-version = { path = "../rs-platform-version" } serde_json = { version = "1.0" } platform-value = { path = "../rs-platform-value" } diff --git a/packages/rs-dapi-client/Cargo.toml b/packages/rs-dapi-client/Cargo.toml index c95a2a458df..79beb87fd8e 100644 --- a/packages/rs-dapi-client/Cargo.toml +++ b/packages/rs-dapi-client/Cargo.toml @@ -25,7 +25,7 @@ dapi-grpc = { path = "../dapi-grpc" } futures = "0.3.28" http-serde = { version = "1.1.3", optional = true } rand = { version = "0.8.5", features = ["small_rng"] } -thiserror = "1.0.58" +thiserror = "1.0.64" tracing = "0.1.40" tokio = { version = "1.32.0", default-features = false } sha2 = { version = "0.10", optional = true } diff --git a/packages/rs-dpp/src/errors/consensus/basic/identity/invalid_identity_credit_withdrawal_transition_amount_error.rs b/packages/rs-dpp/src/errors/consensus/basic/identity/invalid_identity_credit_withdrawal_transition_amount_error.rs index 666c8ff2991..11b91aba7e7 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/identity/invalid_identity_credit_withdrawal_transition_amount_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/identity/invalid_identity_credit_withdrawal_transition_amount_error.rs @@ -10,7 +10,7 @@ use bincode::{Decode, Encode}; #[derive( Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize, )] -#[error("Credit withdrawal amount {amount} must be greater or equal to {min_amount}")] +#[error("Credit withdrawal amount {amount} must be greater or equal to {min_amount} and less than {max_amount}")] #[platform_serialize(unversioned)] pub struct InvalidIdentityCreditWithdrawalTransitionAmountError { /* @@ -20,11 +20,16 @@ pub struct InvalidIdentityCreditWithdrawalTransitionAmountError { */ pub amount: u64, pub min_amount: u64, + pub max_amount: u64, } impl InvalidIdentityCreditWithdrawalTransitionAmountError { - pub fn new(amount: u64, min_amount: u64) -> Self { - Self { amount, min_amount } + pub fn new(amount: u64, min_amount: u64, max_amount: u64) -> Self { + Self { + amount, + min_amount, + max_amount, + } } pub fn amount(&self) -> u64 { @@ -34,6 +39,10 @@ impl InvalidIdentityCreditWithdrawalTransitionAmountError { pub fn min_amount(&self) -> u64 { self.min_amount } + + pub fn max_amount(&self) -> u64 { + self.max_amount + } } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/identity/core_script.rs b/packages/rs-dpp/src/identity/core_script.rs index 8aa7fd38a48..ff43e445717 100644 --- a/packages/rs-dpp/src/identity/core_script.rs +++ b/packages/rs-dpp/src/identity/core_script.rs @@ -51,19 +51,23 @@ impl CoreScript { Self::from_bytes(bytes) } - pub fn random_p2pkh(rng: &mut StdRng) -> Self { - Self::new_p2pkh(rng.gen::<[u8; 20]>()) - } - - pub fn random_p2sh(rng: &mut StdRng) -> Self { + pub fn new_p2sh(script_hash: [u8; 20]) -> Self { let mut bytes = vec![ opcodes::all::OP_HASH160.to_u8(), opcodes::all::OP_PUSHBYTES_20.to_u8(), ]; - bytes.append(&mut rng.gen::<[u8; 20]>().to_vec()); + bytes.extend_from_slice(&script_hash); bytes.push(opcodes::all::OP_EQUAL.to_u8()); Self::from_bytes(bytes) } + + pub fn random_p2sh(rng: &mut StdRng) -> Self { + Self::new_p2sh(rng.gen()) + } + + pub fn random_p2pkh(rng: &mut StdRng) -> Self { + Self::new_p2pkh(rng.gen()) + } } impl From> for CoreScript { diff --git a/packages/rs-dpp/src/util/units.rs b/packages/rs-dpp/src/util/units.rs index ca00561cf05..83af5e89769 100644 --- a/packages/rs-dpp/src/util/units.rs +++ b/packages/rs-dpp/src/util/units.rs @@ -27,3 +27,33 @@ macro_rules! dash_to_credits { credits as u64 }}; } + +#[macro_export] +macro_rules! dash_to_duffs { + // The macro takes a string literal representing the Dash amount. + ($dash:expr) => {{ + let dash_str = stringify!($dash); + + // Parsing the input string to separate the whole and fractional parts. + let parts: Vec<&str> = dash_str.split('.').collect(); + let mut credits: u128 = 0; + + // Process the whole number part if it exists. + if let Some(whole) = parts.get(0) { + if let Ok(whole_number) = whole.parse::() { + credits += whole_number * 100_000_000; // Whole Dash amount to credits + } + } + + // Process the fractional part if it exists. + if let Some(fraction) = parts.get(1) { + let fraction_length = fraction.len(); + let fraction_number = fraction.parse::().unwrap_or(0); + // Calculate the multiplier based on the number of digits in the fraction. + let multiplier = 10u128.pow(8 - fraction_length as u32); + credits += fraction_number * multiplier; // Fractional Dash to credits + } + + credits as u64 + }}; +} diff --git a/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/mod.rs b/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/mod.rs new file mode 100644 index 00000000000..b1cd0a477db --- /dev/null +++ b/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/mod.rs @@ -0,0 +1,18 @@ +use crate::fee::Credits; +use crate::withdrawal::daily_withdrawal_limit::v0::daily_withdrawal_limit_v0; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub fn daily_withdrawal_limit( + total_credits_in_platform: Credits, + platform_version: &PlatformVersion, +) -> Result { + match platform_version.dpp.methods.daily_withdrawal_limit { + 0 => Ok(daily_withdrawal_limit_v0(total_credits_in_platform)), + v => Err(ProtocolError::UnknownVersionError(format!( + "Unknown daily_withdrawal_limit version {v}" + ))), + } +} diff --git a/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/v0/mod.rs b/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/v0/mod.rs new file mode 100644 index 00000000000..c50b89e152c --- /dev/null +++ b/packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/v0/mod.rs @@ -0,0 +1,54 @@ +use crate::fee::Credits; + +/// Calculates the daily withdrawal limit based on the total credits available in the platform. +/// +/// The function enforces the following rules: +/// +/// 1. If the total credits are 1000 Dash in Credits or more: +/// - The withdrawal limit is set to 10% of the total credits. +/// 2. If the total credits are between 100 and 999 Dash in Credits: +/// - The withdrawal limit is capped at 100 credits. +/// 3. If the total credits are less than 100 Dash in Credits: +/// - The withdrawal limit is the total available credits, as no more than the available amount can be withdrawn. +/// +/// # Parameters +/// +/// * `total_credits_in_platform`: The total amount of credits available in the platform. +/// +/// # Returns +/// +/// * `Credits`: The calculated daily withdrawal limit based on the available credits. +/// +pub fn daily_withdrawal_limit_v0(total_credits_in_platform: Credits) -> Credits { + if total_credits_in_platform >= 100_000_000_000_000 { + // 1000 Dash + total_credits_in_platform / 10 + } else if total_credits_in_platform >= 10_000_000_000_000 { + // 100 Dash + 10_000_000_000_000 + } else { + total_credits_in_platform + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dash_to_credits; + + #[test] + fn test_daily_withdrawal_limit() { + assert_eq!( + daily_withdrawal_limit_v0(dash_to_credits!(2000)), + dash_to_credits!(200) + ); + assert_eq!( + daily_withdrawal_limit_v0(dash_to_credits!(500)), + dash_to_credits!(100) + ); + assert_eq!( + daily_withdrawal_limit_v0(dash_to_credits!(50)), + dash_to_credits!(50) + ); + } +} diff --git a/packages/rs-dpp/src/withdrawal/mod.rs b/packages/rs-dpp/src/withdrawal/mod.rs index 1aaac3052dd..6ce0f25811d 100644 --- a/packages/rs-dpp/src/withdrawal/mod.rs +++ b/packages/rs-dpp/src/withdrawal/mod.rs @@ -1,3 +1,4 @@ +pub mod daily_withdrawal_limit; #[cfg(feature = "system_contracts")] mod document_try_into_asset_unlock_base_transaction_info; @@ -19,4 +20,5 @@ pub enum Pooling { pub type WithdrawalTransactionIndex = u64; /// Simple type alias for withdrawal transaction with it's index + pub type WithdrawalTransactionIndexAndBytes = (WithdrawalTransactionIndex, Vec); diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 6ca9d43b972..4e57a1c9f19 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -23,7 +23,7 @@ drive = { path = "../rs-drive", default-features = false, features = [ "server", "grovedb_operations_logging", ] } -thiserror = "1.0.58" +thiserror = "1.0.64" rand = "0.8.5" tempfile = "3.3.0" hex = "0.4.3" diff --git a/packages/rs-drive-abci/src/execution/engine/initialization/init_chain/v0/mod.rs b/packages/rs-drive-abci/src/execution/engine/initialization/init_chain/v0/mod.rs index d190c5c0551..3d0bc673a1d 100644 --- a/packages/rs-drive-abci/src/execution/engine/initialization/init_chain/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/initialization/init_chain/v0/mod.rs @@ -13,7 +13,7 @@ use crate::platform_types::platform_state::PlatformState; use crate::platform_types::validator_set::ValidatorSetExt; use dpp::version::PlatformVersion; use std::sync::Arc; -use tenderdash_abci::proto::abci::{RequestInitChain, ResponseInitChain, ValidatorSetUpdate}; +use tenderdash_abci::proto::abci::{RequestInitChain, ResponseInitChain}; use tenderdash_abci::proto::google::protobuf::Timestamp; use tenderdash_abci::proto::serializers::timestamp::FromMilis; diff --git a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs index a8420b34b04..e2149678b3a 100644 --- a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs @@ -91,8 +91,19 @@ Your software version: {}, latest supported protocol version: {}."#, ); }; - // Set current protocol version to the block platform state - block_platform_state.set_current_protocol_version_in_consensus(next_protocol_version); + let old_protocol_version = block_platform_state.current_protocol_version_in_consensus(); + + if old_protocol_version != next_protocol_version { + // Set current protocol version to the block platform state + block_platform_state + .set_current_protocol_version_in_consensus(next_protocol_version); + // This is for events like adding stuff to the root tree, or making structural changes/fixes + self.perform_events_on_first_block_of_protocol_change( + transaction, + old_protocol_version, + next_platform_version, + )?; + } next_platform_version } else { diff --git a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs index 63e1437f7fa..0b20f41d880 100644 --- a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs @@ -326,6 +326,17 @@ where platform_version, )?; + // Cleans up the expired locks for withdrawal amounts + // This is for example when we make a withdrawal for 30 Dash + // But we can only withdraw 1000 Dash a day + // after the withdrawal we should only be able to withdraw 970 Dash + // But 24 hours later that locked 30 comes back + self.clean_up_expired_locks_of_withdrawal_amounts( + &block_info, + transaction, + platform_version, + )?; + // Create a new block execution context let mut block_execution_context: BlockExecutionContext = diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/mod.rs index b2e1e826d4e..588f34d1697 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/mod.rs @@ -1,2 +1,3 @@ mod check_for_desired_protocol_upgrade; +mod perform_events_on_first_block_of_protocol_change; mod upgrade_protocol_version; diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs new file mode 100644 index 00000000000..659a7e94aa0 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/mod.rs @@ -0,0 +1,66 @@ +mod v0; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use dpp::version::PlatformVersion; +use dpp::version::ProtocolVersion; +use drive::grovedb::Transaction; + +impl Platform { + /// Executes protocol-specific events on the first block after a protocol version change. + /// + /// This function is triggered when there is a protocol version upgrade detected in the network. + /// It checks if the current protocol version has transitioned from an earlier version to version 4, + /// and if so, performs the necessary setup or migration tasks associated with version 4. + /// + /// Currently, the function handles the transition to version 4 by initializing new structures + /// or states required for the new protocol version. + /// + /// # Parameters + /// + /// * `transaction`: A reference to the transaction context in which the changes should be applied. + /// * `previous_protocol_version`: The protocol version prior to the upgrade. + /// * `platform_version`: The current platform version containing the updated protocol version and relevant configuration details. + /// + /// # Returns + /// + /// * `Ok(())`: If all events related to the protocol change were successfully executed. + /// * `Err(Error)`: If there was an issue executing the protocol-specific events. + /// + /// # Versioning + /// + /// This function uses the `platform_version` parameter to determine which version-specific implementation + /// of the protocol change events should be executed: + /// + /// - If the version is `0`, it calls the `perform_events_on_first_block_of_protocol_change_v0` function, + /// which contains the logic for version `0`. + /// - If no version is specified (`None`), the function does nothing and returns `Ok(())`. + /// - If a different version is specified, it returns an error indicating an unknown version mismatch. + /// + pub fn perform_events_on_first_block_of_protocol_change( + &self, + transaction: &Transaction, + previous_protocol_version: ProtocolVersion, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .protocol_upgrade + .perform_events_on_first_block_of_protocol_change + { + Some(0) => self.perform_events_on_first_block_of_protocol_change_v0( + transaction, + previous_protocol_version, + platform_version, + ), + None => return Ok(()), + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "perform_events_on_first_block_of_protocol_change".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs new file mode 100644 index 00000000000..d732893b80a --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -0,0 +1,73 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use dpp::version::PlatformVersion; +use dpp::version::ProtocolVersion; +use drive::drive::identity::withdrawals::paths::{ + get_withdrawal_root_path, WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, +}; +use drive::grovedb::{Element, Transaction}; + +impl Platform { + /// Executes protocol-specific events on the first block after a protocol version change. + /// + /// This function is triggered when there is a protocol version upgrade detected in the network. + /// It checks if the current protocol version has transitioned from an earlier version to version 4, + /// and if so, performs the necessary setup or migration tasks associated with version 4. + /// + /// Currently, the function handles the transition to version 4 by initializing new structures + /// or states required for the new protocol version. + /// + /// # Parameters + /// + /// * `transaction`: A reference to the transaction context in which the changes should be applied. + /// * `previous_protocol_version`: The protocol version prior to the upgrade. + /// * `platform_version`: The current platform version containing the updated protocol version and relevant configuration details. + /// + /// # Returns + /// + /// * `Ok(())`: If all events related to the protocol change were successfully executed. + /// * `Err(Error)`: If there was an issue executing the protocol-specific events. + pub(super) fn perform_events_on_first_block_of_protocol_change_v0( + &self, + transaction: &Transaction, + previous_protocol_version: ProtocolVersion, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + if previous_protocol_version < 4 && platform_version.protocol_version >= 4 { + self.transition_to_version_4(transaction, platform_version); + } + + Ok(()) + } + + /// Initializes an empty sum tree for withdrawal transactions required for protocol version 4. + /// + /// This function is called during the transition to protocol version 4 to set up + /// an empty sum tree at the specified path if it does not already exist. + /// + /// # Parameters + /// + /// * `transaction`: A reference to the transaction context in which the changes should be applied. + /// * `platform_version`: The current platform version containing the updated protocol version and relevant configuration details. + /// + /// # Returns + /// + /// * `Ok(())`: If the transition to version 4 was successful. + /// * `Err(Error)`: If there was an issue creating or updating the necessary data structures. + fn transition_to_version_4( + &self, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let path = get_withdrawal_root_path(); + self.drive.grove_insert_if_not_exists( + (&path).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + Element::empty_sum_tree(), + Some(transaction), + None, + &platform_version.drive, + )?; + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/append_signatures_and_broadcast_withdrawal_transactions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/append_signatures_and_broadcast_withdrawal_transactions/v0/mod.rs index 6d1deb5c82d..d84db008489 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/append_signatures_and_broadcast_withdrawal_transactions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/append_signatures_and_broadcast_withdrawal_transactions/v0/mod.rs @@ -10,7 +10,7 @@ use dashcore_rpc::Error as CoreRPCError; use dpp::dashcore::bls_sig_utils::BLSSignature; use dpp::dashcore::transaction::special_transaction::TransactionPayload::AssetUnlockPayloadType; use dpp::dashcore::{consensus, Transaction, Txid}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use std::fs::{self, File}; use std::io::Write; diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/mod.rs index 0f0ff66a740..fb6717b7b84 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/mod.rs @@ -4,6 +4,7 @@ use crate::platform_types::platform::Platform; use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::document::Document; +use dpp::fee::Credits; use dpp::version::PlatformVersion; use dpp::withdrawal::{WithdrawalTransactionIndex, WithdrawalTransactionIndexAndBytes}; @@ -35,7 +36,7 @@ where start_index: WithdrawalTransactionIndex, block_info: &BlockInfo, platform_version: &PlatformVersion, - ) -> Result, Error> { + ) -> Result<(Vec, Credits), Error> { match platform_version .drive_abci .methods diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/v0/mod.rs index 2a1e5f8a743..13da6e72f1f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/build_untied_withdrawal_transactions_from_documents/v0/mod.rs @@ -3,7 +3,9 @@ use dpp::block::block_info::BlockInfo; use dpp::data_contracts::withdrawals_contract; use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; use dpp::document::document_methods::DocumentMethodsV0; -use dpp::document::{Document, DocumentV0Setters}; +use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; +use dpp::fee::Credits; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::withdrawal::{WithdrawalTransactionIndex, WithdrawalTransactionIndexAndBytes}; use crate::{ @@ -24,20 +26,35 @@ where start_index: WithdrawalTransactionIndex, block_info: &BlockInfo, platform_version: &PlatformVersion, - ) -> Result, Error> { - documents - .iter_mut() - .enumerate() - .map(|(i, document)| { + ) -> Result<(Vec, Credits), Error> { + documents.iter_mut().enumerate().try_fold( + (Vec::new(), 0u64), // Start with an empty vector for transactions and 0 for total amount. + |(mut transactions, mut total_amount), (i, document)| { + // Calculate the transaction index. let transaction_index = start_index + i as WithdrawalTransactionIndex; + // Convert the document into the withdrawal transaction information. let withdrawal_transaction = document.try_into_asset_unlock_base_transaction_info( transaction_index, platform_version, )?; + // Create a buffer to hold the encoded transaction. let mut transaction_buffer: Vec = vec![]; + // Get the withdrawal amount from the document properties. + let amount: u64 = document + .properties() + .get_integer(withdrawal::properties::AMOUNT)?; + + // Add the amount to the total, checking for overflow. + total_amount = total_amount.checked_add(amount).ok_or_else(|| { + Error::Execution(ExecutionError::Overflow( + "Overflow while calculating total amount", + )) + })?; + + // Consensus encode the withdrawal transaction into the buffer. withdrawal_transaction .consensus_encode(&mut transaction_buffer) .map_err(|_| { @@ -46,24 +63,28 @@ where )) })?; + // Update the document properties. document.set_u64(withdrawal::properties::TRANSACTION_INDEX, transaction_index); - document.set_u8( withdrawal::properties::STATUS, withdrawals_contract::WithdrawalStatus::POOLED as u8, ); - document.set_updated_at(Some(block_info.time_ms)); + // Increment the document revision, handle error if it fails. document.increment_revision().map_err(|_| { - Error::Execution(ExecutionError::CorruptedCodeExecution( - "Could not increment document revision", + Error::Execution(ExecutionError::Overflow( + "Overflow when adding to document revision for withdrawals", )) })?; - Ok((transaction_index, transaction_buffer)) - }) - .collect() + // Add the transaction index and encoded transaction to the result. + transactions.push((transaction_index, transaction_buffer)); + + // Return the updated accumulator. + Ok((transactions, total_amount)) + }, + ) } } @@ -160,7 +181,7 @@ mod tests { let block_info = BlockInfo::default_with_time(50); - let transactions = platform + let (transactions, credits) = platform .build_untied_withdrawal_transactions_from_documents_v0( &mut documents, 50, @@ -190,6 +211,8 @@ mod tests { ), ] ); + + assert_eq!(credits, 2000); } } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/mod.rs new file mode 100644 index 00000000000..837ffd1bf9f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/mod.rs @@ -0,0 +1,58 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; + +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; + +use dpp::version::PlatformVersion; +use drive::grovedb::Transaction; + +mod v0; + +impl Platform +where + C: CoreRPCLike, +{ + /// Cleans up expired locks of withdrawal amounts based on the protocol version. + /// + /// This function determines the appropriate versioned function to call for cleaning up expired + /// withdrawal locks according to the provided platform version. It deletes expired withdrawal locks + /// that have surpassed their allowed time limit, ensuring that only valid withdrawal entries remain. + /// + /// # Parameters + /// * `block_info`: Information about the current block, including the timestamp used to identify expired locks. + /// * `transaction`: The transaction under which this operation should be performed. + /// * `platform_version`: The version of the platform to ensure compatibility with the appropriate cleanup method. + /// + /// # Returns + /// * `Ok(())`: If the cleanup was performed successfully. + /// * `Err(Error::Execution(ExecutionError::UnknownVersionMismatch))`: If the platform version does not match known versions. + /// + /// # Errors + /// Returns an error if the platform version is unknown or if the cleanup process encounters an issue. + pub fn clean_up_expired_locks_of_withdrawal_amounts( + &self, + block_info: &BlockInfo, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .withdrawals + .cleanup_expired_locks_of_withdrawal_amounts + { + 0 => self.cleanup_expired_locks_of_withdrawal_amounts_v0( + block_info, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "cleanup_expired_locks_of_withdrawal_amounts".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/v0/mod.rs new file mode 100644 index 00000000000..74315a1d6c7 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/cleanup_expired_locks_of_withdrawal_amounts/v0/mod.rs @@ -0,0 +1,69 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; + +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; + +use dpp::version::PlatformVersion; +use drive::drive::identity::withdrawals::paths::get_withdrawal_transactions_sum_tree_path_vec; +use drive::grovedb::{PathQuery, QueryItem, Transaction}; +use drive::util::grove_operations::BatchDeleteApplyType; + +impl Platform +where + C: CoreRPCLike, +{ + pub(super) fn cleanup_expired_locks_of_withdrawal_amounts_v0( + &self, + block_info: &BlockInfo, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + if platform_version + .drive_abci + .withdrawal_constants + .cleanup_expired_locks_of_withdrawal_amounts_limit + == 0 + { + // No clean up + return Ok(()); + } + let mut batch_operations = vec![]; + + let sum_path = get_withdrawal_transactions_sum_tree_path_vec(); + + let mut path_query = PathQuery::new_single_query_item( + sum_path, + QueryItem::RangeTo(..block_info.time_ms.to_be_bytes().to_vec()), + ); + + path_query.query.limit = Some( + platform_version + .drive_abci + .withdrawal_constants + .cleanup_expired_locks_of_withdrawal_amounts_limit, + ); + + self.drive.batch_delete_items_in_path_query( + &path_query, + true, + // we know that we are not deleting a subtree + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some((false, false)), + }, + Some(transaction), + &mut batch_operations, + &platform_version.drive, + )?; + + self.drive.apply_batch_low_level_drive_operations( + None, + Some(transaction), + batch_operations, + &mut vec![], + &platform_version.drive, + )?; + + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/dequeue_and_build_unsigned_withdrawal_transactions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/dequeue_and_build_unsigned_withdrawal_transactions/v0/mod.rs index c2506d4f7d8..7a37525de68 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/dequeue_and_build_unsigned_withdrawal_transactions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/dequeue_and_build_unsigned_withdrawal_transactions/v0/mod.rs @@ -42,7 +42,7 @@ where ) -> Result { let mut drive_operations: Vec = vec![]; - // Get withdrawal_transactions_per_block_limit (normally 16) latest withdrawal transactions from the queue + // Get withdrawal_transactions_per_block_limit (normally 4) latest withdrawal transactions from the queue let untied_withdrawal_transactions = self.drive.dequeue_untied_withdrawal_transactions( platform_version .system_limits @@ -150,8 +150,8 @@ where document.set_updated_at(Some(block_info.time_ms)); document.increment_revision().map_err(|_| { - Error::Execution(ExecutionError::CorruptedCodeExecution( - "Could not increment document revision", + Error::Execution(ExecutionError::Overflow( + "Overflow when adding to document revision for withdrawals", )) })?; diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/mod.rs index 75412110566..0de6cbfcef2 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/mod.rs @@ -1,5 +1,6 @@ pub(in crate::execution) mod append_signatures_and_broadcast_withdrawal_transactions; pub(in crate::execution) mod build_untied_withdrawal_transactions_from_documents; +pub(in crate::execution) mod cleanup_expired_locks_of_withdrawal_amounts; pub(in crate::execution) mod dequeue_and_build_unsigned_withdrawal_transactions; pub(in crate::execution) mod fetch_transactions_block_inclusion_status; pub(in crate::execution) mod pool_withdrawals_into_transactions_queue; diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v0/mod.rs index 00f1c8a7615..2f353c8225a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v0/mod.rs @@ -1,13 +1,13 @@ use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; - +use dpp::document::DocumentV0Getters; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::version::PlatformVersion; use drive::grovedb::TransactionArg; use dpp::system_data_contracts::withdrawals_contract; use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; -use drive::config::DEFAULT_QUERY_LIMIT; use crate::platform_types::platform_state::v0::PlatformStateV0Methods; use crate::platform_types::platform_state::PlatformState; @@ -44,9 +44,11 @@ where ); return Ok(()); } - let mut documents = self.drive.fetch_oldest_withdrawal_documents_by_status( + let documents = self.drive.fetch_oldest_withdrawal_documents_by_status( withdrawals_contract::WithdrawalStatus::QUEUED.into(), - DEFAULT_QUERY_LIMIT, + platform_version + .system_limits + .withdrawal_transactions_per_block_limit, transaction, platform_version, )?; @@ -55,16 +57,56 @@ where return Ok(()); } + // Only take documents up to the withdrawal amount + let current_withdrawal_limit = self + .drive + .calculate_current_withdrawal_limit(transaction, platform_version)?; + + // Only process documents up to the current withdrawal limit. + let mut total_withdrawal_amount = 0u64; + + // Iterate over the documents and accumulate their withdrawal amounts. + let mut documents_to_process = vec![]; + for document in documents { + // Get the withdrawal amount from the document properties. + let amount: u64 = document + .properties() + .get_integer(withdrawal::properties::AMOUNT)?; + + // Check if adding this amount would exceed the current withdrawal limit. + let potential_total_withdrawal_amount = + total_withdrawal_amount.checked_add(amount).ok_or_else(|| { + Error::Execution(ExecutionError::Overflow( + "overflow in total withdrawal amount", + )) + })?; + + if potential_total_withdrawal_amount > current_withdrawal_limit { + // If adding this withdrawal would exceed the limit, stop processing further. + break; + } + + total_withdrawal_amount = potential_total_withdrawal_amount; + + // Add this document to the list of documents to be processed. + documents_to_process.push(document); + } + + if documents_to_process.is_empty() { + return Ok(()); + } + let start_transaction_index = self .drive .fetch_next_withdrawal_transaction_index(transaction, platform_version)?; - let withdrawal_transactions = self.build_untied_withdrawal_transactions_from_documents( - &mut documents, - start_transaction_index, - block_info, - platform_version, - )?; + let (withdrawal_transactions, total_amount) = self + .build_untied_withdrawal_transactions_from_documents( + &mut documents_to_process, + start_transaction_index, + block_info, + platform_version, + )?; let withdrawal_transactions_count = withdrawal_transactions.len(); @@ -73,6 +115,7 @@ where self.drive .add_enqueue_untied_withdrawal_transaction_operations( withdrawal_transactions, + total_amount, &mut drive_operations, platform_version, )?; @@ -88,7 +131,7 @@ where tracing::debug!( "Pooled {} withdrawal documents into {} transactions with indices from {} to {}", - documents.len(), + documents_to_process.len(), withdrawal_transactions_count, start_transaction_index, end_transaction_index, @@ -97,7 +140,7 @@ where let withdrawals_contract = self.drive.cache.system_data_contracts.load_withdrawals(); self.drive.add_update_multiple_documents_operations( - &documents, + &documents_to_process, &withdrawals_contract, withdrawals_contract .document_type_for_name(withdrawal::NAME) @@ -142,6 +185,7 @@ mod tests { use dpp::platform_value::platform_value; use dpp::system_data_contracts::load_system_data_contract; use dpp::version::PlatformVersion; + use drive::config::DEFAULT_QUERY_LIMIT; #[test] fn test_pooling() { diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v1/mod.rs index 34b3174933f..cf7c3cec299 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/pool_withdrawals_into_transactions_queue/v1/mod.rs @@ -1,13 +1,13 @@ use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; - +use dpp::document::DocumentV0Getters; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::version::PlatformVersion; use drive::grovedb::TransactionArg; use dpp::system_data_contracts::withdrawals_contract; use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; -use drive::config::DEFAULT_QUERY_LIMIT; use crate::{ error::{execution::ExecutionError, Error}, @@ -20,15 +20,19 @@ where C: CoreRPCLike, { /// Pool withdrawal documents into transactions + /// Version 1 changes on Version 0, by not having the Core 2 Quorum limit. + /// We should switch to Version 1 once Core has fixed the issue pub(super) fn pool_withdrawals_into_transactions_queue_v1( &self, block_info: &BlockInfo, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(), Error> { - let mut documents = self.drive.fetch_oldest_withdrawal_documents_by_status( + let documents = self.drive.fetch_oldest_withdrawal_documents_by_status( withdrawals_contract::WithdrawalStatus::QUEUED.into(), - DEFAULT_QUERY_LIMIT, + platform_version + .system_limits + .withdrawal_transactions_per_block_limit, transaction, platform_version, )?; @@ -37,16 +41,56 @@ where return Ok(()); } + // Only take documents up to the withdrawal amount + let current_withdrawal_limit = self + .drive + .calculate_current_withdrawal_limit(transaction, platform_version)?; + + // Only process documents up to the current withdrawal limit. + let mut total_withdrawal_amount = 0u64; + + // Iterate over the documents and accumulate their withdrawal amounts. + let mut documents_to_process = vec![]; + for document in documents { + // Get the withdrawal amount from the document properties. + let amount: u64 = document + .properties() + .get_integer(withdrawal::properties::AMOUNT)?; + + // Check if adding this amount would exceed the current withdrawal limit. + let potential_total_withdrawal_amount = + total_withdrawal_amount.checked_add(amount).ok_or_else(|| { + Error::Execution(ExecutionError::Overflow( + "overflow in total withdrawal amount", + )) + })?; + + if potential_total_withdrawal_amount > current_withdrawal_limit { + // If adding this withdrawal would exceed the limit, stop processing further. + break; + } + + total_withdrawal_amount = potential_total_withdrawal_amount; + + // Add this document to the list of documents to be processed. + documents_to_process.push(document); + } + + if documents_to_process.is_empty() { + return Ok(()); + } + let start_transaction_index = self .drive .fetch_next_withdrawal_transaction_index(transaction, platform_version)?; - let withdrawal_transactions = self.build_untied_withdrawal_transactions_from_documents( - &mut documents, - start_transaction_index, - block_info, - platform_version, - )?; + let (withdrawal_transactions, total_amount) = self + .build_untied_withdrawal_transactions_from_documents( + &mut documents_to_process, + start_transaction_index, + block_info, + platform_version, + )?; let withdrawal_transactions_count = withdrawal_transactions.len(); @@ -55,6 +99,7 @@ where self.drive .add_enqueue_untied_withdrawal_transaction_operations( withdrawal_transactions, + total_amount, &mut drive_operations, platform_version, )?; @@ -70,7 +115,7 @@ where tracing::debug!( "Pooled {} withdrawal documents into {} transactions with indices from {} to {}", - documents.len(), + documents_to_process.len(), withdrawal_transactions_count, start_transaction_index, end_transaction_index, @@ -79,7 +124,7 @@ where let withdrawals_contract = self.drive.cache.system_data_contracts.load_withdrawals(); self.drive.add_update_multiple_documents_operations( - &documents, + &documents_to_process, &withdrawals_contract, withdrawals_contract .document_type_for_name(withdrawal::NAME) @@ -124,6 +169,7 @@ mod tests { use dpp::platform_value::platform_value; use dpp::system_data_contracts::load_system_data_contract; use dpp::version::PlatformVersion; + use drive::config::DEFAULT_QUERY_LIMIT; #[test] fn test_pooling() { diff --git a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/update_broadcasted_withdrawal_statuses/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/update_broadcasted_withdrawal_statuses/v0/mod.rs index 2eba2e228c8..81210da62e7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/withdrawals/update_broadcasted_withdrawal_statuses/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/withdrawals/update_broadcasted_withdrawal_statuses/v0/mod.rs @@ -22,8 +22,6 @@ use crate::{ rpc::core::CoreRPCLike, }; -const NUMBER_OF_BLOCKS_BEFORE_EXPIRED: u32 = 48; - impl Platform where C: CoreRPCLike, @@ -109,7 +107,12 @@ where ); WithdrawalStatus::COMPLETE - } else if block_height_difference > NUMBER_OF_BLOCKS_BEFORE_EXPIRED { + } else if block_height_difference + > platform_version + .drive_abci + .withdrawal_constants + .core_expiration_blocks + { tracing::debug!( transaction_sign_height, "Withdrawal with transaction index {} is marked as expired", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/mod.rs index f74cb363924..a1ed43702c5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/mod.rs @@ -71,7 +71,7 @@ impl StateTransitionBasicStructureValidationV0 for IdentityCreditWithdrawalTrans // Returns not supported self.validate_basic_structure_v0(platform_version) } - Some(1) => self.validate_basic_structure_v1(), + Some(1) => self.validate_basic_structure_v1(platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "identity credit withdrawal transition: validate_basic_structure" .to_string(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/structure/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/structure/v1/mod.rs index daaf9f7e3d8..7f018630a84 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/structure/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_credit_withdrawal/structure/v1/mod.rs @@ -13,23 +13,31 @@ use dpp::state_transition::identity_credit_withdrawal_transition::{ }; use dpp::util::is_fibonacci_number::is_fibonacci_number; use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; use dpp::withdrawal::Pooling; pub(in crate::execution::validation::state_transition::state_transitions::identity_credit_withdrawal) trait IdentityCreditWithdrawalStateTransitionStructureValidationV1 { - fn validate_basic_structure_v1(&self) -> Result; + fn validate_basic_structure_v1(&self, platform_version: &PlatformVersion) -> Result; } impl IdentityCreditWithdrawalStateTransitionStructureValidationV1 for IdentityCreditWithdrawalTransition { - fn validate_basic_structure_v1(&self) -> Result { + fn validate_basic_structure_v1( + &self, + platform_version: &PlatformVersion, + ) -> Result { let mut result = SimpleConsensusValidationResult::default(); - if self.amount() < MIN_WITHDRAWAL_AMOUNT { + let amount = self.amount(); + if amount < MIN_WITHDRAWAL_AMOUNT + || amount > platform_version.system_limits.max_withdrawal_amount + { result.add_error(ConsensusError::from( InvalidIdentityCreditWithdrawalTransitionAmountError::new( self.amount(), MIN_WITHDRAWAL_AMOUNT, + platform_version.system_limits.max_withdrawal_amount, ), )); } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs index 462ce2c3272..e937924c94d 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs @@ -25,7 +25,6 @@ use indexmap::IndexMap; use crate::config::PlatformConfig; use crate::error::execution::ExecutionError; use crate::platform_types::signature_verification_quorum_set::SignatureVerificationQuorumSet; -use crate::platform_types::validator_set::v0::ValidatorSetV0Getters; use dpp::block::block_info::BlockInfo; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::util::hash::hash_double; diff --git a/packages/rs-drive-abci/tests/strategy_tests/execution.rs b/packages/rs-drive-abci/tests/strategy_tests/execution.rs index 133b281446b..8b6b5767ec3 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/execution.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/execution.rs @@ -1209,5 +1209,6 @@ pub(crate) fn continue_chain_for_strategy( validator_set_updates, state_transition_results_per_block, instant_lock_quorums, + signer, } } diff --git a/packages/rs-drive-abci/tests/strategy_tests/main.rs b/packages/rs-drive-abci/tests/strategy_tests/main.rs index 21d269d6be7..98e2b518a2c 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/main.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/main.rs @@ -47,6 +47,7 @@ mod tests { use crate::execution::{continue_chain_for_strategy, run_chain_for_strategy}; use crate::query::QueryStrategy; use crate::strategy::{FailureStrategy, MasternodeListChangesStrategy}; + use assert_matches::assert_matches; use dashcore_rpc::dashcore::hashes::Hash; use dashcore_rpc::dashcore::BlockHash; use dashcore_rpc::json::QuorumType; @@ -80,6 +81,7 @@ mod tests { use rand::SeedableRng; use tenderdash_abci::proto::abci::{RequestInfo, ResponseInfo}; + use dpp::dash_to_duffs; use dpp::data_contract::document_type::v0::random_document_type::{ FieldMinMaxBounds, FieldTypeWeights, RandomDocumentTypeParameters, }; @@ -1247,6 +1249,16 @@ mod tests { let outcome = run_chain_for_strategy(&mut platform, 1, strategy, config, 15, &mut None); + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + outcome .abci_app .platform @@ -2325,6 +2337,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -2458,6 +2471,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -2581,6 +2595,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, @@ -2750,6 +2765,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -2915,6 +2931,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -2976,7 +2993,7 @@ mod tests { strategy: Strategy { start_contracts: vec![], operations: vec![Operation { - op_type: OperationType::IdentityTopUp, + op_type: OperationType::IdentityTopUp(dash_to_duffs!(1)..=dash_to_duffs!(1)), frequency: Frequency { times_per_block_range: 1..3, chance_per_block: None, @@ -3233,6 +3250,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -3419,6 +3437,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -3581,6 +3600,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -3906,6 +3926,7 @@ mod tests { [(SecurityLevel::CRITICAL, vec![KeyType::ECDSA_SECP256K1])].into(), )] .into(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, @@ -3975,6 +3996,7 @@ mod tests { }, start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, ..Default::default() diff --git a/packages/rs-drive-abci/tests/strategy_tests/strategy.rs b/packages/rs-drive-abci/tests/strategy_tests/strategy.rs index 13f22b16f89..667b8468688 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/strategy.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/strategy.rs @@ -14,7 +14,8 @@ use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; use strategy_tests::frequency::Frequency; use strategy_tests::operations::FinalizeBlockOperation::IdentityAddKeys; use strategy_tests::operations::{ - DocumentAction, DocumentOp, FinalizeBlockOperation, IdentityUpdateOp, OperationType, + AmountRange, DocumentAction, DocumentOp, FinalizeBlockOperation, IdentityUpdateOp, + OperationType, }; use dpp::document::DocumentV0Getters; @@ -40,9 +41,10 @@ use drive_abci::rpc::core::MockCoreRPCLike; use rand::prelude::{IteratorRandom, SliceRandom, StdRng}; use rand::Rng; use strategy_tests::Strategy; -use strategy_tests::transitions::{create_state_transitions_for_identities, create_state_transitions_for_identities_and_proofs, instant_asset_lock_proof_fixture}; +use strategy_tests::transitions::{create_state_transitions_for_identities, create_state_transitions_for_identities_and_proofs, instant_asset_lock_proof_fixture, instant_asset_lock_proof_fixture_with_dynamic_range}; use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::ops::RangeInclusive; use std::str::FromStr; use tenderdash_abci::proto::abci::{ExecTxResult, ValidatorSetUpdate}; use dpp::dashcore::hashes::Hash; @@ -1118,7 +1120,7 @@ impl NetworkStrategy { operations.push(document_batch_transition); } } - OperationType::IdentityTopUp if !current_identities.is_empty() => { + OperationType::IdentityTopUp(amount) if !current_identities.is_empty() => { let indices: Vec = (0..current_identities.len()).choose_multiple(rng, count as usize); let random_identities: Vec<&Identity> = indices @@ -1130,6 +1132,7 @@ impl NetworkStrategy { operations.push(self.create_identity_top_up_transition( rng, random_identity, + amount, instant_lock_quorums, &platform.config, platform_version, @@ -1177,7 +1180,7 @@ impl NetworkStrategy { } } } - OperationType::IdentityWithdrawal if !current_identities.is_empty() => { + OperationType::IdentityWithdrawal(amount) if !current_identities.is_empty() => { let indices: Vec = (0..current_identities.len()).choose_multiple(rng, count as usize); for index in indices { @@ -1185,6 +1188,7 @@ impl NetworkStrategy { let state_transition = strategy_tests::transitions::create_identity_withdrawal_transition( random_identity, + amount.clone(), identity_nonce_counter, signer, rng, @@ -1491,6 +1495,7 @@ impl NetworkStrategy { ) -> Vec<(Identity, StateTransition)> { let key_count = self.strategy.identity_inserts.start_keys as KeyID; let extra_keys = &self.strategy.identity_inserts.extra_keys; + let balance_range = &self.strategy.identity_inserts.start_balance_range; let (mut identities, mut keys) = Identity::random_identities_with_private_keys_with_rng::< Vec<_>, @@ -1524,6 +1529,7 @@ impl NetworkStrategy { if self.sign_instant_locks { let identities_with_proofs = create_signed_instant_asset_lock_proofs_for_identities( identities, + balance_range, rng, instant_lock_quorums, platform_config, @@ -1536,7 +1542,13 @@ impl NetworkStrategy { platform_version, ) } else { - create_state_transitions_for_identities(identities, signer, rng, platform_version) + create_state_transitions_for_identities( + identities, + balance_range, + signer, + rng, + platform_version, + ) } } @@ -1545,6 +1557,7 @@ impl NetworkStrategy { &self, rng: &mut StdRng, identity: &Identity, + amount_range: &AmountRange, instant_lock_quorums: &Quorums, platform_config: &PlatformConfig, platform_version: &PlatformVersion, @@ -1554,8 +1567,11 @@ impl NetworkStrategy { .unwrap(); let sk: [u8; 32] = pk.try_into().unwrap(); let secret_key = SecretKey::from_str(hex::encode(sk).as_str()).unwrap(); - let mut asset_lock_proof = - instant_asset_lock_proof_fixture(PrivateKey::new(secret_key, Network::Dash)); + let mut asset_lock_proof = instant_asset_lock_proof_fixture_with_dynamic_range( + PrivateKey::new(secret_key, Network::Dash), + amount_range, + rng, + ); // Sign transaction and update signature in instant lock proof if self.sign_instant_locks { @@ -1636,6 +1652,7 @@ pub struct ChainExecutionOutcome<'a> { /// height to the validator set update at that height pub validator_set_updates: BTreeMap, pub state_transition_results_per_block: BTreeMap>, + pub signer: SimpleSigner, } impl<'a> ChainExecutionOutcome<'a> { @@ -1667,6 +1684,7 @@ pub struct ChainExecutionParameters { fn create_signed_instant_asset_lock_proofs_for_identities( identities: Vec, + balance_range: &RangeInclusive, rng: &mut StdRng, instant_lock_quorums: &Quorums, platform_config: &PlatformConfig, @@ -1691,7 +1709,11 @@ fn create_signed_instant_asset_lock_proofs_for_identities( let secret_key = SecretKey::from_str(hex::encode(pk_fixed).as_str()).unwrap(); let private_key = PrivateKey::new(secret_key, Network::Dash); - let mut asset_lock_proof = instant_asset_lock_proof_fixture(private_key); + let mut asset_lock_proof = instant_asset_lock_proof_fixture_with_dynamic_range( + private_key, + balance_range, + rng, + ); // Sign transaction and update instant lock let AssetLockProof::Instant(InstantAssetLockProof { instant_lock, .. }) = diff --git a/packages/rs-drive-abci/tests/strategy_tests/voting_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/voting_tests.rs index c27e5e16f51..e14f8d7b1b0 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/voting_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/voting_tests.rs @@ -24,6 +24,7 @@ mod tests { use dapi_grpc::platform::v0::get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::FinishedVoteInfo; use dapi_grpc::platform::v0::get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::finished_vote_info::FinishedVoteOutcome; use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; + use dpp::dash_to_duffs; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::state_transition::StateTransition; use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; @@ -80,6 +81,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, @@ -363,6 +365,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, @@ -634,6 +637,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, @@ -986,6 +990,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, @@ -1350,6 +1355,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &simple_signer, &mut rng, platform_version, diff --git a/packages/rs-drive-abci/tests/strategy_tests/withdrawal_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/withdrawal_tests.rs index 6113c154740..e0c33984174 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/withdrawal_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/withdrawal_tests.rs @@ -12,7 +12,14 @@ mod tests { use dpp::data_contracts::withdrawals_contract; use dpp::identity::{KeyType, Purpose, SecurityLevel}; use dpp::withdrawal::WithdrawalTransactionIndex; + use dpp::{dash_to_credits, dash_to_duffs}; use drive::config::DEFAULT_QUERY_LIMIT; + use drive::drive::balances::TOTAL_SYSTEM_CREDITS_STORAGE_KEY; + use drive::drive::identity::withdrawals::paths::{ + get_withdrawal_root_path, WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + }; + use drive::drive::system::misc_path; + use drive::util::grove_operations::DirectQueryType; use drive_abci::config::{ ChainLockConfig, ExecutionConfig, InstantLockConfig, PlatformConfig, PlatformTestConfig, ValidatorSetConfig, @@ -32,29 +39,20 @@ mod tests { } #[test] - fn run_chain_top_up_and_withdraw_from_identities() { + fn run_chain_withdraw_from_identities() { // TEST_PLATFORM_V3 is like v4, but without the single quorum can sign withdrawals restriction let platform_version = PlatformVersion::get(TEST_PLATFORM_V3.protocol_version) .expect("expected to get platform version"); - let strategy = NetworkStrategy { + let start_strategy = NetworkStrategy { strategy: Strategy { start_contracts: vec![], - operations: vec![ - Operation { - op_type: OperationType::IdentityTopUp, - frequency: Frequency { - times_per_block_range: 1..4, - chance_per_block: None, - }, - }, - Operation { - op_type: OperationType::IdentityWithdrawal, - frequency: Frequency { - times_per_block_range: 1..4, - chance_per_block: None, - }, + operations: vec![Operation { + op_type: OperationType::IdentityTopUp(dash_to_duffs!(10)..=dash_to_duffs!(10)), + frequency: Frequency { + times_per_block_range: 1..4, + chance_per_block: None, }, - ], + }], start_identities: StartIdentities::default(), identity_inserts: IdentityInsertInfo { frequency: Frequency { @@ -67,6 +65,7 @@ mod tests { [(SecurityLevel::CRITICAL, vec![KeyType::ECDSA_SECP256K1])].into(), )] .into(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), }, identity_contract_nonce_gaps: None, signer: None, @@ -81,11 +80,11 @@ mod tests { rotate_quorums: false, failure_testing: None, query_testing: None, - // because we can add an identity and withdraw from it in the same block - // the result would be different from what would be expected - verify_state_transition_results: false, + verify_state_transition_results: true, ..Default::default() }; + + let hour_in_ms = 1000 * 60 * 60; let config = PlatformConfig { validator_set: ValidatorSetConfig::default_100_67(), chain_lock: ChainLockConfig::default_100_67(), @@ -95,7 +94,7 @@ mod tests { ..Default::default() }, - block_spacing_ms: 3000, + block_spacing_ms: hour_in_ms, initial_protocol_version: TEST_PLATFORM_V3.protocol_version, testing_configs: PlatformTestConfig::default_minimal_verifications(), ..Default::default() @@ -155,7 +154,135 @@ mod tests { // Run first two blocks: // - Block 1: creates identity - // - Block 2: tops up identity and initiates withdrawals + // - Block 2: tops up identity + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + signer, + .. + } = { + let outcome = run_chain_for_strategy( + &mut platform, + 2, + start_strategy, + config.clone(), + 1, + &mut None, + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal transactions are not populated to block execution context yet + assert_eq!(outcome.withdrawals.len(), 0); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + assert!(withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, 0); + + let pooled_withdrawals = withdrawal_documents_pooled.len(); + + assert_eq!(pooled_withdrawals, 0); + + outcome + }; + + let continue_strategy_only_withdrawal = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityWithdrawal( + dash_to_credits!(0.1)..=dash_to_credits!(0.1), + ), + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer.clone()), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let continue_strategy_no_operations = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + // Run Block 3: initiates a withdrawal let ( ChainExecutionOutcome { abci_app, @@ -167,20 +294,36 @@ mod tests { identity_nonce_counter, identity_contract_nonce_counter, instant_lock_quorums, + identities, .. }, last_block_pooled_withdrawals_amount, ) = { - let outcome = run_chain_for_strategy( - &mut platform, - 2, - strategy.clone(), + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 3, + core_height_start: 1, + block_count: 1, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_only_withdrawal.clone(), config.clone(), - 1, - &mut None, + StrategyRandomness::SeedEntropy(2), ); for tx_results_per_block in outcome.state_transition_results_per_block.values() { + assert_eq!(tx_results_per_block.len(), 1); for (state_transition, result) in tx_results_per_block { assert_eq!( result.code, 0, @@ -206,12 +349,29 @@ mod tests { ) .unwrap(); assert!(!withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, dash_to_credits!(0.1) as i64); + let pooled_withdrawals = withdrawal_documents_pooled.len(); (outcome, pooled_withdrawals) }; - // Run block 3 + // Run block 4 // Should broadcast previously pooled withdrawals to core let ChainExecutionOutcome { abci_app, @@ -224,12 +384,13 @@ mod tests { identity_nonce_counter, identity_contract_nonce_counter, instant_lock_quorums, + identities, .. } = { let outcome = continue_chain_for_strategy( abci_app, ChainExecutionParameters { - block_start: 3, + block_start: 4, core_height_start: 1, block_count: 1, proposers, @@ -242,11 +403,11 @@ mod tests { start_time_ms: GENESIS_TIME_MS, current_time_ms: end_time_ms, instant_lock_quorums, - current_identities: Vec::new(), + current_identities: identities, }, - strategy.clone(), + continue_strategy_no_operations.clone(), config.clone(), - StrategyRandomness::SeedEntropy(2), + StrategyRandomness::SeedEntropy(3), ); // Withdrawal documents with pooled status should exist. @@ -272,6 +433,22 @@ mod tests { last_block_pooled_withdrawals_amount ); + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, dash_to_credits!(0.1) as i64); + outcome }; @@ -292,29 +469,26 @@ mod tests { }); } - // Run block 4 - // Should change pooled status to broadcasted - let last_block_broadcasted_withdrawals_amount = last_block_withdrawals.len(); - let ( - ChainExecutionOutcome { - abci_app, - proposers, - validator_quorums: quorums, - current_validator_quorum_hash: current_quorum_hash, - current_proposer_versions, - end_time_ms, - withdrawals: last_block_withdrawals, - identity_nonce_counter, - identity_contract_nonce_counter, - instant_lock_quorums, - .. - }, - last_block_broadcased_withdrawals_amount, - ) = { + // Run block 5 + // Should change do nothing, because core doesn't report any change + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + withdrawals: last_block_withdrawals, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { let outcome = continue_chain_for_strategy( abci_app, ChainExecutionParameters { - block_start: 4, + block_start: 5, core_height_start: 1, block_count: 1, proposers, @@ -327,49 +501,45 @@ mod tests { start_time_ms: GENESIS_TIME_MS, current_time_ms: end_time_ms + 1000, instant_lock_quorums, - current_identities: Vec::new(), + current_identities: identities, }, - strategy.clone(), + continue_strategy_no_operations.clone(), config.clone(), - StrategyRandomness::SeedEntropy(3), + StrategyRandomness::SeedEntropy(4), ); - let withdrawal_documents_pooled = outcome + let withdrawal_documents_completed = outcome .abci_app .platform .drive .fetch_oldest_withdrawal_documents_by_status( - withdrawals_contract::WithdrawalStatus::POOLED.into(), + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), DEFAULT_QUERY_LIMIT, None, platform_version, ) .unwrap(); - let withdrawal_documents_broadcasted = outcome + // things have not changed + assert!(withdrawal_documents_completed.is_empty()); + + let locked_amount = outcome .abci_app .platform .drive - .fetch_oldest_withdrawal_documents_by_status( - withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), - DEFAULT_QUERY_LIMIT, + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, None, - platform_version, + &mut vec![], + &platform_version.drive, ) - .unwrap(); - - // In this block we should have new withdrawals pooled - assert!(!withdrawal_documents_pooled.is_empty()); + .expect("expected to get locked amount"); - // And extra withdrawals broadcasted - let withdrawals_broadcasted_expected = - last_block_broadcasted_withdrawals_amount + outcome.withdrawals.len(); - assert_eq!( - withdrawal_documents_broadcasted.len(), - withdrawals_broadcasted_expected - ); + assert_eq!(locked_amount, dash_to_credits!(0.1) as i64); - (outcome, withdrawal_documents_broadcasted.len()) + outcome }; // Update core state for newly broadcasted transactions @@ -408,29 +578,26 @@ mod tests { drop(core_state); } - // Run block 5 + // Run block 6 // Previously broadcasted transactions should be settled after block 5, // and their corresponding statuses should be changed to COMPLETED - let ( - ChainExecutionOutcome { - abci_app, - proposers, - validator_quorums: quorums, - current_validator_quorum_hash: current_quorum_hash, - current_proposer_versions, - end_time_ms, - withdrawals: last_block_withdrawals, - identity_nonce_counter, - identity_contract_nonce_counter, - instant_lock_quorums, - .. - }, - last_block_withdrawals_completed_amount, - ) = { + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { let outcome = continue_chain_for_strategy( abci_app, ChainExecutionParameters { - block_start: 5, + block_start: 6, core_height_start: 1, block_count: 1, proposers, @@ -443,11 +610,11 @@ mod tests { start_time_ms: GENESIS_TIME_MS, current_time_ms: end_time_ms + 1000, instant_lock_quorums, - current_identities: Vec::new(), + current_identities: identities, }, - strategy.clone(), + continue_strategy_no_operations.clone(), config.clone(), - StrategyRandomness::SeedEntropy(4), + StrategyRandomness::SeedEntropy(5), ); let withdrawal_documents_pooled = outcome @@ -487,62 +654,1592 @@ mod tests { .unwrap(); // In this block we should have new withdrawals pooled - assert!(!withdrawal_documents_pooled.is_empty()); + assert!(withdrawal_documents_pooled.is_empty()); + assert!(withdrawal_documents_broadcasted.is_empty()); - // And some withdrawals completed - let withdrawals_completed_expected = - // Withdrawals issued on {previous_block - 1} considered completed - last_block_broadcased_withdrawals_amount - last_block_withdrawals.len(); - assert_eq!( - withdrawal_documents_completed.len(), - withdrawals_completed_expected - ); + assert_eq!(withdrawal_documents_completed.len(), 1); - // And extra withdrawals broadcasted - let withdrawals_broadcasted_expected = - // Withdrawals issued on previous block + withdrawals from this block are still in broadcasted state - last_block_withdrawals.len() + outcome.withdrawals.len(); + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); - assert_eq!( - withdrawal_documents_broadcasted.len(), - withdrawals_broadcasted_expected - ); + assert_eq!(locked_amount, dash_to_credits!(0.1) as i64); - (outcome, withdrawal_documents_completed.len()) + outcome }; - // Update state of the core before proceeding to the next block - { - // Simulate transactions being added to the core mempool - let mut core_state = shared_core_state.lock().unwrap(); + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 7, + core_height_start: 1, + block_count: 24, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms + 1000, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(6), + ); - let number_of_blocks_before_expiration: u32 = 48; - chain_locked_height += number_of_blocks_before_expiration; + // We should have unlocked the amounts by now + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); - core_state.chain_lock.block_height = chain_locked_height; + assert_eq!(locked_amount, 0); + } - last_block_withdrawals.iter().for_each(|tx| { + #[test] + fn run_chain_withdrawal_expired() { + // TEST_PLATFORM_V3 is like v4, but without the single quorum can sign withdrawals restriction + let platform_version = PlatformVersion::get(TEST_PLATFORM_V3.protocol_version) + .expect("expected to get platform version"); + let start_strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityTopUp(dash_to_duffs!(10)..=dash_to_duffs!(10)), + frequency: Frequency { + times_per_block_range: 1..4, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo { + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + start_keys: 3, + extra_keys: [( + Purpose::TRANSFER, + [(SecurityLevel::CRITICAL, vec![KeyType::ECDSA_SECP256K1])].into(), + )] + .into(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), + }, + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let hour_in_ms = 1000 * 60 * 60; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: hour_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .build_with_mock_rpc(); + + platform + .core_rpc + .expect_send_raw_transaction() + .returning(move |_| Ok(Txid::all_zeros())); + + let mut chain_locked_height = 1; + + // Have to go with a complicated shared object for the core state because we need to change + // rpc response along the way but we can't mutate `platform.core_rpc` later + // because platform reference is moved into the AbciApplication. + let shared_core_state = Arc::new(Mutex::new(CoreState { + asset_unlock_statuses: BTreeMap::new(), + chain_lock: ChainLock { + block_height: chain_locked_height, + block_hash: BlockHash::from_byte_array([1; 32]), + signature: BLSSignature::from([2; 96]), + }, + })); + + // Set up Core RPC responses + { + let core_state = shared_core_state.clone(); + + platform + .core_rpc + .expect_get_asset_unlock_statuses() + .returning(move |indices, _| { + Ok(indices + .iter() + .map(|index| { + core_state + .lock() + .unwrap() + .asset_unlock_statuses + .get(index) + .cloned() + .unwrap() + }) + .collect()) + }); + + let core_state = shared_core_state.clone(); + platform + .core_rpc + .expect_get_best_chain_lock() + .returning(move || Ok(core_state.lock().unwrap().chain_lock.clone())); + } + + // Run first two blocks: + // - Block 1: creates identity + // - Block 2: tops up identity + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + signer, + .. + } = { + let outcome = run_chain_for_strategy( + &mut platform, + 2, + start_strategy, + config.clone(), + 1, + &mut None, + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal transactions are not populated to block execution context yet + assert_eq!(outcome.withdrawals.len(), 0); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + assert!(withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, 0); + + let pooled_withdrawals = withdrawal_documents_pooled.len(); + + assert_eq!(pooled_withdrawals, 0); + + outcome + }; + + let continue_strategy_only_withdrawal = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityWithdrawal( + dash_to_credits!(0.1)..=dash_to_credits!(0.1), + ), + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer.clone()), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let continue_strategy_no_operations = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + // Run Block 3: initiates a withdrawal + let ( + ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + }, + last_block_pooled_withdrawals_amount, + ) = { + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 3, + core_height_start: 1, + block_count: 1, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_only_withdrawal.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(2), + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + assert_eq!(tx_results_per_block.len(), 1); + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal transactions are not populated to block execution context yet + assert_eq!(outcome.withdrawals.len(), 0); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + assert!(!withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, dash_to_credits!(0.1) as i64); + + let pooled_withdrawals = withdrawal_documents_pooled.len(); + + (outcome, pooled_withdrawals) + }; + + // Run block 4 + // Should broadcast previously pooled withdrawals to core + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + withdrawals: last_block_withdrawals, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 4, + core_height_start: 1, + block_count: 1, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(2), + ); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // In this block all previously pooled withdrawals should be broadcasted + assert_eq!( + outcome.withdrawals.len(), + last_block_pooled_withdrawals_amount + ); + assert_eq!( + withdrawal_documents_broadcasted.len(), + last_block_pooled_withdrawals_amount + ); + + outcome + }; + + // Update state of the core before proceeding to the next block + { + // Simulate transactions being added to the core mempool + let mut core_state = shared_core_state.lock().unwrap(); + + let number_of_blocks_before_expiration: u32 = 48; + chain_locked_height += number_of_blocks_before_expiration; + + core_state.chain_lock.block_height = chain_locked_height; + + last_block_withdrawals.iter().for_each(|tx| { let index = asset_unlock_index(tx); - core_state.asset_unlock_statuses.insert( - index, - AssetUnlockStatusResult { - index, - status: AssetUnlockStatus::Unknown, - }, - ); - }); + core_state.asset_unlock_statuses.insert( + index, + AssetUnlockStatusResult { + index, + status: AssetUnlockStatus::Unknown, + }, + ); + }); + } + + // Run block 5 + // Tests withdrawal expiration + let ChainExecutionOutcome { .. } = { + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 5, + core_height_start: 2, + block_count: 1, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms + 1000, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(5), + ); + + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + let withdrawal_documents_completed = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + let withdrawal_documents_expired = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::EXPIRED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + assert!(withdrawal_documents_pooled.is_empty()); + assert!(withdrawal_documents_completed.is_empty()); + + assert_eq!(withdrawal_documents_expired.len(), 1); + + assert!(withdrawal_documents_broadcasted.is_empty()); + + outcome + }; + } + + #[test] + fn run_chain_withdraw_from_identities_too_many_withdrawals_within_a_day_hitting_limit() { + // TEST_PLATFORM_V3 is like v4, but without the single quorum can sign withdrawals restriction + let platform_version = PlatformVersion::get(TEST_PLATFORM_V3.protocol_version) + .expect("expected to get platform version"); + let start_strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityTopUp(dash_to_duffs!(10)..=dash_to_duffs!(10)), + frequency: Frequency { + times_per_block_range: 10..11, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo { + frequency: Frequency { + times_per_block_range: 10..11, + chance_per_block: None, + }, + start_keys: 3, + extra_keys: [( + Purpose::TRANSFER, + [(SecurityLevel::CRITICAL, vec![KeyType::ECDSA_SECP256K1])].into(), + )] + .into(), + start_balance_range: dash_to_duffs!(200)..=dash_to_duffs!(200), + }, + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let minute_in_ms = 1000 * 60; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: minute_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .build_with_mock_rpc(); + + platform + .core_rpc + .expect_send_raw_transaction() + .returning(move |_| Ok(Txid::all_zeros())); + + let chain_locked_height = 1; + + // Have to go with a complicated shared object for the core state because we need to change + // rpc response along the way but we can't mutate `platform.core_rpc` later + // because platform reference is moved into the AbciApplication. + let shared_core_state = Arc::new(Mutex::new(CoreState { + asset_unlock_statuses: BTreeMap::new(), + chain_lock: ChainLock { + block_height: chain_locked_height, + block_hash: BlockHash::from_byte_array([1; 32]), + signature: BLSSignature::from([2; 96]), + }, + })); + + // Set up Core RPC responses + { + let core_state = shared_core_state.clone(); + + platform + .core_rpc + .expect_get_asset_unlock_statuses() + .returning(move |indices, _| { + Ok(indices + .iter() + .map(|index| { + core_state + .lock() + .unwrap() + .asset_unlock_statuses + .get(index) + .cloned() + .unwrap() + }) + .collect()) + }); + + let core_state = shared_core_state.clone(); + platform + .core_rpc + .expect_get_best_chain_lock() + .returning(move || Ok(core_state.lock().unwrap().chain_lock.clone())); + } + + // Run first two blocks: + // - Block 1: creates identity + // - Block 2: tops up identity + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + signer, + .. + } = { + let outcome = run_chain_for_strategy( + &mut platform, + 2, + start_strategy, + config.clone(), + 1, + &mut None, + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal transactions are not populated to block execution context yet + assert_eq!(outcome.withdrawals.len(), 0); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + assert!(withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, 0); + + let pooled_withdrawals = withdrawal_documents_pooled.len(); + + assert_eq!(pooled_withdrawals, 0); + + let total_credits_balance = outcome + .abci_app + .platform + .drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to get total credits balance"); + + assert_eq!( + total_credits_balance.total_identity_balances, + 409997575280380 + ); // Around 4100 Dash. + + assert_eq!( + total_credits_balance.total_in_trees().unwrap(), + 410000000000000 + ); // Around 4100 Dash. + + let total_credits_in_platform = outcome + .abci_app + .platform + .drive + .grove_get_raw_value_u64_from_encoded_var_vec( + (&misc_path()).into(), + TOTAL_SYSTEM_CREDITS_STORAGE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get total credits in platform"); + + assert_eq!(total_credits_in_platform, Some(410000000000000)); + + outcome + }; + + let continue_strategy_only_withdrawal = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityWithdrawal( + dash_to_credits!(25)..=dash_to_credits!(25), + ), + frequency: Frequency { + times_per_block_range: 4..5, // 25 Dash x 4 Withdrawals = 100 Dash + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer.clone()), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let continue_strategy_no_operations = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + // Run Block 3 onwards: initiates withdrawals + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 3, + core_height_start: 1, + block_count: 20, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_only_withdrawal.clone(), + config.clone(), + StrategyRandomness::SeedEntropy(2), + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + assert_eq!(tx_results_per_block.len(), 4); + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal documents with pooled status should not exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None are currently pooled since we have no more room + assert!(withdrawal_documents_pooled.is_empty()); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_queued = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::QUEUED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have 66 out of 100 queued + assert_eq!(withdrawal_documents_queued.len(), 66); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_completed = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None have completed because core didn't acknowledge them + assert_eq!(withdrawal_documents_completed.len(), 0); + + // Withdrawal documents with EXPIRED status should not exist yet. + let withdrawal_documents_expired = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::EXPIRED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have none expired yet + assert_eq!(withdrawal_documents_expired.len(), 0); + + // Withdrawal documents with broadcasted status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have 14 broadcasted (66 queued + 14 broadcasted = 80 total) + // 14 broadcasted = 14 * 25 Dash = 350 Dash + // This is explained by + // withdrawal block 1 : 4 withdrawals taking us to 4000 Dash and 100 Dash Taken + // limit is 100, 4 broadcasted + // withdrawal block 2 : 4 withdrawals taking us to 3900 Dash and 100 Dash Taken + // limit is 390 - 100 = 290, 4 broadcasted + // withdrawal block 3 : 4 withdrawals taking us to 3800 Dash and 100 Dash Taken + // limit is 380 - 200 = 180, 4 broadcasted + // withdrawal block 4 : 4 withdrawals taking us to 3700 Dash and 100 Dash Taken + // limit is 370 - 300 = 70, 2 broadcasted + assert_eq!(withdrawal_documents_broadcasted.len(), 14); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, dash_to_credits!(350) as i64); + + outcome + }; + + let hour_in_ms = 1000 * 60 * 60; + + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 23, + core_height_start: 1, + block_count: 48, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms + 1000, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: hour_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }, + StrategyRandomness::SeedEntropy(9), + ); + + // We should have unlocked the amounts by now + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + // There's about 2000 Dash left in platform, it's normal we are locking 1/10th of it + assert_eq!(locked_amount, 20000000000000); + + // Withdrawal documents with pooled status should not exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None are currently pooled since we have no more room + assert!(withdrawal_documents_pooled.is_empty()); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_queued = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::QUEUED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have 58 out of 100 queued + assert_eq!(withdrawal_documents_queued.len(), 58); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_completed = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None have completed because core didn't acknowledge them + assert_eq!(withdrawal_documents_completed.len(), 0); + + // Withdrawal documents with EXPIRED status should not exist yet. + let withdrawal_documents_expired = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::EXPIRED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have none expired yet, because the core height never went up + assert_eq!(withdrawal_documents_expired.len(), 0); + + // Withdrawal documents with broadcasted status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + assert_eq!(withdrawal_documents_broadcasted.len(), 22); + outcome + }; + + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 71, + core_height_start: 1, + block_count: 250, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms + 1000, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: hour_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }, + StrategyRandomness::SeedEntropy(9), + ); + + // We should have unlocked the amounts by now + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + // We have nothing locked left + assert_eq!(locked_amount, 0); + + // Withdrawal documents with pooled status should not exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None are currently pooled since we have no more room + assert!(withdrawal_documents_pooled.is_empty()); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_queued = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::QUEUED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // Nothing is left in the queue + assert_eq!(withdrawal_documents_queued.len(), 0); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_completed = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None have completed because core didn't acknowledge them + assert_eq!(withdrawal_documents_completed.len(), 0); + + // Withdrawal documents with EXPIRED status should not exist yet. + let withdrawal_documents_expired = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::EXPIRED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have none expired yet, because the core height never went up + assert_eq!(withdrawal_documents_expired.len(), 0); + + // Withdrawal documents with broadcasted status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + assert_eq!(withdrawal_documents_broadcasted.len(), 80); + } + + #[test] + fn run_chain_withdraw_from_identities_many_small_withdrawals() { + // TEST_PLATFORM_V3 is like v4, but without the single quorum can sign withdrawals restriction + let platform_version = PlatformVersion::get(TEST_PLATFORM_V3.protocol_version) + .expect("expected to get platform version"); + let start_strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityTopUp(dash_to_duffs!(10)..=dash_to_duffs!(10)), + frequency: Frequency { + times_per_block_range: 10..11, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo { + frequency: Frequency { + times_per_block_range: 10..11, + chance_per_block: None, + }, + start_keys: 3, + extra_keys: [( + Purpose::TRANSFER, + [(SecurityLevel::CRITICAL, vec![KeyType::ECDSA_SECP256K1])].into(), + )] + .into(), + start_balance_range: dash_to_duffs!(200)..=dash_to_duffs!(200), + }, + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let minute_in_ms = 1000 * 60; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: minute_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .build_with_mock_rpc(); + + platform + .core_rpc + .expect_send_raw_transaction() + .returning(move |_| Ok(Txid::all_zeros())); + + let chain_locked_height = 1; + + // Have to go with a complicated shared object for the core state because we need to change + // rpc response along the way but we can't mutate `platform.core_rpc` later + // because platform reference is moved into the AbciApplication. + let shared_core_state = Arc::new(Mutex::new(CoreState { + asset_unlock_statuses: BTreeMap::new(), + chain_lock: ChainLock { + block_height: chain_locked_height, + block_hash: BlockHash::from_byte_array([1; 32]), + signature: BLSSignature::from([2; 96]), + }, + })); + + // Set up Core RPC responses + { + let core_state = shared_core_state.clone(); + + platform + .core_rpc + .expect_get_asset_unlock_statuses() + .returning(move |indices, _| { + Ok(indices + .iter() + .map(|index| { + core_state + .lock() + .unwrap() + .asset_unlock_statuses + .get(index) + .cloned() + .unwrap() + }) + .collect()) + }); + + let core_state = shared_core_state.clone(); + platform + .core_rpc + .expect_get_best_chain_lock() + .returning(move || Ok(core_state.lock().unwrap().chain_lock.clone())); } - // Run block 6. - // Tests withdrawal expiration - let ChainExecutionOutcome { .. } = { + // Run first two blocks: + // - Block 1: creates identity + // - Block 2: tops up identity + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + signer, + .. + } = { + let outcome = run_chain_for_strategy( + &mut platform, + 2, + start_strategy, + config.clone(), + 1, + &mut None, + ); + + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal transactions are not populated to block execution context yet + assert_eq!(outcome.withdrawals.len(), 0); + + // Withdrawal documents with pooled status should exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + assert!(withdrawal_documents_pooled.is_empty()); + + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, 0); + + let pooled_withdrawals = withdrawal_documents_pooled.len(); + + assert_eq!(pooled_withdrawals, 0); + + let total_credits_balance = outcome + .abci_app + .platform + .drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to get total credits balance"); + + assert_eq!( + total_credits_balance.total_identity_balances, + 409997575280380 + ); // Around 4100 Dash. + + assert_eq!( + total_credits_balance.total_in_trees().unwrap(), + 410000000000000 + ); // Around 4100 Dash. + + let total_credits_in_platform = outcome + .abci_app + .platform + .drive + .grove_get_raw_value_u64_from_encoded_var_vec( + (&misc_path()).into(), + TOTAL_SYSTEM_CREDITS_STORAGE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get total credits in platform"); + + assert_eq!(total_credits_in_platform, Some(410000000000000)); + + outcome + }; + + let continue_strategy_only_withdrawal = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![Operation { + op_type: OperationType::IdentityWithdrawal( + dash_to_credits!(0.0025)..=dash_to_credits!(0.0025), + ), + frequency: Frequency { + times_per_block_range: 40..41, // 0.0025 Dash x 40 Withdrawals = 0.1 Dash + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer.clone()), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + let continue_strategy_no_operations = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![], + operations: vec![], + start_identities: StartIdentities::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + verify_state_transition_results: true, + ..Default::default() + }; + + // Run Block 3-23 onwards: initiates withdrawals + let ChainExecutionOutcome { + abci_app, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions, + end_time_ms, + identity_nonce_counter, + identity_contract_nonce_counter, + instant_lock_quorums, + identities, + .. + } = { let outcome = continue_chain_for_strategy( abci_app, ChainExecutionParameters { - block_start: 6, + block_start: 3, core_height_start: 1, - block_count: 1, + block_count: 20, proposers, validator_quorums: quorums, current_validator_quorum_hash: current_quorum_hash, @@ -551,15 +2248,27 @@ mod tests { current_identity_contract_nonce_counter: identity_contract_nonce_counter, current_votes: BTreeMap::default(), start_time_ms: GENESIS_TIME_MS, - current_time_ms: end_time_ms + 1000, + current_time_ms: end_time_ms, instant_lock_quorums, - current_identities: Vec::new(), + current_identities: identities, }, - strategy.clone(), + continue_strategy_only_withdrawal.clone(), config.clone(), - StrategyRandomness::SeedEntropy(5), + StrategyRandomness::SeedEntropy(2), ); + for tx_results_per_block in outcome.state_transition_results_per_block.values() { + assert_eq!(tx_results_per_block.len(), 20); + for (state_transition, result) in tx_results_per_block { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // Withdrawal documents with pooled status should not exist. let withdrawal_documents_pooled = outcome .abci_app .platform @@ -572,18 +2281,26 @@ mod tests { ) .unwrap(); - let withdrawal_documents_broadcasted = outcome + // We should have 4 pooled, which is the limit per block + assert_eq!(withdrawal_documents_pooled.len(), 4); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_queued = outcome .abci_app .platform .drive .fetch_oldest_withdrawal_documents_by_status( - withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), - DEFAULT_QUERY_LIMIT, + withdrawals_contract::WithdrawalStatus::QUEUED.into(), + 10000, None, platform_version, ) .unwrap(); + // We have 320 queued + assert_eq!(withdrawal_documents_queued.len(), 320); + + // Withdrawal documents with queued status should exist. let withdrawal_documents_completed = outcome .abci_app .platform @@ -596,6 +2313,10 @@ mod tests { ) .unwrap(); + // None have completed because core didn't acknowledge them + assert_eq!(withdrawal_documents_completed.len(), 0); + + // Withdrawal documents with EXPIRED status should not exist yet. let withdrawal_documents_expired = outcome .abci_app .platform @@ -608,36 +2329,177 @@ mod tests { ) .unwrap(); - // In this block we should have new withdrawals pooled - assert!(!withdrawal_documents_pooled.is_empty()); - - // Amount of completed withdrawals stays the same as in the last block - assert_eq!( - withdrawal_documents_completed.len(), - last_block_withdrawals_completed_amount - ); + // We have none expired yet + assert_eq!(withdrawal_documents_expired.len(), 0); - // And some withdrawals got expired - let withdrawals_expired_expected = - // Withdrawals issued on {previous_block - 1}, but not chainlocked yet, considered expired - last_block_broadcased_withdrawals_amount - last_block_withdrawals.len(); + // Withdrawal documents with broadcasted status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); - assert_eq!( - withdrawal_documents_expired.len(), - withdrawals_expired_expected - ); + // We have 76 broadcasted (4 pooled + 76 broadcasted = 80 total for 4 per block in 20 blocks) + assert_eq!(withdrawal_documents_broadcasted.len(), 76); - // And extra withdrawals broadcasted - let withdrawals_broadcasted_expected = - // Withdrawals issued on previous block + withdrawals from this block are still in broadcasted state - last_block_withdrawals.len() + outcome.withdrawals.len(); + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); - assert_eq!( - withdrawal_documents_broadcasted.len(), - withdrawals_broadcasted_expected - ); + // 0.2 is 0.0025 amount * 80 withdrawals + assert_eq!(locked_amount, dash_to_credits!(0.2) as i64); outcome }; + + let hour_in_ms = 1000 * 60 * 60; + + let outcome = continue_chain_for_strategy( + abci_app, + ChainExecutionParameters { + block_start: 23, + core_height_start: 1, + block_count: 88, + proposers, + validator_quorums: quorums, + current_validator_quorum_hash: current_quorum_hash, + current_proposer_versions: Some(current_proposer_versions), + current_identity_nonce_counter: identity_nonce_counter, + current_identity_contract_nonce_counter: identity_contract_nonce_counter, + current_votes: BTreeMap::default(), + start_time_ms: GENESIS_TIME_MS, + current_time_ms: end_time_ms + 1000, + instant_lock_quorums, + current_identities: identities, + }, + continue_strategy_no_operations.clone(), + PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + + ..Default::default() + }, + block_spacing_ms: hour_in_ms, + initial_protocol_version: TEST_PLATFORM_V3.protocol_version, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }, + StrategyRandomness::SeedEntropy(9), + ); + + // We should have unlocked the amounts by now + let locked_amount = outcome + .abci_app + .platform + .drive + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to get locked amount"); + + assert_eq!(locked_amount, 18000000000); + + // Withdrawal documents with pooled status should not exist. + let withdrawal_documents_pooled = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::POOLED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None are currently pooled since we finished the queue + assert!(withdrawal_documents_pooled.is_empty()); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_queued = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::QUEUED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // Queue has been finished + assert_eq!(withdrawal_documents_queued.len(), 0); + + // Withdrawal documents with queued status should exist. + let withdrawal_documents_completed = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::COMPLETE.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // None have completed because core didn't acknowledge them + assert_eq!(withdrawal_documents_completed.len(), 0); + + // Withdrawal documents with EXPIRED status should not exist yet. + let withdrawal_documents_expired = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::EXPIRED.into(), + DEFAULT_QUERY_LIMIT, + None, + platform_version, + ) + .unwrap(); + + // We have none expired yet, because the core height never went up + assert_eq!(withdrawal_documents_expired.len(), 0); + + // Withdrawal documents with broadcasted status should exist. + let withdrawal_documents_broadcasted = outcome + .abci_app + .platform + .drive + .fetch_oldest_withdrawal_documents_by_status( + withdrawals_contract::WithdrawalStatus::BROADCASTED.into(), + 1000, + None, + platform_version, + ) + .unwrap(); + + assert_eq!(withdrawal_documents_broadcasted.len(), 400); } } diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 15c207cc230..7c37d1ad6c2 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -77,6 +77,7 @@ dpp = { path = "../rs-dpp", features = [ ], default-features = false } once_cell = "1.7" serde_json = { version = "1.0", features = ["preserve_order"] } +assert_matches = "1.5.0" [[bench]] name = "benchmarks" diff --git a/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/mod.rs new file mode 100644 index 00000000000..172eda82c97 --- /dev/null +++ b/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/mod.rs @@ -0,0 +1,53 @@ +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use dpp::fee::Credits; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; + +mod v0; + +impl Drive { + /// Calculates the current withdrawal limit based on the total credits available in the platform + /// and the amount already withdrawn in the last 24 hours, using the appropriate version-specific logic. + /// + /// This function selects the version-specific implementation based on the provided `platform_version`. + /// It currently supports only version 0 (`calculate_current_withdrawal_limit_v0`). + /// + /// # Parameters + /// + /// * `transaction`: The transaction context used for querying data. + /// * `platform_version`: The version of the platform being used, which contains configuration details and version-specific methods. + /// + /// # Returns + /// + /// * `Ok(Credits)`: The calculated current withdrawal limit, representing the maximum amount that can still be withdrawn in the current 24-hour window. + /// * `Err(Error)`: Returns an error if the version specified in `platform_version` is not supported or if there is an issue in the version-specific calculation. + /// + /// # Errors + /// + /// * `Error::Drive(DriveError::UnknownVersionMismatch)`: + /// - If the platform version provided does not match any known versions supported by this function. + /// + /// * `Error`: Any error propagated from the version-specific implementation, such as issues in retrieving data or calculating the withdrawal limit. + pub fn calculate_current_withdrawal_limit( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive + .methods + .identity + .withdrawals + .calculate_current_withdrawal_limit + { + 0 => self.calculate_current_withdrawal_limit_v0(transaction, platform_version), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "calculate_current_withdrawal_limit".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/v0/mod.rs new file mode 100644 index 00000000000..f7610ec941c --- /dev/null +++ b/packages/rs-drive/src/drive/identity/withdrawals/calculate_current_withdrawal_limit/v0/mod.rs @@ -0,0 +1,86 @@ +use crate::drive::balances::TOTAL_SYSTEM_CREDITS_STORAGE_KEY; +use crate::drive::identity::withdrawals::paths::{ + get_withdrawal_root_path, WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, +}; +use crate::drive::system::misc_path; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::fee::Credits; +use dpp::withdrawal::daily_withdrawal_limit::daily_withdrawal_limit; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; + +impl Drive { + /// Calculates the current withdrawal limit based on the available credits in the platform + /// and the amount already withdrawn in the last 24 hours. + /// + /// This function considers two main components to calculate the current withdrawal limit: + /// 1. The total maximum withdrawal allowed in a 24-hour period (`daily_maximum`). + /// 2. The amount already withdrawn in the last 24 hours (`withdrawal_amount_in_last_day`). + /// + /// The formula used is: `daily_maximum - withdrawal_amount_in_last_day`. + /// If the withdrawal amount in the last 24 hours exceeds the daily maximum, the result will be 0. + /// + /// # Parameters + /// + /// * `transaction`: The transaction context to use for querying data. + /// * `platform_version`: The version of the platform being used, containing relevant configuration details. + /// + /// # Returns + /// + /// * `Ok(Credits)`: The calculated current withdrawal limit, which is the maximum amount that can still be withdrawn in the current 24-hour window. + /// * `Err(Error)`: Returns an error if there was an issue retrieving the total credits, the daily maximum, or the already withdrawn amount. + /// + /// # Errors + /// + /// * `Error::Drive(DriveError::CriticalCorruptedState)`: + /// - If the total credits in the platform cannot be found, indicating a critical state corruption. + /// - If the withdrawal amount in the last 24 hours is negative, which should not happen. + pub(super) fn calculate_current_withdrawal_limit_v0( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + // The current withdrawal limit has two components + // 1. The total maximum that we are allowed to do in 24 hours + // 2. The amount that we have already withdrawn in the last 24 hours + let mut drive_operations = vec![]; + + let path_holding_total_credits = misc_path(); + let total_credits_in_platform = self + .grove_get_raw_value_u64_from_encoded_var_vec( + (&path_holding_total_credits).into(), + TOTAL_SYSTEM_CREDITS_STORAGE_KEY, + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + &platform_version.drive, + )? + .ok_or(Error::Drive(DriveError::CriticalCorruptedState( + "Credits not found in Platform", + )))?; + + // Let's get the amount that we are allowed to get in the last 24 hours. + let daily_maximum = daily_withdrawal_limit(total_credits_in_platform, platform_version)?; + + let withdrawal_amount_in_last_day: u64 = self + .grove_get_sum_tree_total_value( + (&get_withdrawal_root_path()).into(), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + &platform_version.drive, + )? + .try_into() + .map_err(|_| { + Error::Drive(DriveError::CriticalCorruptedState( + "Withdrawal amount in last day is negative", + )) + })?; + + Ok(daily_maximum.saturating_sub(withdrawal_amount_in_last_day)) + } +} diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs index d4214a53e70..ca67af28bc7 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs @@ -25,7 +25,7 @@ impl Drive { .identity .withdrawals .document - .find_up_to_100_withdrawal_documents_by_status_and_transaction_indices + .find_withdrawal_documents_by_status_and_transaction_indices { 0 => self.find_withdrawal_documents_by_status_and_transaction_indices_v0( status, @@ -35,8 +35,7 @@ impl Drive { platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { - method: "find_up_to_100_withdrawal_documents_by_status_and_transaction_indices" - .to_string(), + method: "find_withdrawal_documents_by_status_and_transaction_indices".to_string(), known_versions: vec![0], received: version, })), diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs index b04bc85b5b4..e6b4a8e6406 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs @@ -16,6 +16,7 @@ use std::collections::BTreeMap; impl Drive { // TODO(withdrawals): Currently it queries only up to 100 documents. // It works while we don't have pooling + // This should be a pathquery directly instead of a drive query for efficiency pub(super) fn find_withdrawal_documents_by_status_and_transaction_indices_v0( &self, diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/mod.rs index 0f3e6aec298..e047ea5a45f 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/mod.rs @@ -1,4 +1,4 @@ -/// This module dedicated for a versioned fetch_up_to_100_oldest_withdrawal_documents_by_status +/// This module dedicated for a versioned fetch_oldest_withdrawal_documents_by_status pub mod fetch_oldest_withdrawal_documents_by_status; -/// This module dedicated for a versioned find_up_to_100_withdrawal_documents_by_status_and_transaction_indices +/// This module dedicated for a versioned find_withdrawal_documents_by_status_and_transaction_indices pub mod find_withdrawal_documents_by_status_and_transaction_indices; diff --git a/packages/rs-drive/src/drive/identity/withdrawals/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/mod.rs index 1571e0e230a..25296ff37a6 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/mod.rs @@ -1,6 +1,7 @@ /// Functions related to withdrawal documents pub mod document; +mod calculate_current_withdrawal_limit; /// Functions and constants related to GroveDB paths pub mod paths; /// Functions related to withdrawal transactions diff --git a/packages/rs-drive/src/drive/identity/withdrawals/paths.rs b/packages/rs-drive/src/drive/identity/withdrawals/paths.rs index 0dfcb3c4c94..fd1e3d04058 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/paths.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/paths.rs @@ -1,17 +1,22 @@ -use grovedb::Element; - use crate::drive::{Drive, RootTree}; use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; use crate::util::batch::GroveDbOpBatch; +use grovedb::Element; +use platform_version::version::PlatformVersion; /// constant key for transaction counter pub const WITHDRAWAL_TRANSACTIONS_NEXT_INDEX_KEY: [u8; 1] = [0]; /// constant id for subtree containing transactions queue pub const WITHDRAWAL_TRANSACTIONS_QUEUE_KEY: [u8; 1] = [1]; +/// constant id for subtree containing the sum of withdrawals +pub const WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY: [u8; 1] = [2]; impl Drive { /// Add operations for creating initial withdrawal state structure - pub fn add_initial_withdrawal_state_structure_operations(batch: &mut GroveDbOpBatch) { + pub fn add_initial_withdrawal_state_structure_operations( + batch: &mut GroveDbOpBatch, + platform_version: &PlatformVersion, + ) { batch.add_insert( vec![vec![RootTree::WithdrawalTransactions as u8]], WITHDRAWAL_TRANSACTIONS_NEXT_INDEX_KEY.to_vec(), @@ -22,6 +27,13 @@ impl Drive { vec![vec![RootTree::WithdrawalTransactions as u8]], WITHDRAWAL_TRANSACTIONS_QUEUE_KEY.to_vec(), ); + + if platform_version.protocol_version >= 4 { + batch.add_insert_empty_sum_tree( + vec![vec![RootTree::WithdrawalTransactions as u8]], + WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY.to_vec(), + ); + } } } @@ -50,3 +62,19 @@ pub fn get_withdrawal_transactions_queue_path() -> [&'static [u8]; 2] { &WITHDRAWAL_TRANSACTIONS_QUEUE_KEY, ] } + +/// Helper function to get the withdrawal transactions sum tree path as Vec +pub fn get_withdrawal_transactions_sum_tree_path_vec() -> Vec> { + vec![ + vec![RootTree::WithdrawalTransactions as u8], + WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY.to_vec(), + ] +} + +/// Helper function to get the withdrawal transactions sum tree path as [u8] +pub fn get_withdrawal_transactions_sum_tree_path() -> [&'static [u8]; 2] { + [ + Into::<&[u8; 1]>::into(RootTree::WithdrawalTransactions), + &WITHDRAWAL_TRANSACTIONS_SUM_AMOUNT_TREE_KEY, + ] +} diff --git a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/mod.rs index 1b6a52728c1..f23c60bba41 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/mod.rs @@ -2,6 +2,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::util::batch::DriveOperation; +use dpp::fee::Credits; use dpp::withdrawal::WithdrawalTransactionIndexAndBytes; use platform_version::version::PlatformVersion; @@ -12,6 +13,7 @@ impl Drive { pub fn add_enqueue_untied_withdrawal_transaction_operations( &self, withdrawal_transactions: Vec, + total_sum: Credits, drive_operation_types: &mut Vec, platform_version: &PlatformVersion, ) -> Result<(), Error> { @@ -27,6 +29,7 @@ impl Drive { 0 => { self.add_enqueue_untied_withdrawal_transaction_operations_v0( withdrawal_transactions, + total_sum, drive_operation_types, ); diff --git a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/v0/mod.rs index a9d8a604e2d..1df5e12da9d 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/add_enqueue_untied_withdrawal_transaction_operations/v0/mod.rs @@ -1,12 +1,16 @@ use crate::drive::Drive; use crate::util::batch::drive_op_batch::WithdrawalOperationType; use crate::util::batch::DriveOperation; +use dpp::fee::Credits; +use dpp::prelude::TimestampMillis; use dpp::withdrawal::WithdrawalTransactionIndexAndBytes; +const DAY_AND_A_HOUR_IN_MS: TimestampMillis = 90_000_000; //25 hours impl Drive { pub(super) fn add_enqueue_untied_withdrawal_transaction_operations_v0( &self, withdrawal_transactions: Vec, + total_sum: Credits, drive_operation_types: &mut Vec, ) { if !withdrawal_transactions.is_empty() { @@ -15,6 +19,13 @@ impl Drive { withdrawal_transactions, }, )); + drive_operation_types.push(DriveOperation::WithdrawalOperation( + WithdrawalOperationType::ReserveWithdrawalAmount { + amount: total_sum, + // Best to use a constant here and not a versioned item as this most likely will not change + expiration_after: DAY_AND_A_HOUR_IN_MS, + }, + )); } } } diff --git a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/mod.rs index e519f56bd50..8926ea5c0c3 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/transaction/queue/mod.rs @@ -37,6 +37,7 @@ mod tests { drive .add_enqueue_untied_withdrawal_transaction_operations( withdrawals, + 100, &mut drive_operations, platform_version, ) diff --git a/packages/rs-drive/src/drive/initialization/v0/mod.rs b/packages/rs-drive/src/drive/initialization/v0/mod.rs index e1c4ee377d4..39387184e54 100644 --- a/packages/rs-drive/src/drive/initialization/v0/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v0/mod.rs @@ -170,7 +170,7 @@ impl Drive { )?; // In Withdrawals - Drive::add_initial_withdrawal_state_structure_operations(&mut batch); + Drive::add_initial_withdrawal_state_structure_operations(&mut batch, platform_version); // For Versioning via forks Drive::add_initial_fork_update_structure_operations(&mut batch); diff --git a/packages/rs-drive/src/state_transition_action/identity/identity_credit_withdrawal/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/identity/identity_credit_withdrawal/v0/transformer.rs index 8da24e5759a..e2c443d9ab7 100644 --- a/packages/rs-drive/src/state_transition_action/identity/identity_credit_withdrawal/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/identity/identity_credit_withdrawal/v0/transformer.rs @@ -16,7 +16,7 @@ use dpp::document::{Document, DocumentV0}; use dpp::identity::core_script::CoreScript; use dpp::identity::hash::IdentityPublicKeyHashMethodsV0; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dpp::identity::IdentityPublicKey; +use dpp::identity::{IdentityPublicKey, KeyType}; use dpp::platform_value::platform_value; use dpp::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; use dpp::state_transition::state_transitions::identity::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; @@ -131,19 +131,27 @@ impl IdentityCreditWithdrawalTransitionActionV0 { )); } } - if !key.key_type().is_core_address_key_type() { - return Ok(ConsensusValidationResult::new_with_error( - ConsensusError::StateError( - StateError::NoTransferKeyForCoreWithdrawalAvailableError( - NoTransferKeyForCoreWithdrawalAvailableError::new( - identity_credit_withdrawal.identity_id, + match key.key_type() { + KeyType::ECDSA_HASH160 => { + // We should get the withdrawal address + CoreScript::new_p2pkh(key.public_key_hash()?).to_bytes() + } + KeyType::BIP13_SCRIPT_HASH => { + // We should get the withdrawal address + CoreScript::new_p2sh(key.public_key_hash()?).to_bytes() + } + _ => { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::NoTransferKeyForCoreWithdrawalAvailableError( + NoTransferKeyForCoreWithdrawalAvailableError::new( + identity_credit_withdrawal.identity_id, + ), ), ), - ), - )); + )); + } } - // We should get the withdrawal address - CoreScript::new_p2pkh(key.public_key_hash()?).to_bytes() }; let mut entropy = Vec::new(); diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/withdrawals.rs b/packages/rs-drive/src/util/batch/drive_op_batch/withdrawals.rs index 7ad820d6013..a24c5b8efe5 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/withdrawals.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/withdrawals.rs @@ -2,14 +2,17 @@ use std::collections::HashMap; use crate::drive::identity::withdrawals::paths::{ get_withdrawal_root_path_vec, get_withdrawal_transactions_queue_path, - get_withdrawal_transactions_queue_path_vec, WITHDRAWAL_TRANSACTIONS_NEXT_INDEX_KEY, + get_withdrawal_transactions_queue_path_vec, get_withdrawal_transactions_sum_tree_path_vec, + WITHDRAWAL_TRANSACTIONS_NEXT_INDEX_KEY, }; -use crate::util::grove_operations::BatchDeleteApplyType; +use crate::util::grove_operations::{BatchDeleteApplyType, BatchInsertApplyType}; use crate::util::object_size_info::PathKeyElementInfo; use crate::{drive::Drive, error::Error, fees::op::LowLevelDriveOperation}; use dpp::block::block_info::BlockInfo; use super::DriveLowLevelOperationConverter; +use dpp::fee::{Credits, SignedCredits}; +use dpp::prelude::TimestampMillis; use dpp::version::PlatformVersion; use dpp::withdrawal::{WithdrawalTransactionIndex, WithdrawalTransactionIndexAndBytes}; use grovedb::Element; @@ -33,6 +36,13 @@ pub enum WithdrawalOperationType { /// withdrawal transaction tuple with id and bytes index: WithdrawalTransactionIndex, }, + /// Reserve an amount in the system for withdrawals, the reservation will expire at the date given + ReserveWithdrawalAmount { + /// amount to reserve + amount: Credits, + /// expiration date + expiration_after: TimestampMillis, + }, } impl DriveLowLevelOperationConverter for WithdrawalOperationType { @@ -42,7 +52,7 @@ impl DriveLowLevelOperationConverter for WithdrawalOperationType { _estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - _block_info: &BlockInfo, + block_info: &BlockInfo, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -73,7 +83,7 @@ impl DriveLowLevelOperationConverter for WithdrawalOperationType { for (index, bytes) in withdrawal_transactions { drive.batch_insert( - PathKeyElementInfo::PathKeyElement::<'_, 1>(( + PathKeyElementInfo::PathKeyElement::<'_, 0>(( path.clone(), index.to_be_bytes().to_vec(), Element::Item(bytes, None), @@ -85,6 +95,30 @@ impl DriveLowLevelOperationConverter for WithdrawalOperationType { Ok(drive_operations) } + WithdrawalOperationType::ReserveWithdrawalAmount { + amount, + expiration_after, + } => { + let mut drive_operations = vec![]; + + let expiration_date = block_info.time_ms + expiration_after; + + let sum_path = get_withdrawal_transactions_sum_tree_path_vec(); + + drive.batch_insert_sum_item_or_add_to_if_already_exists( + PathKeyElementInfo::PathKeyElement::<'_, 0>(( + sum_path.clone(), + expiration_date.to_be_bytes().to_vec(), + Element::SumItem(amount as SignedCredits, None), + )), + BatchInsertApplyType::StatefulBatchInsert, + transaction, + &mut drive_operations, + &platform_version.drive, + )?; + + Ok(drive_operations) + } WithdrawalOperationType::DeleteWithdrawalTransaction { index } => { let mut drive_operations = vec![]; diff --git a/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/mod.rs new file mode 100644 index 00000000000..24b03094ef4 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/mod.rs @@ -0,0 +1,56 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::BatchDeleteApplyType; + +use dpp::version::drive_versions::DriveVersion; + +use grovedb::{PathQuery, TransactionArg}; + +impl Drive { + /// Pushes multiple "delete element" operations for items in a given path based on a `PathQuery` to `drive_operations`. + /// + /// # Parameters + /// * `path_query`: The path query specifying the items to delete within the path. + /// * `error_if_intermediate_path_tree_not_present`: Tells the function to either error or do nothing if an intermediate tree is not present. + /// * `apply_type`: The apply type for the delete operations. + /// * `transaction`: The transaction argument. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + pub fn batch_delete_items_in_path_query( + &self, + path_query: &PathQuery, + error_if_intermediate_path_tree_not_present: bool, + apply_type: BatchDeleteApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> { + match drive_version + .grove_methods + .batch + .batch_delete_items_in_path_query + { + 0 => self.batch_delete_items_in_path_query_v0( + path_query, + error_if_intermediate_path_tree_not_present, + apply_type, + transaction, + drive_operations, + drive_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_delete_items_in_path_query".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/v0/mod.rs new file mode 100644 index 00000000000..a530776986d --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_delete_items_in_path_query/v0/mod.rs @@ -0,0 +1,588 @@ +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::fees::op::LowLevelDriveOperation::GroveOperation; +use crate::util::grove_operations::{push_drive_operation_result, BatchDeleteApplyType}; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::KeyInfoPath; +use grovedb::operations::delete::DeleteOptions; +use grovedb::query_result_type::QueryResultType; +use grovedb::{GroveDb, PathQuery, TransactionArg}; +use grovedb_storage::rocksdb_storage::RocksDbStorage; +use platform_version::version::drive_versions::DriveVersion; + +impl Drive { + /// Version 0 implementation of the "delete multiple elements" operation based on a `PathQuery`. + /// Deletes items in the specified path that match the given query. + /// + /// # Parameters + /// * `path_query`: The path query specifying the items to delete within the path. + /// * `error_if_intermediate_path_tree_not_present`: Tells the function to either error or do nothing if an intermediate tree is not present. + /// * `apply_type`: The apply type for the delete operations. + /// * `transaction`: The transaction argument. + /// * `drive_operations`: The vector containing low-level drive operations. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::CorruptedCodeExecution)` if the operation is not supported. + pub(super) fn batch_delete_items_in_path_query_v0( + &self, + path_query: &PathQuery, + error_if_intermediate_path_tree_not_present: bool, + apply_type: BatchDeleteApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> { + if path_query.query.limit == None { + Error::Drive(DriveError::NotSupported( + "Limits are required for path_query", + )); + } + let query_result = if path_query + .query + .query + .items + .iter() + .all(|query_item| query_item.is_key()) + { + // Fetch the elements that match the path query + let query_result = self.grove_get_raw_path_query_with_optional( + path_query, + error_if_intermediate_path_tree_not_present, + transaction, + drive_operations, + drive_version, + )?; + + query_result + .into_iter() + .filter_map(|(path, key, maybe_element)| { + maybe_element.map(|element| (path, key, element)) + }) + .collect() + } else { + self.grove_get_raw_path_query( + path_query, + transaction, + QueryResultType::QueryPathKeyElementTrioResultType, + drive_operations, + drive_version, + )? + .0 + .to_path_key_elements() + }; + + // Iterate over each element and add a delete operation for it + for (path, key, _) in query_result { + let current_batch_operations = + LowLevelDriveOperation::grovedb_operations_batch(drive_operations); + let options = DeleteOptions { + allow_deleting_non_empty_trees: false, + deleting_non_empty_trees_returns_error: true, + base_root_storage_is_free: true, + validate_tree_at_path_exists: false, + }; + let delete_operation = match apply_type { + BatchDeleteApplyType::StatelessBatchDelete { + is_sum_tree, + estimated_key_size, + estimated_value_size, + } => GroveDb::average_case_delete_operation_for_delete::( + &KeyInfoPath::from_known_owned_path(path.to_vec()), + &KeyInfo::KnownKey(key.to_vec()), + is_sum_tree, + false, + true, + 0, + (estimated_key_size, estimated_value_size), + &drive_version.grove_version, + ) + .map(|r| r.map(Some)), + BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum, + } => self.grove.delete_operation_for_delete_internal( + (path.as_slice()).into(), + key.as_slice(), + &options, + is_known_to_be_subtree_with_sum, + ¤t_batch_operations.operations, + transaction, + &drive_version.grove_version, + ), + }; + + if let Some(delete_operation) = + push_drive_operation_result(delete_operation, drive_operations)? + { + // Add the delete operation to the batch of drive operations + drive_operations.push(GroveOperation(delete_operation)); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::util::grove_operations::QueryType; + use crate::util::test_helpers::setup::setup_drive; + use crate::{ + error::Error, + grovedb::{Element, PathQuery, Query}, + util::grove_operations::BatchDeleteApplyType, + }; + use assert_matches::assert_matches; + use grovedb::SizedQuery; + use grovedb_path::SubtreePath; + use platform_version::version::PlatformVersion; + + #[test] + fn test_batch_delete_items_in_path_query_success() { + // Set up a test drive instance and transaction + let drive = setup_drive(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Insert some elements that will be matched and deleted + let path = vec![b"root".to_vec()]; + let key = b"key".to_vec(); + let element = Element::new_item(b"value".to_vec()); + + drive + .grove_insert_empty_tree( + SubtreePath::empty(), + b"root", + Some(&transaction), + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected insert root tree"); + + drive + .grove + .insert( + path.as_slice(), + &key, + element.clone(), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected insert"); + + // Create a path query that matches the inserted elements + let mut query = Query::new(); + query.insert_key(key.clone()); + let path_query = PathQuery::new(path.clone(), SizedQuery::new(query, Some(1), None)); + + // Set up the apply type and drive operations vector + let apply_type = BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: None, + }; + let mut drive_operations = Vec::new(); + + // Call the function + drive + .batch_delete_items_in_path_query_v0( + &path_query, + true, + apply_type, + Some(&transaction), + &mut drive_operations, + &platform_version.drive, + ) + .expect("expected to batch delete items"); + + drive + .apply_batch_low_level_drive_operations( + None, + Some(&transaction), + drive_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to apply operations"); + + // Commit the transaction + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Verify the element has been deleted + let get_result = drive.grove_get( + path.as_slice().into(), + &key, + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!( + get_result, + Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) + ); + } + + #[test] + fn test_batch_delete_items_in_path_query_range_query() { + // Set up a test drive instance and transaction + let drive = setup_drive(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Insert the root tree + drive + .grove_insert_empty_tree( + SubtreePath::empty(), + b"root", + Some(&transaction), + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to insert root tree"); + + // Insert three elements with keys 1, 2, and 3 + let path = vec![b"root".to_vec()]; + let key1 = b"1".to_vec(); + let key2 = b"2".to_vec(); + let key3 = b"3".to_vec(); + let element = Element::new_item(b"value".to_vec()); + + drive + .grove + .insert( + path.as_slice(), + &key1, + element.clone(), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected insert for key 1"); + + drive + .grove + .insert( + path.as_slice(), + &key2, + element.clone(), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected insert for key 2"); + + drive + .grove + .insert( + path.as_slice(), + &key3, + element.clone(), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected insert for key 3"); + + // Create a range path query that matches keys less than 3 + let mut query = Query::new(); + query.insert_range_to(..b"3".to_vec()); + let path_query = PathQuery::new(path.clone(), SizedQuery::new(query, Some(100), None)); + + // Set up the apply type and drive operations vector + let apply_type = BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: None, + }; + let mut drive_operations = Vec::new(); + + // Call the function + drive + .batch_delete_items_in_path_query_v0( + &path_query, + true, + apply_type, + Some(&transaction), + &mut drive_operations, + &platform_version.drive, + ) + .expect("expected to batch delete items"); + + // Apply batch operations + drive + .apply_batch_low_level_drive_operations( + None, + Some(&transaction), + drive_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to apply operations"); + + // Commit the transaction + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Verify that keys 1 and 2 have been deleted + let get_result_1 = drive.grove_get( + path.as_slice().into(), + &key1, + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!( + get_result_1, + Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) + ); + + let get_result_2 = drive.grove_get( + path.as_slice().into(), + &key2, + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!( + get_result_2, + Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) + ); + + // Verify that key 3 is still there + let get_result_3 = drive.grove_get( + path.as_slice().into(), + &key3, + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!(get_result_3, Ok(Some(Element::Item(..)))); + } + + #[test] + fn test_batch_delete_items_in_path_query_no_elements() { + // Set up a test drive instance and transaction + let drive = setup_drive(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Create the root tree to allow querying it + drive + .grove_insert_empty_tree( + SubtreePath::empty(), + b"root", + Some(&transaction), + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to insert root tree"); + + // Create a path query that does not match any elements + let path = vec![b"root".to_vec()]; + let mut query = Query::new(); + query.insert_key(b"non_existent_key".to_vec()); + let path_query = PathQuery::new(path.clone(), SizedQuery::new(query, Some(1), None)); + + // Set up the apply type and drive operations vector + let apply_type = BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: None, + }; + let mut drive_operations = Vec::new(); + + // Call the function + drive + .batch_delete_items_in_path_query_v0( + &path_query, + true, + apply_type, + Some(&transaction), + &mut drive_operations, + &platform_version.drive, + ) + .expect("expected batch delete to succeed"); + + // Apply batch operations + drive + .apply_batch_low_level_drive_operations( + None, + Some(&transaction), + drive_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to apply operations"); + + // Commit the transaction + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Verify that no elements were deleted as no matching elements existed + let get_result = drive.grove_get( + path.as_slice().into(), + b"non_existent_key", + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!( + get_result, + Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) + ); + } + + #[test] + fn test_batch_delete_items_in_path_query_intermediate_path_missing() { + // Set up a test drive instance and transaction + let drive = setup_drive(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Create a path query with a missing intermediate path + let path = vec![b"missing_root".to_vec()]; + let mut query = Query::new(); + query.insert_key(b"key".to_vec()); + let path_query = PathQuery::new(path.clone(), SizedQuery::new(query, Some(1), None)); + + // Set up the apply type and drive operations vector + let apply_type = BatchDeleteApplyType::StatefulBatchDelete { + is_known_to_be_subtree_with_sum: None, + }; + let mut drive_operations = Vec::new(); + + // Call the function with error_if_intermediate_path_tree_not_present set to true + let result = drive.batch_delete_items_in_path_query_v0( + &path_query, + true, + apply_type, + Some(&transaction), + &mut drive_operations, + &platform_version.drive, + ); + + // Assert failure due to missing intermediate path + assert_matches!( + result, + Err(Error::GroveDB(grovedb::Error::PathParentLayerNotFound(_))) + ); + } + + #[test] + fn test_batch_delete_items_in_path_query_stateless_delete() { + // Set up a test drive instance and transaction + let drive = setup_drive(None); + let platform_version = PlatformVersion::latest(); + let transaction = drive.grove.start_transaction(); + + // Insert some elements that will be matched and deleted + let path = vec![b"root".to_vec()]; + let key = b"key".to_vec(); + let element = Element::new_item(b"value".to_vec()); + + // Insert the root tree + drive + .grove_insert_empty_tree( + SubtreePath::empty(), + b"root", + Some(&transaction), + None, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to insert root tree"); + + // Insert an item in the tree + drive + .grove + .insert( + path.as_slice(), + &key, + element.clone(), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("expected insert"); + + // Create a path query that matches the inserted elements + let mut query = Query::new(); + query.insert_key(key.clone()); + let path_query = PathQuery::new(path.clone(), SizedQuery::new(query, Some(1), None)); + + // Set up the stateless apply type with estimated sizes + let apply_type = BatchDeleteApplyType::StatelessBatchDelete { + is_sum_tree: false, + estimated_key_size: key.len() as u32, + estimated_value_size: element + .serialized_size(&platform_version.drive.grove_version) + .unwrap() as u32, + }; + let mut drive_operations = Vec::new(); + + // Call the function + drive + .batch_delete_items_in_path_query_v0( + &path_query, + true, + apply_type, + Some(&transaction), + &mut drive_operations, + &platform_version.drive, + ) + .expect("expected to batch delete items"); + + // Apply batch operations + drive + .apply_batch_low_level_drive_operations( + None, + Some(&transaction), + drive_operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected to apply operations"); + + // Commit the transaction + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + + // Verify the element has been deleted + let get_result = drive.grove_get( + path.as_slice().into(), + &key, + QueryType::StatefulQuery, + None, + &mut vec![], + &platform_version.drive, + ); + assert_matches!( + get_result, + Err(Error::GroveDB(grovedb::Error::PathKeyNotFound(_))) + ); + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/mod.rs index d14f1e2773b..c08390ba86f 100644 --- a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/mod.rs @@ -23,7 +23,7 @@ impl Drive { /// * `drive_version`: The drive version to select the correct function version to run. /// /// # Returns - /// * `Ok(bool)` if the operation was successful. Returns true if the path key already exists without references. + /// * `Ok(bool)` if the operation was successful. Returns true if we were able to insert. /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. /// * `Err(DriveError::CorruptedCodeExecution)` if the operation is not supported. pub fn batch_insert_if_not_exists( diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/v0/mod.rs index 8bacd806062..8fc5c16c5b4 100644 --- a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/v0/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists/v0/mod.rs @@ -15,7 +15,7 @@ use grovedb::{GroveDb, TransactionArg}; impl Drive { /// Pushes an "insert element if the path key does not yet exist" operation to `drive_operations`. - /// Returns true if the path key already exists without references. + /// Returns true if we inserted. pub(crate) fn batch_insert_if_not_exists_v0( &self, path_key_element_info: PathKeyElementInfo, diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/mod.rs new file mode 100644 index 00000000000..df437be5b29 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/mod.rs @@ -0,0 +1,69 @@ +mod v0; + +use crate::util::grove_operations::BatchInsertApplyType; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::PathKeyElementInfo; + +use dpp::version::drive_versions::DriveVersion; +use grovedb::{Element, TransactionArg}; + +impl Drive { + /// Pushes an "insert element if the path key does not yet exist" operation to `drive_operations` + /// and returns the existing element if it already exists. + /// + /// This function attempts to insert a new element into the database at a specified path and key. + /// If an element already exists at the given path and key, it returns the existing element without inserting the new one. + /// If the element does not exist, it inserts the new element and returns `Ok(None)`. + /// + /// The function dynamically selects the appropriate implementation version based on the provided `drive_version`. + /// + /// # Parameters + /// + /// * `path_key_element_info`: Information about the path, key, and element to be inserted. + /// - Supports various configurations including direct references, owned elements, fixed-size keys, and estimated sizes. + /// * `apply_type`: Defines whether the operation is stateless or stateful, influencing how the insertion is handled. + /// * `transaction`: The transaction context in which the operation will be executed. + /// * `drive_operations`: A mutable reference to the list of drive operations where this operation will be appended. + /// * `drive_version`: The version of the drive being used, determining which function version to execute. + /// + /// # Returns + /// + /// * `Ok(Some(Element))`: If an element already exists at the specified path and key, it returns the existing element. + /// * `Ok(None)`: If the element was successfully inserted because it did not exist before. + /// * `Err(Error)`: Returns an error if: + /// - The drive version provided is not supported (`Error::Drive(DriveError::UnknownVersionMismatch)`). + /// - The operation is not supported in the current state due to invalid configurations or unsupported features (`Error::Drive(DriveError::NotSupportedPrivate)`). + /// + /// # Errors + /// + /// * `Error::Drive(DriveError::UnknownVersionMismatch)`: If the provided drive version is not supported by this function. + /// * `Error::Drive(DriveError::NotSupportedPrivate)`: If the function encounters unsupported configurations such as unknown element sizes for batch operations. + /// + pub fn batch_insert_if_not_exists_return_existing_element( + &self, + path_key_element_info: PathKeyElementInfo, + apply_type: BatchInsertApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result, Error> { + match drive_version.grove_methods.batch.batch_insert_if_not_exists { + 0 => self.batch_insert_if_not_exists_return_existing_element_v0( + path_key_element_info, + apply_type, + transaction, + drive_operations, + drive_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_if_not_exists_return_existing_element".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/v0/mod.rs new file mode 100644 index 00000000000..55f411caebc --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_if_not_exists_return_existing_element/v0/mod.rs @@ -0,0 +1,165 @@ +use crate::util::grove_operations::BatchInsertApplyType; +use crate::util::object_size_info::PathKeyElementInfo::{ + PathFixedSizeKeyRefElement, PathKeyElement, PathKeyElementSize, PathKeyRefElement, + PathKeyUnknownElementSize, +}; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::fees::op::LowLevelDriveOperation::CalculatedCostOperation; +use crate::util::object_size_info::PathKeyElementInfo; +use dpp::version::drive_versions::DriveVersion; +use grovedb::{Element, GroveDb, TransactionArg}; + +impl Drive { + /// Version 0 implementation of the "insert element if the path key does not yet exist" operation. + /// If the element already exists, it returns the existing element. + /// + /// This function checks whether an element exists at a specified path and key. + /// If the element exists, it returns the existing element. If not, it inserts the new element + /// into the database and returns `None`. This operation supports different types of path, key, and element configurations + /// and can be applied in either stateless or stateful contexts. + /// + /// # Parameters + /// + /// * `path_key_element_info`: Information about the path, key, and element to insert. + /// - Supports multiple configurations: direct references, owned elements, fixed size keys, or estimated sizes. + /// * `apply_type`: The application type of the operation, defining whether the operation is stateless or stateful. + /// * `transaction`: The transaction context for the operation, allowing it to be atomic within a batch. + /// * `drive_operations`: A mutable reference to the list of drive operations to which this operation will be appended. + /// * `drive_version`: The version of the drive being used, ensuring compatibility with the function version. + /// + /// # Returns + /// + /// * `Ok(Some(Element))`: If the element already exists at the specified path and key, returning the existing element. + /// * `Ok(None)`: If the element was successfully inserted because it did not exist before. + /// * `Err(Error)`: Returns an error if: + /// - The insertion operation is not supported in the current state. + /// - The operation encounters any unexpected issues related to invalid configurations or unsupported features. + /// + /// # Errors + /// + /// * `Error::Drive(DriveError::NotSupportedPrivate)`: If the function encounters unsupported configurations, such as document sizes for stateful inserts. + /// * `Error::Drive(DriveError::UnknownVersionMismatch)`: If the drive version is not supported for the operation. + /// + /// # PathKeyElementInfo Variants + /// + /// The function supports various `PathKeyElementInfo` variants: + /// * `PathKeyRefElement`: Reference to the path, key, and element. + /// * `PathKeyElement`: Owned path, key, and element. + /// * `PathFixedSizeKeyRefElement`: Reference to the path with a fixed-size key and element. + /// * `PathKeyElementSize`: Path and key with known element size, used for estimation. + /// * `PathKeyUnknownElementSize`: Unsupported in this version and returns an error. + pub(super) fn batch_insert_if_not_exists_return_existing_element_v0( + &self, + path_key_element_info: PathKeyElementInfo, + apply_type: BatchInsertApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result, Error> { + match path_key_element_info { + PathKeyRefElement((path, key, element)) => { + // Check if the element already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key, + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + if let Some(existing_element) = existing_element { + return Ok(Some(existing_element)); + } + + // Element does not exist, proceed with insertion + drive_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + path, + key.to_vec(), + element, + )); + Ok(None) + } + PathKeyElement((path, key, element)) => { + // Check if the element already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key.as_slice(), + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + if let Some(existing_element) = existing_element { + return Ok(Some(existing_element)); + } + + // Element does not exist, proceed with insertion + drive_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + path, key, element, + )); + Ok(None) + } + PathFixedSizeKeyRefElement((path, key, element)) => { + // Check if the element already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key, + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + if let Some(existing_element) = existing_element { + return Ok(Some(existing_element)); + } + + // Element does not exist, proceed with insertion + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + path_items, + key.to_vec(), + element, + )); + Ok(None) + } + PathKeyElementSize((key_info_path, key_info, element)) => { + match apply_type { + BatchInsertApplyType::StatelessBatchInsert { + in_tree_using_sums, .. + } => { + // Estimate if the element with the given size already exists + drive_operations.push(CalculatedCostOperation( + GroveDb::average_case_for_has_raw( + &key_info_path, + &key_info, + element.serialized_size(&drive_version.grove_version)? as u32, + in_tree_using_sums, + &drive_version.grove_version, + )?, + )); + drive_operations.push( + LowLevelDriveOperation::insert_for_estimated_path_key_element( + key_info_path, + key_info, + element, + ), + ); + Ok(None) + } + BatchInsertApplyType::StatefulBatchInsert => { + Err(Error::Drive(DriveError::NotSupportedPrivate( + "document sizes for stateful insert in batch operations not supported", + ))) + } + } + } + PathKeyUnknownElementSize(_) => Err(Error::Drive(DriveError::NotSupportedPrivate( + "document sizes in batch operations not supported", + ))), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/mod.rs new file mode 100644 index 00000000000..87331adad07 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/mod.rs @@ -0,0 +1,58 @@ +mod v0; + +use crate::util::grove_operations::BatchInsertApplyType; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::object_size_info::PathKeyElementInfo; + +use dpp::version::drive_versions::DriveVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Pushes an "insert sum item or add to it if the item already exists" operation to `drive_operations`. + /// This operation either inserts a new sum item at the given path and key or adds the value to the existing sum item. + /// + /// # Parameters + /// * `path_key_element_info`: Information about the path, key, and element. + /// * `apply_type`: The apply type for the operation. + /// * `transaction`: The transaction argument for the operation. + /// * `drive_operations`: The list of drive operations to append to. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + /// * `Err(DriveError::CorruptedCodeExecution)` (rare) if the operation is not supported. + /// * `Err(DriveError::CorruptedElementType)` (rare) if drive is in a corrupted state and + /// gives back an incorrect element type. + pub fn batch_insert_sum_item_or_add_to_if_already_exists( + &self, + path_key_element_info: PathKeyElementInfo, + apply_type: BatchInsertApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> { + match drive_version + .grove_methods + .batch + .batch_insert_sum_item_or_add_to_if_already_exists + { + 0 => self.batch_insert_sum_item_or_add_to_if_already_exists_v0( + path_key_element_info, + apply_type, + transaction, + drive_operations, + drive_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "batch_insert_sum_item_or_add_to_if_already_exists".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/v0/mod.rs new file mode 100644 index 00000000000..8294a6786b5 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/batch_insert_sum_item_or_add_to_if_already_exists/v0/mod.rs @@ -0,0 +1,220 @@ +use crate::util::grove_operations::BatchInsertApplyType; +use crate::util::object_size_info::PathKeyElementInfo::{ + PathFixedSizeKeyRefElement, PathKeyElement, PathKeyElementSize, PathKeyRefElement, + PathKeyUnknownElementSize, +}; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::fees::op::LowLevelDriveOperation::CalculatedCostOperation; +use crate::util::object_size_info::PathKeyElementInfo; +use dpp::version::drive_versions::DriveVersion; +use dpp::ProtocolError; +use grovedb::{Element, GroveDb, TransactionArg}; + +impl Drive { + /// Version 0 implementation of the "insert sum item or add to it if the item already exists" operation. + /// This operation either inserts a new sum item at the given path and key or adds the value to the existing sum item. + /// + /// # Parameters + /// * `path_key_element_info`: Information about the path, key, and element. + /// * `apply_type`: The apply type for the operation. + /// * `transaction`: The transaction argument for the operation. + /// * `drive_operations`: The list of drive operations to append to. + /// * `drive_version`: The drive version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(())` if the operation was successful. + /// * `Err(DriveError::CorruptedCodeExecution)` if the operation is not supported. + pub(crate) fn batch_insert_sum_item_or_add_to_if_already_exists_v0( + &self, + path_key_element_info: PathKeyElementInfo, + apply_type: BatchInsertApplyType, + transaction: TransactionArg, + drive_operations: &mut Vec, + drive_version: &DriveVersion, + ) -> Result<(), Error> { + match path_key_element_info { + PathKeyRefElement((path, key, element)) => { + if let Element::SumItem(new_value, _) = element { + // Check if the sum item already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key, + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + + if let Some(Element::SumItem(existing_value, _)) = existing_element { + // Add to the existing sum item + let updated_value = existing_value + .checked_add(new_value) + .ok_or(ProtocolError::Overflow("overflow when adding to sum item"))?; + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path, + key.to_vec(), + Element::new_sum_item(updated_value), + ), + ); + } else if existing_element.is_some() { + return Err(Error::Drive(DriveError::CorruptedElementType( + "expected sum item element type", + ))); + } else { + // Insert as a new sum item + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path, + key.to_vec(), + Element::new_sum_item(new_value), + ), + ); + } + } else { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "expected sum item element type", + ))); + } + Ok(()) + } + PathKeyElement((path, key, element)) => { + if let Element::SumItem(new_value, _) = element { + // Check if the sum item already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key.as_slice(), + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + + if let Some(Element::SumItem(existing_value, _)) = existing_element { + // Add to the existing sum item + let updated_value = existing_value + .checked_add(new_value) + .ok_or(ProtocolError::Overflow("overflow when adding to sum item"))?; + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path, + key, + Element::new_sum_item(updated_value), + ), + ); + } else if existing_element.is_some() { + return Err(Error::Drive(DriveError::CorruptedElementType( + "expected sum item element type", + ))); + } else { + // Insert as a new sum item + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path, + key, + Element::new_sum_item(new_value), + ), + ); + } + } else { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "expected sum item element type", + ))); + } + Ok(()) + } + PathFixedSizeKeyRefElement((path, key, element)) => { + if let Element::SumItem(new_value, _) = element { + // Check if the sum item already exists + let existing_element = self.grove_get_raw_optional( + path.as_slice().into(), + key, + apply_type.to_direct_query_type(), + transaction, + drive_operations, + drive_version, + )?; + + if let Some(Element::SumItem(existing_value, _)) = existing_element { + // Add to the existing sum item + let updated_value = existing_value + .checked_add(new_value) + .ok_or(ProtocolError::Overflow("overflow when adding to sum item"))?; + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path_items, + key.to_vec(), + Element::new_sum_item(updated_value), + ), + ); + } else if existing_element.is_some() { + return Err(Error::Drive(DriveError::CorruptedElementType( + "expected sum item element type", + ))); + } else { + // Insert as a new sum item + let path_items: Vec> = path.into_iter().map(Vec::from).collect(); + drive_operations.push( + LowLevelDriveOperation::insert_for_known_path_key_element( + path_items, + key.to_vec(), + Element::new_sum_item(new_value), + ), + ); + } + } else { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "expected sum item element type", + ))); + } + Ok(()) + } + PathKeyElementSize((key_info_path, key_info, element)) => { + if let Element::SumItem(new_value, _) = element { + match apply_type { + BatchInsertApplyType::StatelessBatchInsert { + in_tree_using_sums, .. + } => { + // Estimate if the sum item with the given size already exists + drive_operations.push(CalculatedCostOperation( + GroveDb::average_case_for_has_raw( + &key_info_path, + &key_info, + element.serialized_size(&drive_version.grove_version)? as u32, + in_tree_using_sums, + &drive_version.grove_version, + )?, + )); + + drive_operations.push( + LowLevelDriveOperation::insert_for_estimated_path_key_element( + key_info_path, + key_info, + Element::new_sum_item(new_value), + ), + ); + Ok(()) + } + BatchInsertApplyType::StatefulBatchInsert => { + Err(Error::Drive(DriveError::NotSupportedPrivate( + "document sizes for stateful insert in batch operations not supported", + ))) + } + } + } else { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "expected sum item element type", + ))) + } + } + PathKeyUnknownElementSize(_) => Err(Error::Drive(DriveError::NotSupportedPrivate( + "document sizes in batch operations not supported", + ))), + } + } +} diff --git a/packages/rs-drive/src/util/grove_operations/grove_get_sum_tree_total_value/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/grove_get_sum_tree_total_value/v0/mod.rs index a2a27c7c641..65bd03f3e30 100644 --- a/packages/rs-drive/src/util/grove_operations/grove_get_sum_tree_total_value/v0/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/grove_get_sum_tree_total_value/v0/mod.rs @@ -14,7 +14,7 @@ use platform_version::version::drive_versions::DriveVersion; impl Drive { /// Gets the element at the given path from groveDB. /// Pushes the `OperationCost` of getting the element to `drive_operations`. - pub(crate) fn grove_get_sum_tree_total_value_v0>( + pub(super) fn grove_get_sum_tree_total_value_v0>( &self, path: SubtreePath<'_, B>, key: &[u8], diff --git a/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/mod.rs b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/mod.rs new file mode 100644 index 00000000000..8c5455a0380 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/mod.rs @@ -0,0 +1,54 @@ +//todo: uncomment once version 2.1 of GroveDB is released. +// mod v0; +// +// use crate::drive::Drive; +// use crate::error::drive::DriveError; +// use crate::error::Error; +// use crate::fees::op::LowLevelDriveOperation; +// use dpp::version::drive_versions::DriveVersion; +// use grovedb::{Element, TransactionArg}; +// use grovedb_path::SubtreePath; +// +// impl Drive { +// /// Inserts an element into groveDB only if the specified path and key do not exist. +// /// This operation costs are then stored in `drive_operations`. +// /// +// /// # Parameters +// /// * `path`: The groveDB hierarchical authenticated structure path where the new element is to be inserted. +// /// * `key`: The key where the new element should be inserted in the subtree. +// /// * `element`: The element to be inserted. +// /// * `transaction`: The groveDB transaction associated with this operation. +// /// * `drive_operations`: A vector to collect the costs of operations for later computation. In this case, +// /// it collects the cost of this insert operation if the path and key did not exist. +// /// * `platform_version`: The platform version to select the correct function version to run. +// /// +// /// # Returns +// /// * `Ok(true)` if the insertion was successful. +// /// * `Ok(false)` if the path and key already existed. +// /// * `Err(DriveError::UnknownVersionMismatch)` if the platform version does not match known versions. +// pub fn grove_insert_if_not_exists_return_existing_element>( +// &self, +// path: SubtreePath<'_, B>, +// key: &[u8], +// element: Element, +// transaction: TransactionArg, +// drive_operations: Option<&mut Vec>, +// drive_version: &DriveVersion, +// ) -> Result, Error> { +// match drive_version.grove_methods.basic.grove_insert_if_not_exists { +// 0 => self.grove_insert_if_not_exists_return_existing_element_v0( +// path, +// key, +// element, +// transaction, +// drive_operations, +// drive_version, +// ), +// version => Err(Error::Drive(DriveError::UnknownVersionMismatch { +// method: "grove_insert_if_not_exists_return_existing_element".to_string(), +// known_versions: vec![0], +// received: version, +// })), +// } +// } +// } diff --git a/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/v0/mod.rs b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/v0/mod.rs new file mode 100644 index 00000000000..5030cd6cc54 --- /dev/null +++ b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists_return_existing_element/v0/mod.rs @@ -0,0 +1,30 @@ +// use crate::drive::Drive; +// use crate::error::Error; +// use crate::fees::op::LowLevelDriveOperation; +// use crate::util::grove_operations::push_drive_operation_result_optional; +// use grovedb::{Element, TransactionArg}; +// use grovedb_path::SubtreePath; +// use platform_version::version::drive_versions::DriveVersion; +// +// impl Drive { +// /// Pushes the `OperationCost` of inserting an element in groveDB where the path key does not yet exist +// /// to `drive_operations`. +// pub(crate) fn grove_insert_if_not_exists_return_existing_element_v0>( +// &self, +// path: SubtreePath<'_, B>, +// key: &[u8], +// element: Element, +// transaction: TransactionArg, +// drive_operations: Option<&mut Vec>, +// drive_version: &DriveVersion, +// ) -> Result { +// let cost_context = self.grove.insert_if_not_exists_return_existing_element( +// path, +// key, +// element, +// transaction, +// &drive_version.grove_version, +// ); +// push_drive_operation_result_optional(cost_context, drive_operations) +// } +// } diff --git a/packages/rs-drive/src/util/grove_operations/mod.rs b/packages/rs-drive/src/util/grove_operations/mod.rs index a328aa8efb1..9861e146481 100644 --- a/packages/rs-drive/src/util/grove_operations/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/mod.rs @@ -108,10 +108,24 @@ pub mod grove_batch_operations_costs; /// Clear a subtree in grovedb pub mod grove_clear; +/// Provides functionality to delete items in a path based on a query. +pub mod batch_delete_items_in_path_query; + +/// Inserts an element if it does not exist and returns the existing element if it does. +pub mod batch_insert_if_not_exists_return_existing_element; + +/// Inserts a sum item or adds to it if it already exists. +pub mod batch_insert_sum_item_or_add_to_if_already_exists; + +/// Retrieves serialized or sum results from a path query in GroveDB. mod grove_get_path_query_serialized_or_sum_results; -/// Proved path query in grovedb with a conditional query + +/// Executes a proved path query in GroveDB with an optional conditional query. pub mod grove_get_proved_path_query_with_conditional; +/// Inserts an element if it does not exist and returns the existing element if it does in GroveDB. +pub mod grove_insert_if_not_exists_return_existing_element; + use grovedb_costs::CostContext; use grovedb::EstimatedLayerInformation; diff --git a/packages/rs-json-schema-compatibility-validator/Cargo.toml b/packages/rs-json-schema-compatibility-validator/Cargo.toml index d31332bb208..bc90fffebef 100644 --- a/packages/rs-json-schema-compatibility-validator/Cargo.toml +++ b/packages/rs-json-schema-compatibility-validator/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Ivan Shumkov "] [dependencies] json-patch = "1.2.0" serde_json = "1.0.115" -thiserror = "1.0.58" +thiserror = "1.0.64" once_cell = "1.19.0" [dev-dependencies] diff --git a/packages/rs-platform-value/Cargo.toml b/packages/rs-platform-value/Cargo.toml index 604a4dae4b9..f60a9a15a0f 100644 --- a/packages/rs-platform-value/Cargo.toml +++ b/packages/rs-platform-value/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" [dependencies] bincode = { version = "2.0.0-rc.3", features = ["serde"] } ciborium = { git = "https://github.com/qrayven/ciborium", branch = "feat-ser-null-as-undefined", optional = true } -thiserror = "1.0.58" +thiserror = "1.0.64" bs58 = "0.5.1" base64 = "0.22.1" hex = "0.4.3" diff --git a/packages/rs-platform-version/src/version/dpp_versions.rs b/packages/rs-platform-version/src/version/dpp_versions.rs index b42befd472b..7c84e660a49 100644 --- a/packages/rs-platform-version/src/version/dpp_versions.rs +++ b/packages/rs-platform-version/src/version/dpp_versions.rs @@ -271,4 +271,5 @@ pub struct DocumentMethodVersions { #[derive(Clone, Debug, Default)] pub struct DPPMethodVersions { pub epoch_core_reward_credits_for_distribution: FeatureVersion, + pub daily_withdrawal_limit: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_abci_versions.rs b/packages/rs-platform-version/src/version/drive_abci_versions.rs index 3d60df57366..22905e5ce53 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions.rs @@ -5,6 +5,7 @@ pub struct DriveAbciVersion { pub structs: DriveAbciStructureVersions, pub methods: DriveAbciMethodVersions, pub validation_and_processing: DriveAbciValidationVersions, + pub withdrawal_constants: DriveAbciWithdrawalConstants, pub query: DriveAbciQueryVersions, } @@ -112,6 +113,12 @@ pub struct DriveAbciValidationVersions { pub event_constants: DriveAbciValidationConstants, } +#[derive(Clone, Debug, Default)] +pub struct DriveAbciWithdrawalConstants { + pub core_expiration_blocks: u32, + pub cleanup_expired_locks_of_withdrawal_amounts_limit: u16, +} + #[derive(Clone, Debug, Default)] pub struct DriveAbciValidationConstants { pub maximum_vote_polls_to_process: u16, @@ -332,12 +339,14 @@ pub struct DriveAbciIdentityCreditWithdrawalMethodVersions { pub pool_withdrawals_into_transactions_queue: FeatureVersion, pub update_broadcasted_withdrawal_statuses: FeatureVersion, pub append_signatures_and_broadcast_withdrawal_transactions: FeatureVersion, + pub cleanup_expired_locks_of_withdrawal_amounts: FeatureVersion, } #[derive(Clone, Debug, Default)] pub struct DriveAbciProtocolUpgradeMethodVersions { pub check_for_desired_protocol_upgrade: FeatureVersion, pub upgrade_protocol_version_on_epoch_change: FeatureVersion, + pub perform_events_on_first_block_of_protocol_change: OptionalFeatureVersion, pub protocol_version_upgrade_percentage_needed: u64, } diff --git a/packages/rs-platform-version/src/version/drive_versions.rs b/packages/rs-platform-version/src/version/drive_versions.rs index 5e67347b443..c4bb57124c5 100644 --- a/packages/rs-platform-version/src/version/drive_versions.rs +++ b/packages/rs-platform-version/src/version/drive_versions.rs @@ -431,6 +431,7 @@ pub struct DriveGroveBasicMethodVersions { pub grove_insert_empty_tree: FeatureVersion, pub grove_insert_empty_sum_tree: FeatureVersion, pub grove_insert_if_not_exists: FeatureVersion, + pub grove_insert_if_not_exists_return_existing_element: FeatureVersion, pub grove_clear: FeatureVersion, pub grove_delete: FeatureVersion, pub grove_get_raw: FeatureVersion, @@ -454,11 +455,13 @@ pub struct DriveGroveBatchMethodVersions { pub batch_insert_empty_tree: FeatureVersion, pub batch_insert_empty_tree_if_not_exists: FeatureVersion, pub batch_insert_empty_tree_if_not_exists_check_existing_operations: FeatureVersion, + pub batch_insert_sum_item_or_add_to_if_already_exists: FeatureVersion, pub batch_insert: FeatureVersion, pub batch_insert_if_not_exists: FeatureVersion, pub batch_insert_if_changed_value: FeatureVersion, pub batch_replace: FeatureVersion, pub batch_delete: FeatureVersion, + pub batch_delete_items_in_path_query: FeatureVersion, pub batch_remove_raw: FeatureVersion, pub batch_delete_up_tree_while_empty: FeatureVersion, pub batch_refresh_reference: FeatureVersion, @@ -562,12 +565,13 @@ pub struct DriveIdentityMethodVersions { pub struct DriveIdentityWithdrawalMethodVersions { pub document: DriveIdentityWithdrawalDocumentMethodVersions, pub transaction: DriveIdentityWithdrawalTransactionMethodVersions, + pub calculate_current_withdrawal_limit: FeatureVersion, } #[derive(Clone, Debug, Default)] pub struct DriveIdentityWithdrawalDocumentMethodVersions { pub fetch_oldest_withdrawal_documents_by_status: FeatureVersion, - pub find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: FeatureVersion, + pub find_withdrawal_documents_by_status_and_transaction_indices: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/limits.rs b/packages/rs-platform-version/src/version/limits.rs index 207cde0d1ed..ef155395652 100644 --- a/packages/rs-platform-version/src/version/limits.rs +++ b/packages/rs-platform-version/src/version/limits.rs @@ -5,4 +5,5 @@ pub struct SystemLimits { pub max_state_transition_size: u64, pub max_transitions_in_documents_batch: u16, pub withdrawal_transactions_per_block_limit: u16, + pub max_withdrawal_amount: u64, } diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index bb83ecba960..813ac270dfd 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -465,7 +465,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -477,6 +477,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -547,6 +548,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -568,11 +570,13 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -636,6 +640,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 0, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: None, protocol_version_upgrade_percentage_needed: 75, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -670,6 +675,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 0, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -843,6 +849,10 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 0, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1264,7 +1274,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { default_current_version: 0, }, }, - methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0 }, + methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, daily_withdrawal_limit: 0 }, }, system_data_contracts: SystemDataContractVersions { withdrawals: 1, @@ -1280,6 +1290,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_state_transition_size: 20000, max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/mocks/v3_test.rs b/packages/rs-platform-version/src/version/mocks/v3_test.rs index d1366771292..a81949c4909 100644 --- a/packages/rs-platform-version/src/version/mocks/v3_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v3_test.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -465,7 +465,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -477,6 +477,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -547,6 +548,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -568,11 +570,13 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -636,6 +640,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 1, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: None, protocol_version_upgrade_percentage_needed: 67, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -670,6 +675,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 1, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -843,6 +849,10 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 64, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1266,6 +1276,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { }, methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, + daily_withdrawal_limit: 0, }, }, system_data_contracts: SystemDataContractVersions { @@ -1282,6 +1293,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { max_state_transition_size: 20480, //20 KiB max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, }, consensus: ConsensusVersions { tenderdash_consensus_version: 1, diff --git a/packages/rs-platform-version/src/version/v1.rs b/packages/rs-platform-version/src/version/v1.rs index b9d9eda4648..9ea4fad720c 100644 --- a/packages/rs-platform-version/src/version/v1.rs +++ b/packages/rs-platform-version/src/version/v1.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -464,7 +464,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -476,6 +476,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -546,6 +547,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -567,11 +569,13 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -635,6 +639,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 0, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: None, protocol_version_upgrade_percentage_needed: 75, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -669,6 +674,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 0, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -842,6 +848,10 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 0, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1265,6 +1275,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { }, methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, + daily_withdrawal_limit: 0, }, }, system_data_contracts: SystemDataContractVersions { @@ -1281,6 +1292,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { max_state_transition_size: 20480, //20 KiB max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/v2.rs b/packages/rs-platform-version/src/version/v2.rs index 2f80cedf64a..dc7b10212dc 100644 --- a/packages/rs-platform-version/src/version/v2.rs +++ b/packages/rs-platform-version/src/version/v2.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -464,7 +464,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -476,6 +476,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -546,6 +547,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -567,11 +569,13 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -635,6 +639,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 0, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: None, protocol_version_upgrade_percentage_needed: 75, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -669,6 +674,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 0, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -842,6 +848,10 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 0, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1265,6 +1275,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { }, methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, + daily_withdrawal_limit: 0, }, }, system_data_contracts: SystemDataContractVersions { @@ -1281,6 +1292,7 @@ pub const PLATFORM_V2: PlatformVersion = PlatformVersion { max_state_transition_size: 20480, //20 KiB max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/v3.rs b/packages/rs-platform-version/src/version/v3.rs index 314e17aa191..ba527c98f62 100644 --- a/packages/rs-platform-version/src/version/v3.rs +++ b/packages/rs-platform-version/src/version/v3.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -471,7 +471,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -483,6 +483,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -553,6 +554,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -574,11 +576,13 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -642,6 +646,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 1, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: None, protocol_version_upgrade_percentage_needed: 67, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -676,6 +681,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 0, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -849,6 +855,10 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 0, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1272,6 +1282,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { }, methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, + daily_withdrawal_limit: 0, }, }, system_data_contracts: SystemDataContractVersions { @@ -1288,6 +1299,7 @@ pub const PLATFORM_V3: PlatformVersion = PlatformVersion { max_state_transition_size: 20480, //20 KiB max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, }, consensus: ConsensusVersions { tenderdash_consensus_version: 1, diff --git a/packages/rs-platform-version/src/version/v4.rs b/packages/rs-platform-version/src/version/v4.rs index 86c62b8e8f0..4d05a510c9f 100644 --- a/packages/rs-platform-version/src/version/v4.rs +++ b/packages/rs-platform-version/src/version/v4.rs @@ -32,7 +32,7 @@ use crate::version::drive_abci_versions::{ DriveAbciStateTransitionValidationVersions, DriveAbciStructureVersions, DriveAbciValidationConstants, DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, DriveAbciValidationVersions, DriveAbciVersion, - DriveAbciVotingMethodVersions, PenaltyAmounts, + DriveAbciVotingMethodVersions, DriveAbciWithdrawalConstants, PenaltyAmounts, }; use crate::version::drive_versions::{ DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, @@ -466,7 +466,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { withdrawals: DriveIdentityWithdrawalMethodVersions { document: DriveIdentityWithdrawalDocumentMethodVersions { fetch_oldest_withdrawal_documents_by_status: 0, - find_up_to_100_withdrawal_documents_by_status_and_transaction_indices: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 0, }, transaction: DriveIdentityWithdrawalTransactionMethodVersions { index: DriveIdentityWithdrawalTransactionIndexMethodVersions { @@ -478,6 +478,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { dequeue_untied_withdrawal_transactions: 0, }, }, + calculate_current_withdrawal_limit: 0, }, }, platform_system: DrivePlatformSystemMethodVersions { @@ -548,6 +549,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { grove_insert_empty_tree: 0, grove_insert_empty_sum_tree: 0, grove_insert_if_not_exists: 0, + grove_insert_if_not_exists_return_existing_element: 0, grove_clear: 0, grove_delete: 0, grove_get_raw: 0, @@ -569,11 +571,13 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { batch_insert_empty_tree: 0, batch_insert_empty_tree_if_not_exists: 0, batch_insert_empty_tree_if_not_exists_check_existing_operations: 0, + batch_insert_sum_item_or_add_to_if_already_exists: 0, batch_insert: 0, batch_insert_if_not_exists: 0, batch_insert_if_changed_value: 0, batch_replace: 0, batch_delete: 0, + batch_delete_items_in_path_query: 0, batch_remove_raw: 0, batch_delete_up_tree_while_empty: 0, batch_refresh_reference: 0, @@ -637,6 +641,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { check_for_desired_protocol_upgrade: 1, upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: Some(4), protocol_version_upgrade_percentage_needed: 67, }, block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { @@ -671,6 +676,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { pool_withdrawals_into_transactions_queue: 0, update_broadcasted_withdrawal_statuses: 0, append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 0, }, voting: DriveAbciVotingMethodVersions { keep_record_of_finished_contested_resource_vote_poll: 0, @@ -844,6 +850,10 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { maximum_contenders_to_consider: 100, }, }, + withdrawal_constants: DriveAbciWithdrawalConstants { + core_expiration_blocks: 24, + cleanup_expired_locks_of_withdrawal_amounts_limit: 64, + }, query: DriveAbciQueryVersions { max_returned_elements: 100, response_metadata: 0, @@ -1267,6 +1277,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { }, methods: DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, + daily_withdrawal_limit: 0, }, }, system_data_contracts: SystemDataContractVersions { @@ -1283,6 +1294,7 @@ pub const PLATFORM_V4: PlatformVersion = PlatformVersion { max_state_transition_size: 20480, //20 KiB max_transitions_in_documents_batch: 1, withdrawal_transactions_per_block_limit: 4, + max_withdrawal_amount: 50_000_000_000_000, //500 Dash }, consensus: ConsensusVersions { tenderdash_consensus_version: 1, diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index b2efa718059..b53cbdca568 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -15,10 +15,10 @@ drive = { path = "../rs-drive", default-features = false, features = [ ] } drive-proof-verifier = { path = "../rs-drive-proof-verifier" } dapi-grpc-macros = { path = "../rs-dapi-grpc-macros" } -thiserror = "1.0.58" -tokio = { version = "1.36.0", features = ["macros"] } -tokio-util = { version = "0.7.10" } -async-trait = { version = "0.1.79" } +thiserror = "1.0.64" +tokio = { version = "1.40.0", features = ["macros"] } +tokio-util = { version = "0.7.12" } +async-trait = { version = "0.1.83" } http = { version = "0.2.12" } ciborium = { git = "https://github.com/qrayven/ciborium", branch = "feat-ser-null-as-undefined" } serde = { version = "1.0.197", default-features = false, features = [ @@ -38,7 +38,7 @@ bip37-bloom-filter = { git = "https://github.com/dashpay/rs-bip37-bloom-filter", pollster = { version = "0.3.0" } [dev-dependencies] -tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } rs-dapi-client = { path = "../rs-dapi-client", features = ["mocks"] } base64 = { version = "0.22.1" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/packages/strategy-tests/src/lib.rs b/packages/strategy-tests/src/lib.rs index 126538a64a0..61395d99f2a 100644 --- a/packages/strategy-tests/src/lib.rs +++ b/packages/strategy-tests/src/lib.rs @@ -45,13 +45,15 @@ use rand::prelude::StdRng; use rand::seq::{IteratorRandom, SliceRandom}; use rand::Rng; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::ops::RangeInclusive; use bincode::{Decode, Encode}; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::identifier::Identifier; use dpp::data_contract::document_type::DocumentType; +use dpp::fee::Credits; use dpp::identity::accessors::IdentityGettersV0; use dpp::platform_value::{BinaryData, Bytes32, Value}; -use dpp::ProtocolError; +use dpp::{dash_to_duffs, ProtocolError}; use dpp::ProtocolError::{PlatformDeserializationError, PlatformSerializationError}; use dpp::state_transition::documents_batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; use dpp::state_transition::documents_batch_transition::document_create_transition::{DocumentCreateTransition, DocumentCreateTransitionV0}; @@ -153,6 +155,7 @@ pub struct IdentityInsertInfo { pub frequency: Frequency, pub start_keys: u8, pub extra_keys: KeyMaps, + pub start_balance_range: RangeInclusive, } impl Default for IdentityInsertInfo { @@ -161,6 +164,7 @@ impl Default for IdentityInsertInfo { frequency: Default::default(), start_keys: 5, extra_keys: Default::default(), + start_balance_range: dash_to_duffs!(1)..=dash_to_duffs!(1), } } } @@ -1167,7 +1171,10 @@ impl Strategy { } // Generate state transition for identity top-up operation - OperationType::IdentityTopUp if !current_identities.is_empty() => { + OperationType::IdentityTopUp(_amount_range) + if !current_identities.is_empty() => + { + // todo: use amount ranges // Use a cyclic iterator over the identities to ensure we can create 'count' transitions let cyclic_identities = current_identities.iter().cycle(); @@ -1261,13 +1268,16 @@ impl Strategy { } // Generate state transition for identity withdrawal operation - OperationType::IdentityWithdrawal if !current_identities.is_empty() => { + OperationType::IdentityWithdrawal(amount_range) + if !current_identities.is_empty() => + { for i in 0..count { let index = (i as usize) % current_identities.len(); let random_identity = &mut current_identities[index]; let state_transition = crate::transitions::create_identity_withdrawal_transition( random_identity, + amount_range.clone(), identity_nonce_counter, signer, rng, @@ -1853,6 +1863,7 @@ mod tests { use crate::operations::{DocumentAction, DocumentOp, Operation, OperationType}; use crate::transitions::create_state_transitions_for_identities; use crate::{StartIdentities, Strategy}; + use dpp::dash_to_duffs; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::random_document::{ DocumentFieldFillSize, DocumentFieldFillType, @@ -1902,6 +1913,7 @@ mod tests { let start_identities = create_state_transitions_for_identities( vec![identity1, identity2], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), &mut simple_signer, &mut rng, platform_version, diff --git a/packages/strategy-tests/src/operations.rs b/packages/strategy-tests/src/operations.rs index c3e52414cd1..675e9968433 100644 --- a/packages/strategy-tests/src/operations.rs +++ b/packages/strategy-tests/src/operations.rs @@ -9,6 +9,7 @@ use dpp::data_contract::document_type::v0::random_document_type::RandomDocumentT use dpp::data_contract::document_type::DocumentType; use dpp::data_contract::serialized_version::DataContractInSerializationFormat; use dpp::data_contract::{DataContract as Contract, DataContract}; +use dpp::fee::Credits; use dpp::identifier::Identifier; use dpp::identity::IdentityPublicKey; use dpp::platform_value::Value; @@ -26,7 +27,7 @@ use platform_version::{TryFromPlatformVersioned, TryIntoPlatformVersioned}; use rand::distributions::{Distribution, WeightedIndex}; use rand::prelude::StdRng; use std::collections::BTreeMap; -use std::ops::Range; +use std::ops::{Range, RangeInclusive}; #[derive(Clone, Debug, PartialEq, Encode, Decode)] pub enum DocumentAction { @@ -494,12 +495,14 @@ impl VoteAction { } } +pub type AmountRange = RangeInclusive; + #[derive(Clone, Debug, PartialEq)] pub enum OperationType { Document(DocumentOp), - IdentityTopUp, + IdentityTopUp(AmountRange), IdentityUpdate(IdentityUpdateOp), - IdentityWithdrawal, + IdentityWithdrawal(AmountRange), ContractCreate(RandomDocumentTypeParameters, DocumentTypeCount), ContractUpdate(DataContractUpdateOp), IdentityTransfer, @@ -509,9 +512,9 @@ pub enum OperationType { #[derive(Clone, Debug, Encode, Decode)] enum OperationTypeInSerializationFormat { Document(Vec), - IdentityTopUp, + IdentityTopUp(AmountRange), IdentityUpdate(IdentityUpdateOp), - IdentityWithdrawal, + IdentityWithdrawal(AmountRange), ContractCreate(RandomDocumentTypeParameters, DocumentTypeCount), ContractUpdate(Vec), IdentityTransfer, @@ -540,12 +543,14 @@ impl PlatformSerializableWithPlatformVersion for OperationType { .serialize_consume_to_bytes_with_platform_version(platform_version)?; OperationTypeInSerializationFormat::Document(document_op_in_serialization_format) } - OperationType::IdentityTopUp => OperationTypeInSerializationFormat::IdentityTopUp, + OperationType::IdentityTopUp(amount_range) => { + OperationTypeInSerializationFormat::IdentityTopUp(amount_range) + } OperationType::IdentityUpdate(identity_update_op) => { OperationTypeInSerializationFormat::IdentityUpdate(identity_update_op) } - OperationType::IdentityWithdrawal => { - OperationTypeInSerializationFormat::IdentityWithdrawal + OperationType::IdentityWithdrawal(amount_range) => { + OperationTypeInSerializationFormat::IdentityWithdrawal(amount_range) } OperationType::ContractCreate(p, c) => { OperationTypeInSerializationFormat::ContractCreate(p, c) @@ -601,12 +606,14 @@ impl PlatformDeserializableWithPotentialValidationFromVersionedStructure for Ope )?; OperationType::Document(document_op) } - OperationTypeInSerializationFormat::IdentityTopUp => OperationType::IdentityTopUp, + OperationTypeInSerializationFormat::IdentityTopUp(amount_range) => { + OperationType::IdentityTopUp(amount_range) + } OperationTypeInSerializationFormat::IdentityUpdate(identity_update_op) => { OperationType::IdentityUpdate(identity_update_op) } - OperationTypeInSerializationFormat::IdentityWithdrawal => { - OperationType::IdentityWithdrawal + OperationTypeInSerializationFormat::IdentityWithdrawal(amount_range) => { + OperationType::IdentityWithdrawal(amount_range) } OperationTypeInSerializationFormat::ContractCreate(p, c) => { OperationType::ContractCreate(p, c) diff --git a/packages/strategy-tests/src/transitions.rs b/packages/strategy-tests/src/transitions.rs index 7b0f3d92c42..85d03eb333d 100644 --- a/packages/strategy-tests/src/transitions.rs +++ b/packages/strategy-tests/src/transitions.rs @@ -33,6 +33,7 @@ use dpp::withdrawal::Pooling; use rand::prelude::{IteratorRandom, StdRng}; use simple_signer::signer::SimpleSigner; +use crate::operations::AmountRange; use crate::KeyMaps; use dpp::dashcore::transaction::special_transaction::asset_lock::AssetLockPayload; use dpp::dashcore::transaction::special_transaction::TransactionPayload; @@ -76,6 +77,24 @@ pub fn instant_asset_lock_proof_fixture(one_time_private_key: PrivateKey) -> Ass AssetLockProof::Instant(is_lock_proof) } +pub fn instant_asset_lock_proof_fixture_with_dynamic_range( + one_time_private_key: PrivateKey, + amount_range: &AmountRange, + rng: &mut StdRng, +) -> AssetLockProof { + let transaction = instant_asset_lock_proof_transaction_fixture_with_dynamic_amount( + one_time_private_key, + amount_range, + rng, + ); + + let instant_lock = instant_asset_lock_is_lock_fixture(transaction.txid()); + + let is_lock_proof = InstantAssetLockProof::new(instant_lock, transaction, 0); + + AssetLockProof::Instant(is_lock_proof) +} + /// Constructs a fixture of a `Transaction` representing an instant asset lock proof. /// /// The `Transaction` structure is a basic unit of data in a blockchain, recording the transfer of assets between parties. @@ -160,6 +179,73 @@ pub fn instant_asset_lock_proof_transaction_fixture( } } +pub fn instant_asset_lock_proof_transaction_fixture_with_dynamic_amount( + one_time_private_key: PrivateKey, + amount_range: &AmountRange, + rng: &mut StdRng, +) -> Transaction { + let secp = Secp256k1::new(); + + let private_key_hex = "cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY"; + let private_key = PrivateKey::from_str(private_key_hex).unwrap(); + let public_key = private_key.public_key(&secp); + let public_key_hash = public_key.pubkey_hash(); + //let from_address = Address::p2pkh(&public_key, Network::Testnet); + let one_time_public_key = one_time_private_key.public_key(&secp); + + // We are going to fund 1 Dash and + // assume that input has 100005000 + // 5000 will be returned back + + let input_txid = + Txid::from_str("a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458").unwrap(); + + let input_outpoint = OutPoint::new(input_txid, 0); + + let input = TxIn { + previous_output: input_outpoint, + script_sig: ScriptBuf::new_p2pkh(&public_key_hash), + sequence: 0, + witness: Default::default(), + }; + + let one_time_key_hash = one_time_public_key.pubkey_hash(); + + let value_amount = if amount_range.start() == amount_range.end() { + *amount_range.start() //avoid using rng if possible + } else { + rng.gen_range(amount_range.clone()) + }; + + let funding_output = TxOut { + value: value_amount, + script_pubkey: ScriptBuf::new_p2pkh(&one_time_key_hash), + }; + + let burn_output = TxOut { + value: value_amount, + script_pubkey: ScriptBuf::new_op_return(&[]), + }; + + let change_output = TxOut { + value: 5000, + script_pubkey: ScriptBuf::new_p2pkh(&public_key_hash), + }; + + let payload = TransactionPayload::AssetLockPayloadType(AssetLockPayload { + version: 0, + credit_outputs: vec![funding_output], + }); + + Transaction { + version: 0, + lock_time: 0, + input: vec![input], + output: vec![burn_output, change_output], + special_transaction_payload: Some(payload), + } +} + /// Constructs a fixture of `InstantLock` representing an instant asset lock. /// /// The `InstantLock` structure is often used in blockchain systems to represent a condition where funds (or assets) are locked instantly, making them non-spendable until a specified condition is met or the lock duration expires. @@ -483,6 +569,7 @@ pub fn create_identity_update_transition_disable_keys( /// - If the identity does not have a suitable withdrawal address or key for signing. pub fn create_identity_withdrawal_transition( identity: &mut Identity, + amount_range: AmountRange, identity_nonce_counter: &mut BTreeMap, signer: &mut SimpleSigner, rng: &mut StdRng, @@ -499,12 +586,15 @@ pub fn create_identity_withdrawal_transition( // We can send it to the withdrawal address create_identity_withdrawal_transition_sent_to_identity_transfer_key( identity, + amount_range, identity_nonce_counter, signer, + rng, ) } else { create_identity_withdrawal_transition_with_output_address( identity, + amount_range, identity_nonce_counter, signer, rng, @@ -546,14 +636,16 @@ pub fn create_identity_withdrawal_transition( /// - If there's an error during the signing process. pub fn create_identity_withdrawal_transition_sent_to_identity_transfer_key( identity: &mut Identity, + amount_range: AmountRange, identity_nonce_counter: &mut BTreeMap, signer: &mut SimpleSigner, + rng: &mut StdRng, ) -> StateTransition { let nonce = identity_nonce_counter.entry(identity.id()).or_default(); *nonce += 1; let mut withdrawal: StateTransition = IdentityCreditWithdrawalTransitionV1 { identity_id: identity.id(), - amount: 1000000, // 1 duff + amount: rng.gen_range(amount_range), core_fee_per_byte: MIN_CORE_FEE_PER_BYTE, pooling: Pooling::Never, output_script: None, @@ -627,6 +719,7 @@ pub fn create_identity_withdrawal_transition_sent_to_identity_transfer_key( /// - If there's an error during the signing process. pub fn create_identity_withdrawal_transition_with_output_address( identity: &mut Identity, + amount_range: AmountRange, identity_nonce_counter: &mut BTreeMap, signer: &mut SimpleSigner, rng: &mut StdRng, @@ -635,10 +728,14 @@ pub fn create_identity_withdrawal_transition_with_output_address( *nonce += 1; let mut withdrawal: StateTransition = IdentityCreditWithdrawalTransitionV1 { identity_id: identity.id(), - amount: 1000000, // 1 duff + amount: rng.gen_range(amount_range), core_fee_per_byte: MIN_CORE_FEE_PER_BYTE, pooling: Pooling::Never, - output_script: Some(CoreScript::random_p2sh(rng)), + output_script: if rng.gen_bool(0.5) { + Some(CoreScript::random_p2pkh(rng)) + } else { + Some(CoreScript::random_p2sh(rng)) + }, nonce: *nonce, user_fee_increase: 0, signature_public_key_id: 0, @@ -923,6 +1020,7 @@ pub fn create_identities_state_transitions( /// - Conversion and encoding errors related to the cryptographic data. pub fn create_state_transitions_for_identities( identities: Vec, + amount_range: &AmountRange, signer: &SimpleSigner, rng: &mut StdRng, platform_version: &PlatformVersion, @@ -935,8 +1033,11 @@ pub fn create_state_transitions_for_identities( .unwrap(); let sk: [u8; 32] = pk.try_into().unwrap(); let secret_key = SecretKey::from_str(hex::encode(sk).as_str()).unwrap(); - let asset_lock_proof = - instant_asset_lock_proof_fixture(PrivateKey::new(secret_key, Network::Dash)); + let asset_lock_proof = instant_asset_lock_proof_fixture_with_dynamic_range( + PrivateKey::new(secret_key, Network::Dash), + amount_range, + rng, + ); let identity_create_transition = IdentityCreateTransition::try_from_identity_with_signer( &identity.clone(), diff --git a/packages/withdrawals-contract/Cargo.toml b/packages/withdrawals-contract/Cargo.toml index 357bc6d42f3..2523ed32026 100644 --- a/packages/withdrawals-contract/Cargo.toml +++ b/packages/withdrawals-contract/Cargo.toml @@ -7,7 +7,7 @@ rust-version.workspace = true license = "MIT" [dependencies] -thiserror = "1.0.58" +thiserror = "1.0.64" platform-version = { path = "../rs-platform-version" } platform-value = { path = "../rs-platform-value" } num_enum = "0.5.7" diff --git a/packages/withdrawals-contract/src/lib.rs b/packages/withdrawals-contract/src/lib.rs index 0eece0b61b8..baa5887f8e8 100644 --- a/packages/withdrawals-contract/src/lib.rs +++ b/packages/withdrawals-contract/src/lib.rs @@ -32,10 +32,15 @@ pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); IntoPrimitive, )] pub enum WithdrawalStatus { + /// The documents are in the state and waiting to be processed. QUEUED = 0, + /// Pooled happens when we are waiting for signing. POOLED = 1, + /// We have broadcasted the transaction to core. BROADCASTED = 2, + /// The transaction is now complete. COMPLETE = 3, + /// We broadcasted the transaction but core never saw it or rejected it. EXPIRED = 4, }