diff --git a/book/src/contract-keywords/refers-to-lookup.md b/book/src/contract-keywords/refers-to-lookup.md index affbe5914ed..6b29c0e8ae1 100644 --- a/book/src/contract-keywords/refers-to-lookup.md +++ b/book/src/contract-keywords/refers-to-lookup.md @@ -177,7 +177,7 @@ WHERE saltedDomainHash = -- findBy `constraints` (holding `minimumAgeBlocks` and [`chooseFirst`](#choosing-the-first-commitment-choosefirst)) and `consume` sit on the `refersTo`, beside `findBy`: they describe the document `findBy` finds when the referring document is created, so the reference must be judged on the create alone. A function in `findBy` makes it so, and so does a referring type whose documents are never replaced (`documentsMutable: false`): a replace is the one write that checks a reference again. Any other `findBy` declaring either is refused at registration (10231), and so is either beside [`inList`](refers-to-list-element.md), which checks that a list holds the value rather than finding a document by a key. - **`constraints.minimumAgeBlocks`**: the commitment's `$createdAtBlockHeight` is at least that many blocks below the create's height. The commitment type must list `$createdAtBlockHeight` in `required`. -- **`consume: true`**: the create deletes the commitment, its storage refunded to its owner. Only on a `deletableDocument` reference with the `where` entry `"$ownerId": "$ownerId"`, into a type of the same contract whose owner may delete its documents (`canBeDeleted: true`) or whose documents only a consume deletes (`canBeDeleted: "onlyWhenConsumed"`, see [Deletion](deletion.md#canbedeleted)), that declares no delete token cost or delete action fee and no [`deleteConstraints`](deletion.md#deleteconstraints) (no delete transition charges or judges them), and that requires no stricter signature security level than the revealing type. A contract-bound key signing the create must be allowed to act on the consumed type too (`ContractBoundedKeyOutOfBoundsError`, 20014). +- **`consume: true`**: the create deletes the commitment, its storage refunded to its owner. Only on a `deletableDocument` reference with the `where` entry `"$ownerId": "$ownerId"`, into a type of the same contract whose owner may delete its documents (`canBeDeleted: true`) or whose documents only a consume deletes (`canBeDeleted: "onlyWhenConsumed"`, see [Deletion](deletion.md#canbedeleted)), that declares no delete token cost or delete action fee and no [`deleteConstraints`](deletion.md#deleteconstraints) (no delete transition charges or judges them), and that requires no stricter signature security level than the revealing type. A contract-bound key signing the create must be allowed to act on the consumed type too (`ContractBoundedKeyOutOfBoundsError`, 20014). From protocol version 15 the delete uses the commitment `findBy` read, so the create pays for one read of it; before, the delete read it again by id and that read was charged too. A create missing a param, or with a value holding its separator, is refused before any read (`DocumentReferencePreimageInvalidError`, 10423). No commitment is `ReferencedEntityNotFoundError` (40120), another identity's `ReferencedDocumentPropertyMismatchError` (40127), and one too young `ReferencedDocumentRequirementNotMetError` (40142). The [Documents chapter](../data-model/documents.md#commit-and-reveal-a-findby-function) has the full rules. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs index a6eee31a0fd..0d6d630cf8c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_reference_validation/v0/mod.rs @@ -59,6 +59,7 @@ use drive::drive::identity::key::fetch::{ IdentityKeysRequest, OptionalSingleIdentityPublicKeyOutcome, }; use drive::query::{TransactionArg, WhereClause, WhereOperator}; +use drive::util::storage_flags::StorageFlags; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{ @@ -1366,7 +1367,7 @@ fn validate_reference_target_v0( } } }; - let looked_up_document; + let mut looked_up_document: Option<(Document, Option)> = None; let referenced_document: Option<&Document> = match (lookup, document_id) { (_, None) => None, (Some(lookup), Some(document_id)) => { @@ -1396,7 +1397,7 @@ fn validate_reference_target_v0( transaction, platform_version, )?; - looked_up_document.as_ref() + looked_up_document.as_ref().map(|(document, _)| document) } (None, Some(document_id)) => fetched_documents.document( effective_contract_id, @@ -1660,17 +1661,6 @@ fn validate_reference_target_v0( return Ok(result); } } - if lookup.consume { - consumed_documents.push(ConsumedLookupDocument { - document: ConsumedDocument { - document_id: found.id(), - document_type_name: document_type_name.to_string(), - }, - referenced_id: Identifier::from(referenced_id), - reference_target: reference_target.clone(), - path: path.to_string(), - }); - } } // What the type's rules read of the document, from the document in @@ -1686,6 +1676,26 @@ fn validate_reference_target_v0( // holds the value; the list is collected once per document and // path, so each value is a set lookup let document_exists = referenced_document.is_some(); + + // The commitment the reference consumes, recorded once nothing above reads it any + // more: the create deletes it from the document and storage flags the lookup read, + // moved here, without reading it again + if let (Some(lookup), Some((document, storage_flags))) = + (lookup, looked_up_document.take()) + { + if lookup.consume { + consumed_documents.push(ConsumedLookupDocument { + document: ConsumedDocument { + document, + storage_flags, + document_type_name: document_type_name.to_string(), + }, + referenced_id: Identifier::from(referenced_id), + reference_target: reference_target.clone(), + path: path.to_string(), + }); + } + } match (list_reference, document_id) { (Some(list_reference), Some(document_id)) if document_exists => fetched_documents .is_listed( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/consumed_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/consumed_documents.rs index cae5825b00e..c5cc3cb2802 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/consumed_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/consumed_documents.rs @@ -28,6 +28,7 @@ use crate::execution::validation::state_transition::batch::action_validation::do use dpp::consensus::state::document::referenced_entity_not_found_error::ReferencedEntityNotFoundError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; +use dpp::document::DocumentV0Getters; use dpp::identifier::Identifier; use dpp::validation::SimpleConsensusValidationResult; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; @@ -94,7 +95,7 @@ impl ConsumedDocuments { let document = ( contract_id, consumption.document.document_type_name.clone(), - consumption.document.document_id, + consumption.document.document.id(), ); if self.written.contains(&document) || self.consumed.contains(&document) @@ -125,7 +126,7 @@ impl ConsumedDocuments { ( contract_id, consumption.document.document_type_name.clone(), - consumption.document.document_id, + consumption.document.document.id(), ) })); } @@ -150,6 +151,7 @@ mod tests { use super::*; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::DocumentPropertyReferenceTarget; + use dpp::document::{Document, DocumentV0}; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::version::PlatformVersion; use drive::drive::contract::DataContractFetchInfo; @@ -168,7 +170,11 @@ mod tests { fn consumption(document_id: [u8; 32]) -> ConsumedLookupDocument { ConsumedLookupDocument { document: ConsumedDocument { - document_id: Identifier::from(document_id), + document: Document::V0(DocumentV0 { + id: Identifier::from(document_id), + ..Default::default() + }), + storage_flags: None, document_type_name: "preorder".to_string(), }, referenced_id: Identifier::from([9; 32]), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index aba579397bb..b542d9762ba 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::{ @@ -19,13 +20,16 @@ use dpp::state_transition::batch_transition::batched_transition::document_transi use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; -use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; +use drive::drive::document::query::{ + QueryDocumentsOutcomeV0Methods, QueryDocumentsWithFlagsOutcomeV0Methods, +}; use drive::drive::Drive; use drive::grovedb::TransactionArg; use drive::query::drive_contested_document_query::{ DriveContestedDocumentQuery, PrimaryContestedInternalClauses, }; use drive::query::{DriveDocumentQuery, InternalClauses, OrderClause, WhereClause, WhereOperator}; +use drive::util::storage_flags::StorageFlags; use std::collections::BTreeMap; // ============================================================================ @@ -456,8 +460,9 @@ pub(crate) fn hash_lookup_key( Some(BilledLookupKey(digest)) } -/// The document a `refersTo` lookup resolves to for one value: the one the -/// declared unique index of `document_type` finds for the key assembled from +/// The document a `refersTo` lookup resolves to for one value, with the storage flags of +/// its stored element from protocol version 15 (a create that consumes it deletes it from +/// these, without reading it again; `None` before): the one the declared unique index of `document_type` finds for the key assembled from /// `reference_value` (the identifier property's value, one array element, the /// writer or the creator, or the value of a string or byte array property /// carrying the reference), the writer `owner_id` and the sources in @@ -496,7 +501,7 @@ pub(crate) fn fetch_document_through_lookup( execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, -) -> Result, Error> { +) -> Result)>, Error> { // Registration resolved the index its `findBy` names, and a type's indexes // never change, so this only guards a declaration no registration admits: // one naming no unique index finds no document @@ -578,30 +583,64 @@ pub(crate) fn fetch_document_through_lookup( sub_queries: vec![], }; - let documents_outcome = drive.query_documents( - drive_query, - Some(epoch), - false, - transaction, - Some(platform_version.protocol_version), - )?; + // From `delete_consumed_document_operations` 1 (protocol version 15) the read keeps the + // stored element's storage flags, so a create consuming the document deletes it from + // them; before, the consume reads it again by id, and this is the plain document query + let (documents, cost) = match platform_version + .drive + .methods + .document + .delete + .delete_consumed_document_operations + { + 0 => { + let outcome = drive.query_documents( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + let cost = outcome.cost(); + let documents = outcome + .documents_owned() + .into_iter() + .map(|document| (document, None)) + .collect::>(); + (documents, cost) + } + 1 => { + let outcome = drive.query_documents_with_flags( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + let cost = outcome.cost(); + (outcome.documents_owned(), cost) + } + version => { + return Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "fetch_document_through_lookup".to_string(), + known_versions: vec![0, 1], + received: version, + })) + } + }; execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { storage_fee: 0, - processing_fee: documents_outcome.cost(), + processing_fee: cost, fee_refunds: Default::default(), removed_bytes_from_system: 0, lifetime_storage_fees: Default::default(), })); - Ok(documents_outcome - .documents_owned() - .into_iter() - .next() - .filter(|document| { - key_values.iter().all(|(index_property, value)| { - holds_key_part(document_type, document, index_property, value) - }) - })) + Ok(documents.into_iter().next().filter(|(document, _)| { + key_values.iter().all(|(index_property, value)| { + holds_key_part(document_type, document, index_property, value) + }) + })) } /// Whether `document`, found through an index of `document_type` under a key diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/commit_reveal_lookup.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/commit_reveal_lookup.rs index 45a83377963..e6c3b3629c5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/commit_reveal_lookup.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/commit_reveal_lookup.rs @@ -106,6 +106,8 @@ mod commit_reveal_lookup_tests { struct CommitRevealFixture { platform: TempPlatform, + /// The protocol version the chain runs and every write is processed at. + platform_version: &'static PlatformVersion, contract: DataContract, rng: StdRng, alice: Writer, @@ -120,9 +122,17 @@ mod commit_reveal_lookup_tests { /// The fixture with `change` applied to the contract's JSON before it is parsed. fn new_with(change: impl FnOnce(&mut serde_json::Value)) -> Self { - let platform_version = PlatformVersion::latest(); + Self::new_at(PlatformVersion::latest(), change) + } + + /// The fixture on a chain at `platform_version`, with `change` applied to the + /// contract's JSON before it is parsed. + fn new_at( + platform_version: &'static PlatformVersion, + change: impl FnOnce(&mut serde_json::Value), + ) -> Self { let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -161,6 +171,7 @@ mod commit_reveal_lookup_tests { Self { platform, + platform_version, contract, rng: StdRng::seed_from_u64(9061), alice, @@ -191,7 +202,7 @@ mod commit_reveal_lookup_tests { transition: &StateTransition, height: u64, ) -> StateTransitionExecutionResult { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let platform_state = self.platform.state.load(); let serialized = transition .serialize_to_bytes() @@ -263,7 +274,7 @@ mod commit_reveal_lookup_tests { absent: &[&str], token_payment_info: Option, ) -> (Document, StateTransition) { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let owner_id = self.id(who); let document_type = contract .document_type_for_name(type_name) @@ -321,7 +332,7 @@ mod commit_reveal_lookup_tests { transition: &StateTransition, level: CheckTxLevel, ) -> (Vec, Option) { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let state = self.platform.state.load(); let platform_ref = PlatformRef { drive: &self.platform.drive, @@ -342,7 +353,7 @@ mod commit_reveal_lookup_tests { /// Sets `who`'s balance to `credits`. fn set_balance(&self, who: Who, credits: Credits) { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let id = self.id(who).to_buffer(); let balance = self .platform @@ -418,7 +429,7 @@ mod commit_reveal_lookup_tests { change: impl FnOnce(&mut Document), height: u64, ) -> StateTransitionExecutionResult { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let mut replacement = document.clone(); replacement .increment_revision() @@ -456,7 +467,7 @@ mod commit_reveal_lookup_tests { document: &Document, height: u64, ) -> StateTransitionExecutionResult { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let contract = self.contract.clone(); let document_type = contract .document_type_for_name(type_name) @@ -487,7 +498,7 @@ mod commit_reveal_lookup_tests { /// Whether the `type_name` document with `id` is in state. fn document_exists(&self, type_name: &str, id: Identifier) -> bool { - let platform_version = PlatformVersion::latest(); + let platform_version = self.platform_version; let mut execution_context = StateTransitionExecutionContext::default_for_platform_version(platform_version) .expect("expected an execution context"); @@ -1224,7 +1235,7 @@ mod commit_reveal_lookup_tests { ); assert_matches!( consumed.as_slice(), - [consumed] if consumed.document.document_id == preorder.id() + [consumed] if consumed.document.document.id() == preorder.id() && consumed.document.document_type_name == "preorder" && consumed.path == "preorderSalt" ); @@ -2801,32 +2812,40 @@ mod commit_reveal_lookup_tests { /// The fees of a reveal choosing the first of the preorders of its name, refused and /// accepted: the found preorder's `validWhen` hash, the competitors query and each /// competitor's `validWhen` hash are processing, on top of what a reveal without - /// `chooseFirst` costs. + /// `chooseFirst` costs. From protocol version 15 the accepted reveal deletes the preorder + /// it consumes from what its lookup read, without reading it again. #[tokio::test] async fn should_pin_the_fees_of_a_reveal_choosing_the_first_of_its_preorders() { - let mut fixture = choose_first_fixture(true); - commit_name(&mut fixture, Who::Alice, COMMIT_HEIGHT).await; - commit_name(&mut fixture, Who::Mallory, COMMIT_HEIGHT + 1).await; + for (protocol_version, accepted_processing_fee) in [(14, 3_574_720), (15, 3_562_700)] { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected the platform version"); + let mut fixture = CommitRevealFixture::new_at(platform_version, |contract| { + add_name_registration(contract, true) + }); + commit_name(&mut fixture, Who::Alice, COMMIT_HEIGHT).await; + commit_name(&mut fixture, Who::Mallory, COMMIT_HEIGHT + 1).await; - let result = reveal_name(&mut fixture, Who::Mallory, COMMIT_HEIGHT + 2).await; - let PaidConsensusError { actual_fees, .. } = result else { - panic!("expected a paid refusal, got {result:?}"); - }; - assert_eq!( - (actual_fees.processing_fee, actual_fees.storage_fee), - (639_160, 0), - "the refused reveal pays the reads and hashes it made" - ); + let result = reveal_name(&mut fixture, Who::Mallory, COMMIT_HEIGHT + 2).await; + let PaidConsensusError { actual_fees, .. } = result else { + panic!("expected a paid refusal, got {result:?}"); + }; + assert_eq!( + (actual_fees.processing_fee, actual_fees.storage_fee), + (639_160, 0), + "the refused reveal pays the reads and hashes it made, at {protocol_version}" + ); - let result = reveal_name(&mut fixture, Who::Alice, COMMIT_HEIGHT + 2).await; - let StateTransitionExecutionResult::SuccessfulExecution { fee_result, .. } = result else { - panic!("expected the first committer's reveal to be accepted, got {result:?}"); - }; - assert_eq!( - (fee_result.processing_fee, fee_result.storage_fee), - (3_574_720, 25_299_000), - "the accepted reveal" - ); + let result = reveal_name(&mut fixture, Who::Alice, COMMIT_HEIGHT + 2).await; + let StateTransitionExecutionResult::SuccessfulExecution { fee_result, .. } = result + else { + panic!("expected the first committer's reveal to be accepted, got {result:?}"); + }; + assert_eq!( + (fee_result.processing_fee, fee_result.storage_fee), + (accepted_processing_fee, 25_299_000), + "the accepted reveal, at {protocol_version}" + ); + } } /// A signer who cannot pay the fee floor gets past the minimum balance pre-check only by diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns_v3.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns_v3.rs index fcf943c447d..95d2997325a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns_v3.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns_v3.rs @@ -1571,7 +1571,7 @@ mod dpns_v3_tests { (1_159_720, 31_158_000, 0), (1_846_280, 42_687_000, 0), (5_495_080, 11_558_570, 0), - (3_654_380, 42_687_000, 0) + (3_643_720, 42_687_000, 0) ] ); } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/mod.rs index c4884500950..1a9e54524a1 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/mod.rs @@ -4,9 +4,11 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use crate::util::storage_flags::StorageFlags; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; +use dpp::document::{Document, DocumentV0Getters}; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; @@ -158,4 +160,337 @@ impl Drive { })), } } + + /// Deletes `document`, a document a create consumes, which the create's `refersTo` lookup + /// read with its element's `storage_flags`, without its type's `canBeDeleted` guard. From + /// `delete_consumed_document_operations` 1 (protocol version 15) without reading it + /// again: the operations [`Self::force_delete_document_for_contract_operations`] gives, + /// less its read. Before, that force delete by the document's id, which reads it again. + #[allow(clippy::too_many_arguments)] + pub(crate) fn force_delete_read_document_for_contract_operations( + &self, + document: Document, + storage_flags: Option, + contract: &DataContract, + document_type: DocumentTypeRef, + previous_batch_operations: Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + block_time_ms: u64, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .document + .delete + .delete_consumed_document_operations + { + 0 => self.force_delete_document_for_contract_operations( + document.id(), + contract, + document_type, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + 1 => self.force_delete_read_document_for_contract_operations_v0( + document, + storage_flags, + contract, + document_type, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "force_delete_read_document_for_contract_operations".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } + } +} + +#[cfg(test)] +mod tests { + use crate::drive::Drive; + use crate::error::drive::DriveError; + use crate::error::Error; + use crate::fees::op::LowLevelDriveOperation; + use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; + use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use crate::util::storage_flags::StorageFlags; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use crate::util::test_helpers::setup_contract; + use assert_matches::assert_matches; + use dpp::block::block_info::BlockInfo; + use dpp::block::epoch::Epoch; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::DataContract; + use dpp::document::{Document, DocumentV0, DocumentV0Getters}; + use dpp::fee::fee_result::FeeResult; + use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; + use dpp::version::PlatformVersion; + use grovedb::batch::KeyInfoPath; + use grovedb::EstimatedLayerInformation; + use std::borrow::Cow; + use std::collections::HashMap; + + /// What a delete gives: its operations and, when it only estimates costs, the layer + /// information it estimated + type DeleteOutcome = ( + Vec, + Option>, + ); + + /// Deleting a stored `person` under `platform_version`: by its id, as the force delete + /// does, and from the document and storage flags already read, each with a layer + /// information map of its own when `estimate` is set. Gives the drive holding it too. + fn force_delete_by_id_and_from_read( + platform_version: &PlatformVersion, + estimate: bool, + ) -> (Drive, DeleteOutcome, DeleteOutcome) { + let drive = setup_drive_with_initial_state_structure(None); + let contract = setup_contract( + &drive, + "tests/supporting_files/contract/family/family-contract-reduced.json", + None, + None, + None::, + None, + None, + ); + let document_type = contract + .document_type_for_name("person") + .expect("expected the person document type"); + let owner_id = [3u8; 32]; + let document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some(owner_id.into()), + document_type, + platform_version, + ) + .expect("expected the person document"); + let storage_flags = StorageFlags::new_single_epoch(0, Some(owner_id)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + Some(Cow::Borrowed(&storage_flags)), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("expected to store the document"); + + let mut by_id_estimates = estimate.then(HashMap::new); + let by_id = drive + .force_delete_document_for_contract_operations( + document.id(), + &contract, + document_type, + None, + &mut by_id_estimates, + 0, + None, + platform_version, + ) + .expect("expected the force delete operations"); + let mut from_read_estimates = estimate.then(HashMap::new); + let from_read = drive + .force_delete_read_document_for_contract_operations( + document.clone(), + Some(storage_flags), + &contract, + document_type, + None, + &mut from_read_estimates, + 0, + None, + platform_version, + ) + .expect("expected the read-document delete operations"); + ( + drive, + (by_id, by_id_estimates), + (from_read, from_read_estimates), + ) + } + + /// How many layers `estimates` describes + fn layer_count(estimates: &Option>) -> usize { + estimates.as_ref().map_or(0, HashMap::len) + } + + /// The fee of `operations`, costs estimated from `estimates` when it is set, as an + /// estimated delete is charged + fn fee_of( + drive: &Drive, + operations: Vec, + estimates: Option>, + platform_version: &PlatformVersion, + ) -> FeeResult { + let mut drive_operations = vec![]; + drive + .apply_batch_low_level_drive_operations( + estimates, + None, + operations, + &mut drive_operations, + &platform_version.drive, + ) + .expect("expected to cost the operations"); + Drive::calculate_fee( + None, + Some(drive_operations), + &Epoch::new(0).expect("expected epoch 0"), + drive.config.epochs_per_era, + platform_version, + None, + ) + .expect("expected the fee") + } + + /// From protocol version 15 a consumed document is deleted from what its lookup read + #[test] + fn should_delete_a_read_document_as_a_force_delete_does_less_its_read() { + let (_, (by_id, _), (from_read, _)) = + force_delete_by_id_and_from_read(PlatformVersion::latest(), false); + // The force delete reads the document first; every operation after that read is + // the same, the storage removed and refunded included + assert_eq!(by_id.len(), from_read.len() + 1); + assert_eq!(&by_id[1..], from_read.as_slice()); + } + + /// From protocol version 15 the estimate of a consumed document's delete is the force + /// delete's, less its read: as many layers, and the type's estimated size stands for the + /// document in both. The estimated operations and layers hold made-up keys, so they are + /// compared by what they cost. + #[test] + fn should_estimate_a_read_document_delete_as_a_force_delete_less_its_read() { + let platform_version = PlatformVersion::latest(); + let (drive, (mut by_id, by_id_estimates), (from_read, from_read_estimates)) = + force_delete_by_id_and_from_read(platform_version, true); + assert_eq!(by_id.len(), from_read.len() + 1); + assert_matches!( + by_id.remove(0), + LowLevelDriveOperation::CalculatedCostOperation(_) + ); + assert_eq!( + layer_count(&by_id_estimates), + layer_count(&from_read_estimates) + ); + assert!(layer_count(&by_id_estimates) > 0); + assert_eq!( + fee_of(&drive, by_id, by_id_estimates, platform_version), + fee_of(&drive, from_read, from_read_estimates, platform_version) + ); + } + + /// At protocol version 14 a consumed document is deleted by its id, read again + #[test] + fn should_delete_a_consumed_document_by_its_id_at_protocol_version_14() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let (_, by_id, from_read) = force_delete_by_id_and_from_read(platform_version, false); + assert_eq!(by_id, from_read); + } + + /// At protocol version 14 the estimate of a consumed document's delete is the force + /// delete's by its id, its read included: as many operations and layers, the same fee + #[test] + fn should_estimate_a_consumed_document_delete_by_its_id_at_protocol_version_14() { + let platform_version = PlatformVersion::get(14).expect("expected protocol version 14"); + let (drive, (by_id, by_id_estimates), (from_read, from_read_estimates)) = + force_delete_by_id_and_from_read(platform_version, true); + assert_eq!(by_id.len(), from_read.len()); + assert_eq!( + layer_count(&by_id_estimates), + layer_count(&from_read_estimates) + ); + assert_eq!( + fee_of(&drive, by_id, by_id_estimates, platform_version), + fee_of(&drive, from_read, from_read_estimates, platform_version) + ); + } + + /// Deletes a document of `document_type_name` in the contract at `contract_path` from + /// what was read, at the latest protocol version, estimating costs only when `estimate` + /// is set. The type is refused before anything is read, so the contract is not stored + /// and the document is empty. + fn force_delete_read_document_of_type( + contract_path: &str, + document_type_name: &str, + estimate: bool, + ) -> Result, Error> { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract(contract_path, false, platform_version) + .expect("expected the contract"); + let document_type = contract + .document_type_for_name(document_type_name) + .expect("expected the document type"); + drive.force_delete_read_document_for_contract_operations( + Document::V0(DocumentV0::default()), + None, + &contract, + document_type, + None, + &mut estimate.then(HashMap::new), + 0, + None, + platform_version, + ) + } + + /// An indexOnly type has no stored document to delete, as for the force delete by id + #[test] + fn should_refuse_to_delete_a_read_index_only_document() { + for estimate in [false, true] { + assert_matches!( + force_delete_read_document_of_type( + "tests/supporting_files/contract/index-only-scalar-terminal/index-only-scalar-terminal-contract.json", + "answer", + estimate, + ), + Err(Error::Drive(DriveError::CorruptedCodeExecution(_))), + "estimating costs only: {estimate}" + ); + } + } + + /// A type that keeps history can not be deleted, as for the force delete by id + #[test] + fn should_refuse_to_delete_a_read_document_that_keeps_history() { + for estimate in [false, true] { + assert_matches!( + force_delete_read_document_of_type( + "tests/supporting_files/contract/family/family-contract-with-history.json", + "person", + estimate, + ), + Err(Error::Drive( + DriveError::InvalidDeletionOfDocumentThatKeepsHistory(_) + )), + "estimating costs only: {estimate}" + ); + } + } } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs index 4b0b751fc22..2397487824f 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v0/mod.rs @@ -7,6 +7,7 @@ use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; use dpp::data_contract::document_type::DocumentTypeRef; +use std::borrow::Cow; use std::collections::HashMap; use crate::drive::document::paths::contract_documents_primary_key_path; @@ -196,6 +197,71 @@ impl Drive { ) } + /// [`Self::force_delete_document_for_contract_operations_v0`] for a document already read + /// with its element's `storage_flags`: the same guards, then the operations + /// [`Self::delete_read_document_for_contract_operations_v0`] gives, without the read. When + /// only estimating costs, the document's type's estimated size stands for it, as it does + /// for the by-id delete. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn force_delete_read_document_for_contract_operations_v0( + &self, + document: Document, + storage_flags: Option, + contract: &DataContract, + document_type: DocumentTypeRef, + previous_batch_operations: Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + block_time_ms: u64, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if document_type.index_only() { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "indexOnly documents cannot be deleted by id: there is no primary-storage \ + row; use delete_index_only_document_for_contract_operations with the \ + document's values", + ))); + } + + if document_type.documents_keep_history() { + return Err(Error::Drive( + DriveError::InvalidDeletionOfDocumentThatKeepsHistory( + "this document type keeps history and therefore can not be deleted", + ), + )); + } + + let document_id = document.id(); + let document_info = if let Some(estimated_costs_only_with_layer_info) = + estimated_costs_only_with_layer_info + { + Self::add_estimation_costs_for_levels_up_to_contract_document_type_excluded( + contract, + estimated_costs_only_with_layer_info, + &platform_version.drive, + )?; + DocumentEstimatedAverageSize(document_type.estimated_size(platform_version)? as u32) + } else { + DocumentOwnedInfo((document, storage_flags.map(Cow::Owned))) + }; + + self.delete_read_document_for_contract_operations_v0( + document_id, + document_info, + contract, + document_type, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + vec![], + platform_version, + ) + } + /// The part of [`Self::force_delete_document_for_contract_operations_v0`] after the /// document is read from its primary storage: removes it there, removes its index /// entries and, for a type with a `ttl`, its expirations tree entry, appending to diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs index 733c643837d..dc261e34283 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs @@ -54,8 +54,9 @@ impl DriveHighLevelBatchOperationConverter for DocumentCreateTransitionAction { // version 14 batch state validation sets consumed documents, and only for a // create revealing a commitment through a `refersTo` lookup with a computed key // declaring `consume`, which only that version's parser produces. Every earlier - // create has none and converts to exactly the operations it always did - let consumed_documents = self.consumed_documents().to_vec(); + // create has none and converts to exactly the operations it always did. Taken, + // not copied: the document built from the action below does not read them + let consumed_documents = self.take_consumed_documents(); // The values of the type's derived index properties (protocol version 14), which // the reference validation took from the documents it fetched: Drive keys the @@ -134,14 +135,11 @@ impl DriveHighLevelBatchOperationConverter for DocumentCreateTransitionAction { // deleted without their owner's `canBeDeleted` guard: the consume rules // admitted them, and a type whose documents only a consume deletes // (`canBeDeleted: "onlyWhenConsumed"`) refuses its owner's delete - for consumed in consumed_documents { + for consumed_document in consumed_documents { ops.push(DocumentOperation( - DocumentOperationType::ForceDeleteDocument { - document_id: consumed.document_id, + DocumentOperationType::ForceDeleteReadDocument { + consumed_document, contract_info: DataContractFetchInfo(contract_fetch_info.clone()), - document_type_info: DocumentTypeInfo::DocumentTypeName( - consumed.document_type_name, - ), }, )); } @@ -177,10 +175,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentCreateTransitionAction { document_type_info: DocumentTypeInfo::DocumentTypeName( document_type_name, ), - consumed_documents: consumed_documents - .into_iter() - .map(|consumed| (consumed.document_id, consumed.document_type_name)) - .collect(), + consumed_documents, }, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/mod.rs index c55f74eeb69..7d49a0d462b 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/mod.rs @@ -125,6 +125,12 @@ impl DocumentCreateTransitionActionAccessorsV0 for DocumentCreateTransitionActio } } + fn take_consumed_documents(&mut self) -> Vec { + match self { + DocumentCreateTransitionAction::V0(v0) => std::mem::take(&mut v0.consumed_documents), + } + } + fn set_consumed_documents(&mut self, consumed_documents: Vec) { match self { DocumentCreateTransitionAction::V0(v0) => v0.consumed_documents = consumed_documents, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/mod.rs index 386b854b802..3596c4edb68 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/mod.rs @@ -24,6 +24,7 @@ use crate::state_transition_action::batch::batched_transition::document_transiti use crate::state_transition_action::batch::batched_transition::document_transition::drop_transient_values; use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfo; +use crate::util::storage_flags::StorageFlags; use dpp::version::PlatformVersion; use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo; @@ -73,11 +74,15 @@ pub struct DocumentCreateTransitionActionV0 { /// the commitment a `refersTo` lookup with a computed key found, the reference declaring /// `consume`. Its /// owner is the writer (registration demands the `$ownerId` agreement pair), so the delete is -/// the one that owner could have made, and its storage is refunded the same way. -#[derive(Debug, Clone, PartialEq, Eq)] +/// the one that owner could have made, and its storage is refunded the same way. It carries +/// the document and storage flags the lookup read, so from protocol version 15 the delete does +/// not read them again (the flags are `None` before, when the delete reads it by id). +#[derive(Debug, Clone, PartialEq)] pub struct ConsumedDocument { - /// The consumed document's id. - pub document_id: Identifier, + /// The consumed document, as the lookup read it. + pub document: Document, + /// The storage flags of the consumed document's stored element, as the lookup read them. + pub storage_flags: Option, /// The consumed document's type, in the create's contract. pub document_type_name: String, } @@ -143,6 +148,9 @@ pub trait DocumentCreateTransitionActionAccessorsV0 { /// The documents this create consumes, deleted in the same state transition. fn consumed_documents(&self) -> &[ConsumedDocument]; + /// Takes the documents this create consumes, leaving none. + fn take_consumed_documents(&mut self) -> Vec; + /// Sets the documents this create consumes. fn set_consumed_documents(&mut self, consumed_documents: Vec); diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/document.rs b/packages/rs-drive/src/util/batch/drive_op_batch/document.rs index da0c12b3877..0a527311013 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/document.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/document.rs @@ -1,6 +1,7 @@ use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::ConsumedDocument; use crate::util::batch::drive_op_batch::DriveLowLevelOperationConverter; use crate::util::object_size_info::DocumentInfo::{DocumentRefAndSerialization, DocumentRefInfo}; use crate::util::object_size_info::{ @@ -107,8 +108,8 @@ pub enum DocumentOperationType<'a> { contract_info: DataContractInfo<'a>, /// Document type document_type_info: DocumentTypeInfo<'a>, - /// The consumed documents, by id and document type name - consumed_documents: Vec<(Identifier, String)>, + /// The consumed documents, as the create's lookups read them + consumed_documents: Vec, }, /// Deletes a document DeleteDocument { @@ -121,12 +122,12 @@ pub enum DocumentOperationType<'a> { }, /// Deletes a document without consulting `canBeDeleted`, which rules what the document's /// own owner may do. Used for a deletion on behalf of the contract's moderators (the - /// caller has checked that the document type sets `moderatorAbilities.delete`), for a - /// document a contested create consumes (the consume rules admitted it, protocol version - /// 14), and for the platform's deletion of a document whose type declares a `ttl` once it - /// has passed (protocol version 14), which also removes the document's expirations tree - /// entry. A document type that keeps history is still refused, as both keywords are on - /// such a type. + /// caller has checked that the document type sets `moderatorAbilities.delete`) and for + /// the platform's deletion of a document whose type declares a `ttl` once it has passed + /// (protocol version 14), which also removes the document's expirations tree entry. A + /// document a contested create consumes goes through `ForceDeleteReadDocument`. A + /// document type that keeps history is still refused, as both keywords are on such a + /// type. ForceDeleteDocument { /// The document id document_id: Identifier, @@ -135,6 +136,16 @@ pub enum DocumentOperationType<'a> { /// Document type document_type_info: DocumentTypeInfo<'a>, }, + /// Deletes a document a contested create consumes, as `ForceDeleteDocument` does: from + /// protocol version 15 from the document and storage flags the create's lookup read, + /// without reading it again, and before by its id + /// (`Drive::force_delete_read_document_for_contract_operations`). + ForceDeleteReadDocument { + /// The consumed document, as the lookup read it + consumed_document: ConsumedDocument, + /// Data Contract info to potentially be resolved if needed + contract_info: DataContractInfo<'a>, + }, /// Deletes an indexOnly document from its property values — there is /// no primary-storage row to fetch, so the values (plus the owner) /// are what every index entry is recomputed from. `$createdAt` may @@ -333,16 +344,37 @@ impl DocumentOperationType<'_> { document_type_info.clone().resolve(contract)?, true, )?; - for (_, document_type_name) in consumed_documents { + for consumed in consumed_documents { visit( contract, - DocumentTypeInfo::DocumentTypeName(document_type_name.clone()) + DocumentTypeInfo::DocumentTypeName(consumed.document_type_name.clone()) .resolve(contract)?, false, )?; } Ok(()) } + Self::ForceDeleteReadDocument { + consumed_document, + contract_info, + } => { + let resolved = contract_info.clone().resolve( + drive, + block_info, + transaction, + &mut vec![], + platform_version, + )?; + let contract = resolved.as_ref(); + visit( + contract, + DocumentTypeInfo::DocumentTypeName( + consumed_document.document_type_name.clone(), + ) + .resolve(contract)?, + false, + ) + } // These write to system contracts. Self::AddWithdrawalDocument { .. } | Self::DocumentHistory { .. } => Ok(()), } @@ -550,11 +582,13 @@ impl DocumentOperationType<'_> { // without its owner's `canBeDeleted` guard: the consume rules admitted it, // and a type whose documents only a consume deletes // (`canBeDeleted: "onlyWhenConsumed"`) refuses its owner's delete - for (document_id, document_type_name) in consumed_documents { + for consumed in consumed_documents { let consumed_document_type = - DocumentTypeInfo::DocumentTypeName(document_type_name).resolve(contract)?; - let mut operations = drive.force_delete_document_for_contract_operations( - document_id, + DocumentTypeInfo::DocumentTypeName(consumed.document_type_name) + .resolve(contract)?; + let mut operations = drive.force_delete_read_document_for_contract_operations( + consumed.document, + consumed.storage_flags, contract, consumed_document_type, Some(&mut drive_operations), @@ -623,6 +657,36 @@ impl DocumentOperationType<'_> { platform_version, ) } + DocumentOperationType::ForceDeleteReadDocument { + consumed_document, + contract_info, + } => { + let mut drive_operations: Vec = vec![]; + let contract_resolved_info = contract_info.resolve( + drive, + block_info, + transaction, + &mut drive_operations, + platform_version, + )?; + let contract = contract_resolved_info.as_ref(); + let document_type = + DocumentTypeInfo::DocumentTypeName(consumed_document.document_type_name) + .resolve(contract)?; + + // `ForceDeleteDocument`'s deletion, from the document its create's lookup read + drive.force_delete_read_document_for_contract_operations( + consumed_document.document, + consumed_document.storage_flags, + contract, + document_type, + None, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + platform_version, + ) + } DocumentOperationType::DeleteIndexOnlyDocument { document_id, owner_id, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs index bed3b0975cd..bc69096c600 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs @@ -4,6 +4,7 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; #[derive(Clone, Debug, Default)] pub struct DriveDocumentMethodVersions { @@ -183,6 +184,11 @@ pub struct DriveDocumentDeleteMethodVersions { /// The fee-applying indexOnly deletion wrapper (dormant slot, 0 in /// every table; only reachable for indexOnly document types). pub delete_index_only_document_for_contract: FeatureVersion, + /// How a create deletes a document its `refersTo` lookup consumes: 0 reads it again by + /// id (`force_delete_document_for_contract_operations`), 1 deletes it from the document + /// and storage flags the lookup read, which keeps them (`query_documents_with_flags`). + /// Only reachable from protocol version 14, where `consume` exists. + pub delete_consumed_document_operations: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs index f7995eae616..9f8a42ba1af 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs @@ -42,6 +42,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = delete_document_for_contract_operations: 0, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, + delete_consumed_document_operations: 0, }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs index b402a61a29e..db72c589388 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs @@ -44,6 +44,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = delete_document_for_contract_operations: 0, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, + delete_consumed_document_operations: 0, }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs index 7cf1d8631fb..521e6e19230 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs @@ -59,6 +59,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = delete_document_for_contract_operations: 0, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, + delete_consumed_document_operations: 0, }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs index 44e44030ea6..21ce6ee10cb 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs @@ -131,6 +131,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = delete_document_for_contract_operations: 0, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, + delete_consumed_document_operations: 0, }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v5.rs new file mode 100644 index 00000000000..331fc61d8ea --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v5.rs @@ -0,0 +1,17 @@ +use crate::version::drive_versions::drive_document_method_versions::{ + DriveDocumentDeleteMethodVersions, DriveDocumentMethodVersions, +}; + +use super::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; + +/// Protocol v15 document method versions: `delete_consumed_document_operations` 1, a create +/// deletes the document it consumes from what its `refersTo` lookup read, without reading it +/// again. +pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = + DriveDocumentMethodVersions { + delete: DriveDocumentDeleteMethodVersions { + delete_consumed_document_operations: 1, + ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.delete + }, + ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4 + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index ff1ce116d60..d12d010fb77 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,4 +1,5 @@ use crate::version::drive_versions::drive_contract_method_versions::v5::DRIVE_CONTRACT_METHOD_VERSIONS_V5; +use crate::version::drive_versions::drive_document_method_versions::v5::DRIVE_DOCUMENT_METHOD_VERSIONS_V5; use crate::version::drive_versions::drive_verify_method_versions::v4::DRIVE_VERIFY_METHOD_VERSIONS_V4; use crate::version::drive_versions::drive_vote_method_versions::v4::DRIVE_VOTE_METHOD_VERSIONS_V4; use crate::version::drive_versions::v9::DRIVE_VERSION_V9; @@ -21,6 +22,9 @@ use crate::version::drive_versions::{DriveMethodVersions, DriveVersion}; /// `cleanup.remove_vote_poll_end_date_query_operations`, which removes finished polls from /// the end date index, is `Some(0)` (`None` before). `DRIVE_VERIFY_METHOD_VERSIONS_V4` makes /// the yes/no proof verifiers `Some(0)` (`None` before). +/// * **A consume deletes what its lookup read**: `DRIVE_DOCUMENT_METHOD_VERSIONS_V5` sets +/// `delete.delete_consumed_document_operations` to 1: a create deletes the document it +/// consumes from what its `refersTo` lookup read, without reading it again. /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { @@ -28,6 +32,7 @@ pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, // changed in v10: compiled contract storage vote: DRIVE_VOTE_METHOD_VERSIONS_V4, // changed in v10: the yes/no poll kind verify: DRIVE_VERIFY_METHOD_VERSIONS_V4, // changed in v10: the yes/no proof verifiers + document: DRIVE_DOCUMENT_METHOD_VERSIONS_V5, // changed in v10: a consume deletes what its lookup read ..DRIVE_VERSION_V9.methods }, ..DRIVE_VERSION_V9 diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 855e4a41567..42badb38899 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -223,9 +223,20 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// (`DRIVE_VERIFY_METHOD_VERSIONS_V4`). /// Nothing calls the opener yet, so at this version every yes/no vote is /// refused for want of a poll. +/// +/// 11. **A consume deletes what its lookup read**: `DRIVE_VERSION_V10` selects +/// `DRIVE_DOCUMENT_METHOD_VERSIONS_V5`, whose `delete_consumed_document_operations` 1 +/// has a `refersTo` lookup keep the stored element's storage flags beside the document it +/// finds (`query_documents_with_flags`, in `fetch_document_through_lookup`), and a create +/// that consumes the document deletes it from them +/// (`Drive::force_delete_read_document_for_contract_operations`, the force delete's +/// operations less its read), where 0 reads it again by id and bills that read. A consume +/// costs one document read less: a DPNS v3 name create's processing fee drops by 10,660 +/// credits. Storage fees and refunds are unchanged. Protocol v14 and earlier keep +/// `delete_consumed_document_operations` 0. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) + drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10); delete_consumed_document_operations 1 (note 11) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock