From 8eeca0cb90929122e727237f02057e9177244002 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 10 Sep 2026 22:59:32 +0700 Subject: [PATCH 01/48] feat(platform): add version slots for the document lifecycle Protocol version 14 gains the slots the delete and erase lifecycle of keep-history documents needs: the erase transition's wire bounds, its structure and state validation, the lifecycle read every stateful check goes through, the keep-history branch of the document delete, the erase storage operation and its estimation, and the bound on how many retained revisions one erase transition may remove. Every new field is dormant in the released tables and in the hand-written mock, so no released protocol version changes behaviour. Co-Authored-By: Claude Opus 5 (1M context) --- .../mod.rs | 5 ++++ .../v1.rs | 2 ++ .../v2.rs | 2 ++ .../v3.rs | 25 ++++++++++++++----- .../drive_abci_validation_versions/mod.rs | 11 ++++++++ .../drive_abci_validation_versions/v1.rs | 3 +++ .../drive_abci_validation_versions/v10.rs | 14 ++++++++--- .../drive_abci_validation_versions/v2.rs | 3 +++ .../drive_abci_validation_versions/v3.rs | 3 +++ .../drive_abci_validation_versions/v4.rs | 3 +++ .../drive_abci_validation_versions/v5.rs | 3 +++ .../drive_abci_validation_versions/v6.rs | 3 +++ .../drive_abci_validation_versions/v7.rs | 3 +++ .../drive_abci_validation_versions/v8.rs | 3 +++ .../drive_abci_validation_versions/v9.rs | 3 +++ .../drive_document_method_versions/mod.rs | 13 ++++++++++ .../drive_document_method_versions/v1.rs | 3 +++ .../drive_document_method_versions/v2.rs | 3 +++ .../drive_document_method_versions/v3.rs | 3 +++ .../drive_document_method_versions/v4.rs | 19 +++++++++++--- .../mod.rs | 3 +++ .../v1.rs | 1 + .../v2.rs | 1 + .../v3.rs | 1 + .../v4.rs | 1 + .../src/version/mocks/v2_test.rs | 1 + .../src/version/system_limits/mod.rs | 17 +++++++++++++ .../src/version/system_limits/v1.rs | 1 + .../src/version/system_limits/v2.rs | 1 + .../src/version/system_limits/v3.rs | 1 + .../src/version/system_limits/v4.rs | 1 + 31 files changed, 144 insertions(+), 12 deletions(-) diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs index 8e5996f3ba2..e4ae4bfd3a8 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs @@ -28,6 +28,11 @@ pub struct DPPStateTransitionSerializationVersions { /// as `OptionalFeatureVersion`), and the batch basic-structure wire /// gate rejects the variant wherever this is `None`. pub document_index_only_delete_state_transition: Option, + /// The erase kind, which purges the retained revisions of a deleted + /// keep-history document. `None` below protocol version 14 — the kind does + /// not exist on the wire there, and the batch basic-structure wire gate + /// rejects the variant wherever this is `None`. + pub document_erase_state_transition: Option, pub document_transfer_state_transition: DocumentFeatureVersionBounds, pub document_update_price_state_transition: DocumentFeatureVersionBounds, pub document_purchase_state_transition: DocumentFeatureVersionBounds, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs index e6d53d23513..770faf5bc76 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs @@ -98,6 +98,8 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V1: DPPStateTransitionSerializ }, // The indexOnly delete kind joins the wire at PV14. document_index_only_delete_state_transition: None, + // The erase kind joins the wire at PV14. + document_erase_state_transition: None, document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs index c2f5c219fa2..1b05bb649cc 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs @@ -98,6 +98,8 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V2: DPPStateTransitionSerializ }, // The indexOnly delete kind joins the wire at PV14. document_index_only_delete_state_transition: None, + // The erase kind joins the wire at PV14. + document_erase_state_transition: None, document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs index d1e59b2dae3..4768a0722ea 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -1,9 +1,15 @@ -//! V3 (PV14): identical to V2 except the new -//! `document_index_only_delete_state_transition` kind exists (`Some`, V0 -//! only) — the indexOnly delete-by-values transition -//! (`DocumentIndexOnlyDeleteTransition`). It is its own kind, not a -//! version of the delete transition: `document_delete_state_transition` -//! keeps V2's bounds and keeps evolving independently for stored types. +//! V3 (PV14): identical to V2 except for two new kinds, both `Some` with V0 +//! bounds only. +//! +//! `document_index_only_delete_state_transition` is the indexOnly +//! delete-by-values transition (`DocumentIndexOnlyDeleteTransition`). +//! `document_erase_state_transition` is the erase transition +//! (`DocumentEraseTransition`), which purges the retained revisions of a +//! deleted keep-history document. +//! +//! Each is its own kind, not a version of the delete transition: +//! `document_delete_state_transition` keeps V2's bounds and keeps evolving +//! independently for stored types. use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ DPPStateTransitionSerializationVersions, DocumentFeatureVersionBounds, @@ -110,6 +116,13 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ default_current_version: 0, }, }), + document_erase_state_transition: Some(DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }), document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 6617537c899..192f4aacc46 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -225,6 +225,9 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_structure_validation: FeatureVersion, + /// The erase kind (PV14+); 0 in every earlier version table, where the + /// kind cannot appear because the wire gate rejects it. + pub document_erase_transition_structure_validation: FeatureVersion, pub document_replace_transition_structure_validation: FeatureVersion, pub document_transfer_transition_structure_validation: FeatureVersion, pub document_purchase_transition_structure_validation: FeatureVersion, @@ -235,6 +238,14 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_state_validation: FeatureVersion, + /// The erase kind (PV14+); 0 in every earlier version table, where the + /// kind cannot appear because the wire gate rejects it. + pub document_erase_transition_state_validation: FeatureVersion, + /// Versions `fetch_keep_history_document_lifecycle`, the stateful read + /// that classifies a keep-history document as active, deleted, erasing or + /// absent. 0 in every table before protocol version 14, where no caller + /// exists. + pub fetch_keep_history_document_lifecycle: FeatureVersion, pub document_replace_transition_state_validation: FeatureVersion, pub document_transfer_transition_state_validation: FeatureVersion, pub document_purchase_transition_state_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 3a23c94c751..2b7cdec0a5b 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -127,6 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -135,6 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = document_create_transition_state_validation: 0, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index cc1fd27797b..7b923eee2e3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -185,18 +185,26 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = }, is_allowed: 0, document_create_transition_structure_validation: 1, - // Reject deletes on legacy keep-history types as paid consensus errors. - // Protocols through 13 retain the original internal-error outcome. + // Keep-history types take part in the document lifecycle: a delete + // removes the current pointer and its index references while the + // retained revisions stay readable. Protocols through 13 retain the + // original internal-error outcome. document_delete_transition_structure_validation: 1, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, document_update_price_transition_structure_validation: 0, document_base_transition_state_validation: 0, document_create_transition_state_validation: 2, - document_delete_transition_state_validation: 0, + // Consults the keep-history lifecycle before deleting, so a delete + // of an already deleted or erasing document is a paid consensus + // error rather than a second removal. + document_delete_transition_state_validation: 1, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 1, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index c395ded0e36..ca2713f5ff9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -127,6 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -135,6 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 41d57f57298..c32d2f2b13a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -127,6 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -135,6 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 528f2d3594c..f98581dc9ba 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -130,6 +130,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -138,6 +139,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 4547fed686b..c8efc2c2d72 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -131,6 +131,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -139,6 +140,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index f0e7276bc20..5c5ce7aace1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -134,6 +134,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -142,6 +143,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 67017f621d1..88ab4148e91 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -128,6 +128,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -136,6 +137,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 01f465dac4c..1a143aae9e2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -182,6 +182,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -190,6 +191,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index df2c16c4f4b..1167b71b49e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -178,6 +178,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, + document_erase_transition_structure_validation: 0, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -186,6 +187,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, + document_erase_transition_state_validation: 0, + fetch_keep_history_document_lifecycle: 0, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, 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 7dd700ef08d..799d211c1e7 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 @@ -38,6 +38,11 @@ pub struct DriveDocumentQueryMethodVersions { /// Version 1 selects the protocol-15 keep-history layout, where the /// primary-key entry is the current document rather than a history tree. pub primary_key_path_query: FeatureVersion, + /// Reads the lifecycle record of one keep-history document and, when the + /// record says the document is deleted, its newest retained revision. + /// Present in every version table so the slot exists; unreachable before + /// protocol version 14, which introduces the lifecycle tree. + pub fetch_document_lifecycle: FeatureVersion, /// Mode-detection routing table for `SELECT COUNT` queries. /// Versioned because the routing table is consensus-relevant on /// the query surface — a future protocol version that changes @@ -155,6 +160,14 @@ 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, + /// Removes a bounded chunk of the retained revisions of a deleted + /// keep-history document, dropping the history subtree and the lifecycle + /// record with the terminal chunk. Present in every version table so the + /// slot exists; unreachable before protocol version 14. + pub erase_document_for_contract_operations: FeatureVersion, + /// Estimation layers for the erase chunk. Same dormancy as + /// `erase_document_for_contract_operations`. + pub add_estimation_costs_for_erase_document: 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 cea6d64165c..0606a70e8ea 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 @@ -18,6 +18,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, + fetch_document_lifecycle: 0, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -41,6 +42,8 @@ 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, + erase_document_for_contract_operations: 0, + add_estimation_costs_for_erase_document: 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 cd87455ef9c..d329e1e8124 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 @@ -20,6 +20,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, + fetch_document_lifecycle: 0, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -43,6 +44,8 @@ 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, + erase_document_for_contract_operations: 0, + add_estimation_costs_for_erase_document: 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 d8d3cf07b1a..5424d148305 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 @@ -30,6 +30,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, + fetch_document_lifecycle: 0, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -58,6 +59,8 @@ 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, + erase_document_for_contract_operations: 0, + add_estimation_costs_for_erase_document: 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 b4404c89b60..ed8d022de35 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 @@ -82,6 +82,9 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, + // The lifecycle tree is introduced with the delete and erase + // lifecycle; the read exists only from protocol version 14. + fetch_document_lifecycle: 0, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -92,11 +95,16 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = where_clause_grouping: 1, }, delete: DriveDocumentDeleteMethodVersions { - add_estimation_costs_for_remove_document_to_primary_storage: 0, + // v1 at protocol v14: a keep-history primary layer holds current + // pointers, not document items, so its layer sizes come from the + // reference shape. + add_estimation_costs_for_remove_document_to_primary_storage: 1, delete_document_for_contract: 0, delete_document_for_contract_id: 0, delete_document_for_contract_apply_and_add_to_operations: 0, - remove_document_from_primary_storage: 0, + // v1 at protocol v14: the stateless delete of a keep-history + // primary entry is sized as a reference. + remove_document_from_primary_storage: 1, // v1 at protocol v14: the empty-tree pruning climb stops at the // member level on `preallocated` indexOnly indexes, keeping the // trees the referenced document's insert paid for. @@ -105,9 +113,14 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = remove_indices_for_top_index_level_for_contract_operations: 2, delete_document_for_contract_id_with_named_type_operations: 0, delete_document_for_contract_with_named_type_operations: 0, - delete_document_for_contract_operations: 0, + // v1 at protocol v14: keep-history documents lose their current + // pointer and index references and gain a lifecycle record; the + // non-history path is delegated to v0 unchanged. + delete_document_for_contract_operations: 1, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, + erase_document_for_contract_operations: 0, + add_estimation_costs_for_erase_document: 0, }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index ed0b0326f34..f07f67eaafd 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -32,6 +32,9 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub document_create_transition: FeatureVersion, pub document_delete_transition: FeatureVersion, pub document_index_only_delete_transition: FeatureVersion, + /// The erase kind (protocol version 14+); 0 in every table, where the kind + /// either cannot appear or has only one conversion. + pub document_erase_transition: FeatureVersion, pub document_purchase_transition: FeatureVersion, pub document_replace_transition: FeatureVersion, pub document_transfer_transition: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs index 7955a70b8cc..69021c67d9b 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs @@ -20,6 +20,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V1: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, + document_erase_transition: 0, document_purchase_transition: 0, document_replace_transition: 0, document_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs index 8a34eebaf0d..8922224eb44 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs @@ -21,6 +21,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V2: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, + document_erase_transition: 0, document_purchase_transition: 0, document_replace_transition: 0, document_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs index 78288f4334d..74b1c4a361b 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs @@ -21,6 +21,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, + document_erase_transition: 0, // PROTOCOL_VERSION_13: v1 rewrites a transferred or purchased // DPNS domain document's `records.identity` to the new owner // so the username resolves to the buyer. v0 stays for diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index c60cb68582a..f02b720ee86 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -21,6 +21,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, + document_erase_transition: 0, // PROTOCOL_VERSION_13: v1 rewrites a transferred or purchased // DPNS domain document's `records.identity` to the new owner // so the username resolves to the buyer. v0 stays for 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 49e0cb4048f..44a45107bc1 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -534,6 +534,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, minimum_grovedb_proof_envelope_version: 0, + max_document_revisions_erased_per_transition: None, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index a1f83892a36..94ad6f57b47 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -142,6 +142,23 @@ pub struct SystemLimits { /// version 3 (protocol version 13), so every live network already serves /// V1 by the time the floor applies. pub minimum_grovedb_proof_envelope_version: u32, + /// Maximum number of retained revisions one erase transition may remove + /// from a deleted keep-history document. + /// + /// An erase is chunked so that a single transition can never expand into + /// unbounded work: the chunk is bounded here, and the admission estimate + /// every erase must have in balance is sized for a full chunk whatever the + /// document's actual history length. A document with more retained + /// revisions than this stays in the erasing state and is finished by + /// further erase transitions, which any identity may submit. + /// + /// The bound applies per transition, not per block, so raising it raises + /// the work a maximum legal block of erases can demand as well as the + /// balance every single erase needs up front. + /// + /// `None` preserves the behavior of protocol versions that predate the + /// document lifecycle, where no erase transition can exist. + pub max_document_revisions_erased_per_transition: Option, } #[cfg(test)] diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 63474654244..08323b579a5 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -54,4 +54,5 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + max_document_revisions_erased_per_transition: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index b83e8c951f1..1cec8aa2e01 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -35,4 +35,5 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + max_document_revisions_erased_per_transition: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index 6a78c885809..2f42b9162de 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -37,4 +37,5 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + max_document_revisions_erased_per_transition: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 6e1083f2806..3729cac5c56 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -66,4 +66,5 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_time_range_ttl_seconds: Some(604_800), // one week min_time_range_ttl_drop_operations_per_write: Some(32), minimum_grovedb_proof_envelope_version: 1, // clients reject legacy V0 GroveDB proof envelopes from v14 + max_document_revisions_erased_per_transition: Some(100), }; From 2c5f2d7d295cd1857b7cde0e5b9f99d0ccfc6e07 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 10 Sep 2026 22:59:43 +0700 Subject: [PATCH 02/48] feat(dpp)!: admit deletable keep-history types and add canBeErased Generation 3 of the document type parser no longer refuses documentsKeepHistory together with canBeDeleted. A delete on such a type removes the document from ordinary reads and leaves its retained revisions readable, so the two settings no longer contradict each other. It also admits canBeErased, which additionally allows a deleted document's revisions to be purged. The flag defaults to false, requires both documentsKeepHistory and canBeDeleted, and is immutable across contract updates: narrowing it once an erasure had begun would strand a partially erased document forever, and widening it would hand an irreversible operation to a type registered without it. The existing canBeDeleted narrowing stays, except on an erasable type, which could never reach a state erase acts on again. A keep-history type may not carry a contested index: a contested resource is awarded outside transition validation, at an id derived from the winner rather than from the contested values, and that award can land on an id whose retained history already exists. BREAKING CHANGE: at protocol version 14 a contract may combine documentsKeepHistory with canBeDeleted, which earlier drafts of that protocol version rejected. Co-Authored-By: Claude Opus 5 (1M context) --- .../document/v3/document-meta.json | 4 + .../document_type/accessors/mod.rs | 24 +++++ .../document_type/accessors/v2/mod.rs | 5 + .../try_from_schema/common/mod.rs | 102 +++++++++++++++++- .../class_methods/try_from_schema/v3/mod.rs | 36 +++---- .../methods/validate_update/common/mod.rs | 22 ++++ .../methods/validate_update/v1/mod.rs | 18 +++- .../src/data_contract/document_type/mod.rs | 6 ++ .../document_type/v2/accessors.rs | 4 + .../src/data_contract/document_type/v2/mod.rs | 11 ++ 10 files changed, 204 insertions(+), 28 deletions(-) diff --git a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json index 51e6b34c648..d3cdbd4a265 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -767,6 +767,10 @@ "type": "boolean", "description": "True if the documents can be deleted, default is true" }, + "canBeErased": { + "type": "boolean", + "description": "True if a deleted document may have its retained revisions purged by an erase transition; requires documentsKeepHistory and canBeDeleted, default is false, immutable on contract update" + }, "transferable": { "type": "integer", "enum": [ diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs index 6ea4e0cb2a8..86710502e76 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs @@ -980,6 +980,14 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V2(v2) => v2.index_only(), } } + + fn documents_can_be_erased(&self) -> bool { + match self { + DocumentType::V0(_) => false, + DocumentType::V1(_) => false, + DocumentType::V2(v2) => v2.documents_can_be_erased(), + } + } } impl DocumentTypeV2Setters for DocumentType { @@ -1056,6 +1064,14 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V2(v2) => v2.index_only(), } } + + fn documents_can_be_erased(&self) -> bool { + match self { + DocumentTypeRef::V0(_) => false, + DocumentTypeRef::V1(_) => false, + DocumentTypeRef::V2(v2) => v2.documents_can_be_erased(), + } + } } impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { @@ -1098,6 +1114,14 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V2(v2) => v2.index_only(), } } + + fn documents_can_be_erased(&self) -> bool { + match self { + DocumentTypeMutRef::V0(_) => false, + DocumentTypeMutRef::V1(_) => false, + DocumentTypeMutRef::V2(v2) => v2.documents_can_be_erased(), + } + } } #[cfg(test)] diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs index 68017106e45..0f4a952d4a5 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs @@ -30,6 +30,11 @@ pub trait DocumentTypeV2Getters { /// each terminating in an `Item` keyed by the index's `terminal` /// property. Only what is in the indexes exists and is recoverable. fn index_only(&self) -> bool; + + /// Returns whether a deleted document of this type may have its retained + /// revisions purged by an erase transition. Always false for document + /// types parsed by an earlier grammar, which has no such keyword. + fn documents_can_be_erased(&self) -> bool; } /// Trait providing setters for DocumentTypeV2-specific fields. diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 748ce4a3ff2..106513f7747 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -26,8 +26,8 @@ use crate::data_contract::document_type::index_level::IndexLevel; use crate::data_contract::document_type::property::DocumentProperty; use crate::data_contract::document_type::property::DocumentPropertyType; use crate::data_contract::document_type::property_names::{ - CAN_BE_DELETED, CREATION_RESTRICTION_MODE, DOCUMENTS_AVERAGEABLE, DOCUMENTS_COUNTABLE, - DOCUMENTS_KEEP_HISTORY, DOCUMENTS_MUTABLE, DOCUMENTS_SUMMABLE, INDEX_ONLY, + CAN_BE_DELETED, CAN_BE_ERASED, CREATION_RESTRICTION_MODE, DOCUMENTS_AVERAGEABLE, + DOCUMENTS_COUNTABLE, DOCUMENTS_KEEP_HISTORY, DOCUMENTS_MUTABLE, DOCUMENTS_SUMMABLE, INDEX_ONLY, KEEPS_PRICING_HISTORY, KEEPS_PURCHASE_HISTORY, KEEPS_TRANSFER_HISTORY, RANGE_AVERAGEABLE, RANGE_COUNTABLE, RANGE_SUMMABLE, TRADE_MODE, TRANSFERABLE, }; @@ -1949,6 +1949,104 @@ pub(super) fn apply_doctype_aggregates( /// driver calls this; earlier generations ignore the keyword exactly as they /// ignore every doctype-level keyword they predate (their meta-schemas still /// reject it under `full_validation`). +/// Read the doctype-level `canBeErased` flag out of the raw schema. +/// +/// Runs before the core parse for the same reason as +/// [`parse_index_only_keyword`]: the core takes `schema` by value. Only the +/// generation-3 driver calls this; earlier generations have no such keyword +/// and their meta-schemas reject it under `full_validation`. +pub(super) fn parse_can_be_erased_keyword(schema: &Value) -> Result { + let schema_map_opt = schema.to_map().ok(); + + Ok(schema_map_opt + .as_ref() + .and_then(|schema_map| { + Value::inner_optional_bool_value(schema_map, CAN_BE_ERASED) + .map_err(consensus_or_protocol_value_error) + .transpose() + }) + .transpose()? + .unwrap_or(false)) +} + +/// Write the `canBeErased` flag onto the parsed document type after checking +/// the two settings it depends on. +/// +/// Erase purges the retained revisions of a document that has already been +/// deleted, so a type that keeps no history has nothing to purge and a type +/// whose documents can never be deleted can never reach the state erase acts +/// on. Both are rejected rather than silently ignored: a document type whose +/// declared behavior and reachable behavior disagree is a contract-authoring +/// error, not a default. +/// +/// Runs regardless of `full_validation`, like [`apply_index_only`]: the flag +/// governs an irreversible operation, so a stored contract must never come +/// back out of the parser with the flag set on a type that cannot support it. +pub(super) fn apply_can_be_erased( + document_type: &mut DocumentTypeV2, + can_be_erased: bool, + name: &str, +) -> Result<(), ProtocolError> { + if !can_be_erased { + return Ok(()); + } + + let structure_error = |message: String| { + ProtocolError::DataContractError(DataContractError::InvalidContractStructure(message)) + }; + + if !document_type.documents_keep_history { + return Err(structure_error(format!( + "document type \"{}\" sets `canBeErased: true` but does not keep history: erase \ + removes retained revisions, and a type without history has none", + name, + ))); + } + if !document_type.documents_can_be_deleted { + return Err(structure_error(format!( + "document type \"{}\" sets `canBeErased: true` but `canBeDeleted: false`: erase \ + applies to deleted documents only, so its documents could never be erased", + name, + ))); + } + + document_type.documents_can_be_erased = true; + Ok(()) +} + +/// Reject a keep-history document type that carries a contested index. +/// +/// A contested resource is awarded by block processing, outside state +/// transition validation, at an id derived from the winner rather than from +/// the contested values. On a keep-history type that award can land on an id +/// whose retained history already exists, which the storage guard turns into a +/// deterministic block failure on every validator. Making the combination safe +/// needs changes to the contested machinery itself, so until then the two are +/// kept apart at contract registration. +pub(super) fn reject_contested_keep_history( + document_type: &DocumentTypeV2, + name: &str, +) -> Result<(), ProtocolError> { + if !document_type.documents_keep_history { + return Ok(()); + } + if let Some((index_name, _)) = document_type + .indices + .iter() + .find(|(_, index)| index.contested_index.is_some()) + { + return Err(ProtocolError::DataContractError( + DataContractError::InvalidContractStructure(format!( + "document type \"{}\" sets `documentsKeepHistory: true` and declares the \ + contested index \"{}\": a contested resource is awarded outside transition \ + validation and cannot be combined with retained history", + name, index_name, + )), + )); + } + Ok(()) +} + pub(super) fn parse_index_only_keyword(schema: &Value) -> Result { let schema_map_opt = schema.to_map().ok(); diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index 92002b8c6ad..c91180e4f47 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -15,8 +15,7 @@ //! index-key length ceilings, and the constants they are derived from. use crate::data_contract::config::DataContractConfig; -use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; -use crate::data_contract::document_type::class_methods::consensus_or_protocol_data_contract_error; + // Only the ranked key-length rule below names `Index`, and it is validation-only. #[cfg(feature = "validation")] use crate::data_contract::document_type::index::Index; @@ -25,7 +24,7 @@ use crate::data_contract::document_type::index::IndexGrammarAdmissions; use crate::data_contract::document_type::property::DocumentPropertyType; use crate::data_contract::document_type::v2::DocumentTypeV2; use crate::data_contract::document_type::DocumentType; -use crate::data_contract::errors::DataContractError; + use crate::data_contract::{TokenConfiguration, TokenContractPosition}; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; @@ -227,9 +226,13 @@ const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = /// /// This parser is only reachable from protocol version 14+ (via /// CONTRACT_VERSIONS_V6). -/// Full validation rejects keep-history document types that allow deletion. -/// Stored contracts bypass this check so legacy contradictory schemas remain -/// readable and can be repaired by setting `canBeDeleted: false` on update. +/// +/// Generation 3 admits `documentsKeepHistory: true` together with +/// `canBeDeleted: true`: a delete on such a type removes the document from +/// ordinary reads while its retained revisions stay readable. It also admits +/// the `canBeErased` keyword, which additionally allows a deleted document's +/// revisions to be purged, and it refuses a keep-history type that carries a +/// contested index. #[allow(clippy::too_many_arguments)] fn try_from_schema_generation_3( data_contract_id: Identifier, @@ -248,6 +251,7 @@ fn try_from_schema_generation_3( // consumes `schema`. let aggregates = common::parse_doctype_aggregate_keywords(&schema, name)?; let index_only = common::parse_index_only_keyword(&schema)?; + let can_be_erased = common::parse_can_be_erased_keyword(&schema)?; let v1 = common::parse_document_type_core( data_contract_id, @@ -309,21 +313,11 @@ fn try_from_schema_generation_3( // aggregate flags (they describe the primary-key tree, which an // indexOnly type does not have), so it has to see them already applied. common::apply_index_only(&mut v2, index_only, name)?; - - // The flags are read from the parsed result (not the raw schema) so - // the check sees `canBeDeleted` resolved against the contract config - // default (`true` when the key is omitted). - if full_validation && v2.documents_keep_history() && v2.documents_can_be_deleted() { - return Err(consensus_or_protocol_data_contract_error( - DataContractError::InvalidContractStructure(format!( - "document type \"{}\" sets both `documentsKeepHistory: true` and \ - `canBeDeleted: true`, but the storage layer unconditionally refuses to \ - delete a document whose type keeps history. Set `canBeDeleted` to false or \ - disable `documentsKeepHistory`.", - name, - )), - )); - } + // Reads `canBeDeleted` off the parsed result rather than the raw schema, so + // it sees the value resolved against the contract config default (`true` + // when the key is omitted). + common::apply_can_be_erased(&mut v2, can_be_erased, name)?; + common::reject_contested_keep_history(&v2, name)?; Ok(v2) } diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs index fb9bf77de5b..6cff95b8b33 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs @@ -170,6 +170,28 @@ impl DocumentTypeRef<'_> { ); } + // Erasability is immutable in both directions. Widening it would give + // an operation over already-stored revisions to a type registered + // without it; narrowing it after a first erase chunk has irreversibly + // removed revisions would strand a partially erased, invisible + // document forever. A document type parsed by an earlier grammar has + // no such keyword and reports false on both sides, so this check is + // inert before protocol version 14. + if new_document_type.documents_can_be_erased() != self.documents_can_be_erased() { + return SimpleConsensusValidationResult::new_with_error( + DocumentTypeUpdateError::new( + self.data_contract_id(), + self.name(), + format!( + "document type can not change whether its documents can be erased: changing from {} to {}", + self.documents_can_be_erased(), + new_document_type.documents_can_be_erased() + ), + ) + .into(), + ); + } + if new_document_type.documents_keep_history() != self.documents_keep_history() { return SimpleConsensusValidationResult::new_with_error( DocumentTypeUpdateError::new( diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs index 9f8b980f842..d90ad99d01b 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs @@ -23,7 +23,9 @@ use crate::consensus::basic::data_contract::{ DataContractInvalidIndexDefinitionUpdateError, DataContractInvalidRequiredFieldsUpdateError, }; -use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, +}; use crate::data_contract::document_type::DocumentTypeRef; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; @@ -39,14 +41,20 @@ impl DocumentTypeRef<'_> { new_contract_version: u32, platform_version: &PlatformVersion, ) -> Result { - // Legacy keep-history types advertised deletes that Drive never allowed. - // Permit only true -> false for that flag while keeping history enabled. - // Every other config and schema check still runs, and v0 stays immutable. + // A keep-history type may withdraw deletion, and only in that + // direction: turning delete back on would hand an operation over + // already-stored documents to a type registered without it. The + // narrowing is refused while the type is erasable, because erase + // applies to deleted documents only and erasability is itself + // immutable, so such a type could never reach a state erase acts on + // again. Every other config and schema check still runs, and v0 stays + // immutable. let options = UpdateValidationOptions { allow_history_delete_repair: self.documents_keep_history() && new_document_type.documents_keep_history() && self.documents_can_be_deleted() - && !new_document_type.documents_can_be_deleted(), + && !new_document_type.documents_can_be_deleted() + && !self.documents_can_be_erased(), }; let result = self.validate_config_with_options(new_document_type, &options); diff --git a/packages/rs-dpp/src/data_contract/document_type/mod.rs b/packages/rs-dpp/src/data_contract/document_type/mod.rs index 3f9ca299a6c..a40213c2ddd 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -126,6 +126,12 @@ pub(crate) mod property_names { /// 14). See `apply_index_only` in `try_from_schema::common` for the /// structural constraints the flag imposes. pub const INDEX_ONLY: &str = "indexOnly"; + /// Doctype-level flag declaring that a deleted document of this type may + /// have its retained revisions purged by an erase transition. Requires + /// `documentsKeepHistory: true` and `canBeDeleted: true`, defaults to + /// false, and is immutable across contract updates. Meta-schema v3+ + /// (protocol version 14). + pub const CAN_BE_ERASED: &str = "canBeErased"; } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs index 3964010b1d9..5b3e7f0bff7 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs @@ -230,6 +230,10 @@ impl DocumentTypeV2Getters for DocumentTypeV2 { fn index_only(&self) -> bool { self.index_only } + + fn documents_can_be_erased(&self) -> bool { + self.documents_can_be_erased + } } impl DocumentTypeV2Setters for DocumentTypeV2 { diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs index 07242298e31..af983d7ab88 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs @@ -109,6 +109,15 @@ pub struct DocumentTypeV2 { /// indexed, `$ownerId` recoverable from at least one index, immutable / /// non-transferable / no history, and per-index terminal typing. pub(in crate::data_contract) index_only: bool, + /// When true, a deleted document of this type may have its retained + /// revisions purged by an erase transition. Requires + /// [`Self::documents_keep_history`] and [`Self::documents_can_be_deleted`] + /// — erase applies to deleted documents only, so an erase-only type would + /// have nothing to act on. The parser enforces the pairing at contract + /// registration and the update validator keeps the flag immutable, because + /// narrowing it after a first chunk has irreversibly removed revisions + /// would strand a partially erased document forever. + pub(in crate::data_contract) documents_can_be_erased: bool, } impl DocumentTypeBasicMethods for DocumentTypeV2 {} @@ -177,6 +186,7 @@ impl From for DocumentTypeV2 { documents_summable: None, range_summable: false, index_only: false, + documents_can_be_erased: false, } } } @@ -217,6 +227,7 @@ impl From for DocumentTypeV2 { documents_summable: None, range_summable: false, index_only: false, + documents_can_be_erased: false, } } } From 0d705963f3eed3b76a176c7e6108cc0d0b6987ff Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 10 Sep 2026 22:59:54 +0700 Subject: [PATCH 03/48] feat(dpp)!: add the document erase transition kind DocumentTransition::Erase is appended after IndexOnlyDelete so every existing variant keeps its bincode discriminant, and carries only the base transition: which chunk it removes, and whether it starts or continues an erasure, are read from the document's committed lifecycle rather than signed by the submitter. Its JSON action is "erase" and it maps to a new DocumentTransitionActionType. The kind is gated on document_erase_state_transition, which is None before protocol version 14, so a node running an older protocol version refuses it with UnsupportedVersionError and agrees with software that cannot decode it at all. A result proof for an erase shows the document absent by id, which it already was before the transition ran, so the proof classifier reports the outcome as affected state rather than proved execution. A subscription filter never matches an erase: it carries no document values and acts on a document no document query can see. BREAKING CHANGE: DocumentTransition and DocumentTransitionActionType each gain a variant, so exhaustive matches over them must be extended. Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/state_transition/mod.rs | 1 + .../from_document.rs | 52 ++++++++++ .../document_erase_transition/mod.rs | 97 +++++++++++++++++++ .../v0/from_document.rs | 30 ++++++ .../document_erase_transition/v0/mod.rs | 32 ++++++ .../v0/v0_methods.rs | 17 ++++ .../document_erase_transition/v0_methods.rs | 23 +++++ .../batched_transition/document_transition.rs | 32 +++++- .../document_transition_action_type.rs | 3 + .../batched_transition/mod.rs | 2 + .../document/batch_transition/methods/mod.rs | 56 +++++++++++ .../batch_transition/methods/v0/mod.rs | 19 ++++ .../document/batch_transition/mod.rs | 3 +- .../batch_transition/v0/v0_methods.rs | 51 +++++++++- .../batch_transition/v1/v0_methods.rs | 51 +++++++++- .../validate_basic_structure/v0/mod.rs | 39 +++++++- packages/rs-drive/src/query/filter.rs | 4 + .../v0/mod.rs | 33 ++++++- 18 files changed, 534 insertions(+), 11 deletions(-) create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0_methods.rs diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 8ffac4cf8bf..cff847f8ecf 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -990,6 +990,7 @@ impl StateTransition { BatchedTransitionRef::Document(DocumentTransition::IndexOnlyDelete(_)) => { "IndexOnlyDelete" } + BatchedTransitionRef::Document(DocumentTransition::Erase(_)) => "Erase", BatchedTransitionRef::Token(TokenTransition::Transfer(_)) => { "TokenTransfer" } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs new file mode 100644 index 00000000000..6f2926d9211 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs @@ -0,0 +1,52 @@ +use crate::data_contract::document_type::DocumentTypeRef; +use crate::document::Document; +use crate::prelude::IdentityNonce; +use crate::ProtocolError; +use platform_version::version::{FeatureVersion, PlatformVersion}; + +use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; +use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; +use crate::tokens::token_payment_info::TokenPaymentInfo; + +impl DocumentEraseTransition { + #[allow(clippy::too_many_arguments)] + pub fn from_document( + document: Document, + document_type: DocumentTypeRef, + token_payment_info: Option, + identity_contract_nonce: IdentityNonce, + platform_version: &PlatformVersion, + feature_version: Option, + base_feature_version: Option, + ) -> Result { + // `None` bounds mean the kind does not exist at this platform version + // (it joined the wire at protocol version 14) — constructing one there + // could only ever produce a transition the network refuses. + let bounds = platform_version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .as_ref() + .ok_or_else(|| { + ProtocolError::Generic( + "erase transitions do not exist at this platform version".to_string(), + ) + })?; + match feature_version.unwrap_or(bounds.bounds.default_current_version) { + 0 => Ok(DocumentEraseTransitionV0::from_document( + document, + document_type, + token_payment_info, + identity_contract_nonce, + platform_version, + base_feature_version, + )? + .into()), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentEraseTransition::from_document".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs new file mode 100644 index 00000000000..e1b0626b41e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs @@ -0,0 +1,97 @@ +mod from_document; +pub mod v0; +pub mod v0_methods; + +use bincode::{Decode, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::*; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum DocumentEraseTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(DocumentEraseTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentEraseTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentEraseTransition {} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use platform_value::{platform_value, Identifier}; + use serde_json::json; + + /// Non-default values per field so the wire-shape assertion catches any + /// silent zero-out / flip on round-trip. + pub(crate) fn fixture() -> DocumentEraseTransition { + DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([0xe3; 32]), + identity_contract_nonce: 11, + document_type_name: "note".to_string(), + data_contract_id: Identifier::new([0xf4; 32]), + }), + }) + } + + #[test] + fn json_round_trip_with_full_wire_shape() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + // Doubly-tagged externally enum: outer `V0` for + // `DocumentEraseTransition`, inner `V0` for the flattened + // `base: DocumentBaseTransition`. + assert_eq!( + json, + json!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xe3; 32]), + "$identityContractNonce": 11, + "$type": "note", + "$dataContractId": Identifier::new([0xf4; 32]), + }) + ); + let recovered = DocumentEraseTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip_with_full_wire_shape() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + assert_eq!( + value, + platform_value!({ + "$formatVersion": "0", + "$baseFormatVersion": "0", + "$id": Identifier::new([0xe3; 32]), + "$identityContractNonce": 11u64, + "$type": "note", + "$dataContractId": Identifier::new([0xf4; 32]), + }) + ); + let recovered = DocumentEraseTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs new file mode 100644 index 00000000000..b581b61fa97 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs @@ -0,0 +1,30 @@ +use crate::data_contract::document_type::DocumentTypeRef; +use crate::document::Document; +use crate::prelude::IdentityNonce; +use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; +use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; +use crate::tokens::token_payment_info::TokenPaymentInfo; +use crate::ProtocolError; +use platform_version::version::{FeatureVersion, PlatformVersion}; + +impl DocumentEraseTransitionV0 { + pub(crate) fn from_document( + document: Document, + document_type: DocumentTypeRef, + token_payment_info: Option, + identity_contract_nonce: IdentityNonce, + platform_version: &PlatformVersion, + base_feature_version: Option, + ) -> Result { + Ok(DocumentEraseTransitionV0 { + base: DocumentBaseTransition::from_document( + &document, + document_type, + token_payment_info, + identity_contract_nonce, + platform_version, + base_feature_version, + )?, + }) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs new file mode 100644 index 00000000000..b65e0986c97 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs @@ -0,0 +1,32 @@ +mod from_document; +pub mod v0_methods; + +use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + +use bincode::{Decode, Encode}; +use derive_more::Display; + +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +/// Purges the retained revisions of a document that has already been deleted. +/// +/// The transition carries nothing beyond the base: which chunk it removes, and +/// whether it starts or continues an erasure, are decided from the document's +/// committed lifecycle rather than from anything the submitter signs. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] +#[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[display("Base: {}", "base")] +pub struct DocumentEraseTransitionV0 { + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: DocumentBaseTransition, +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..6265e81d824 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/v0_methods.rs @@ -0,0 +1,17 @@ +use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; +use crate::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + +impl DocumentBaseTransitionAccessors for DocumentEraseTransitionV0 { + fn base(&self) -> &DocumentBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut DocumentBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: DocumentBaseTransition) { + self.base = base + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0_methods.rs new file mode 100644 index 00000000000..df32a5641fc --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0_methods.rs @@ -0,0 +1,23 @@ +use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; +use crate::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + +impl DocumentBaseTransitionAccessors for DocumentEraseTransition { + fn base(&self) -> &DocumentBaseTransition { + match self { + DocumentEraseTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut DocumentBaseTransition { + match self { + DocumentEraseTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: DocumentBaseTransition) { + match self { + DocumentEraseTransition::V0(v0) => v0.base = base, + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index 1b9c13ddea9..410ae076dbd 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use bincode::{Encode, Decode, DecodeUntrusted}; use crate::prelude::{IdentityNonce, Revision}; use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; -use crate::state_transition::batch_transition::batched_transition::{DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; +use crate::state_transition::batch_transition::batched_transition::{DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; @@ -28,7 +28,7 @@ use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionRes // `document_type_name` as `$type` in JSON (the long-standing DPP // document-type field). The variant names (`create`, `replace`, // `delete`, `transfer`, `updatePrice`, `purchase`, - // `indexOnlyDelete`) read naturally as + // `indexOnlyDelete`, `erase`) read naturally as // actions, matching the existing `PROPERTY_ACTION = "$action"` // constant on the parent batch transition. serde(tag = "$action", rename_all = "camelCase") @@ -57,6 +57,13 @@ pub enum DocumentTransition { /// PV14+ (see the wire gate in `validate_base_structure_v0`). #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] IndexOnlyDelete(DocumentIndexOnlyDeleteTransition), + + /// The erase kind, which purges the retained revisions of a deleted + /// keep-history document — appended at the end so every existing variant + /// keeps its bincode discriminant. Only exists at protocol version 14 and + /// later (see the wire gate in `validate_base_structure_v0`). + #[display("EraseDocumentTransition({})", "_0")] + Erase(DocumentEraseTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -74,7 +81,7 @@ impl crate::serialization::ValueConvertible for DocumentTransition {} pub(crate) mod json_convertible_tests { use super::*; use crate::state_transition::batch_transition::batched_transition::{ - document_create_transition, document_delete_transition, + document_create_transition, document_delete_transition, document_erase_transition, document_index_only_delete_transition, document_purchase_transition, document_replace_transition, document_transfer_transition, document_update_price_transition, @@ -183,6 +190,14 @@ pub(crate) mod json_convertible_tests { "indexOnlyDelete", ); } + + #[test] + fn umbrella_erase() { + assert_umbrella_round_trip( + DocumentTransition::Erase(document_erase_transition::json_convertible_tests::fixture()), + "erase", + ); + } } impl BatchTransitionResolversV0 for DocumentTransition { @@ -325,6 +340,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base(), DocumentTransition::Purchase(t) => t.base(), DocumentTransition::IndexOnlyDelete(t) => t.base(), + DocumentTransition::Erase(t) => t.base(), } } @@ -337,6 +353,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => t.data().get(path), + DocumentTransition::Erase(_) => None, } } @@ -357,6 +374,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -373,6 +391,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data()), + DocumentTransition::Erase(_) => None, } } @@ -391,6 +410,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => Some(t.revision()), DocumentTransition::Purchase(t) => Some(t.revision()), DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -403,6 +423,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base().identity_contract_nonce(), DocumentTransition::Purchase(t) => t.base().identity_contract_nonce(), DocumentTransition::IndexOnlyDelete(t) => t.base().identity_contract_nonce(), + DocumentTransition::Erase(t) => t.base().identity_contract_nonce(), } } @@ -426,6 +447,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.data_mut().insert(property_name, value); } + DocumentTransition::Erase(_) => {} } } @@ -442,6 +464,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base_mut(), DocumentTransition::Purchase(t) => t.base_mut(), DocumentTransition::IndexOnlyDelete(t) => t.base_mut(), + DocumentTransition::Erase(t) => t.base_mut(), } } @@ -454,6 +477,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data_mut()), + DocumentTransition::Erase(_) => None, } } @@ -466,6 +490,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(ref mut t) => t.set_revision(revision), DocumentTransition::Purchase(ref mut t) => t.set_revision(revision), DocumentTransition::IndexOnlyDelete(_) => {} + DocumentTransition::Erase(_) => {} } } @@ -480,6 +505,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.base_mut().set_identity_contract_nonce(nonce) } + DocumentTransition::Erase(t) => t.base_mut().set_identity_contract_nonce(nonce), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs index 02b5ff95f97..b2a946706ee 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs @@ -12,6 +12,7 @@ pub enum DocumentTransitionActionType { UpdatePrice, IgnoreWhileBumpingRevision, IndexOnlyDelete, + Erase, } pub trait DocumentTransitionActionTypeGetter { @@ -28,6 +29,7 @@ impl DocumentTransitionActionTypeGetter for DocumentTransition { DocumentTransition::UpdatePrice(_) => DocumentTransitionActionType::UpdatePrice, DocumentTransition::Purchase(_) => DocumentTransitionActionType::Purchase, DocumentTransition::IndexOnlyDelete(_) => DocumentTransitionActionType::IndexOnlyDelete, + DocumentTransition::Erase(_) => DocumentTransitionActionType::Erase, } } } @@ -46,6 +48,7 @@ impl TryFrom<&str> for DocumentTransitionActionType { "indexOnlyDelete" | "index_only_delete" => { Ok(DocumentTransitionActionType::IndexOnlyDelete) } + "erase" => Ok(DocumentTransitionActionType::Erase), action_type => Err(ProtocolError::Generic(format!( "unknown action type {action_type}" ))), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index a71a4271563..d2a0fa480d1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub mod document_base_transition; pub mod document_create_transition; pub mod document_delete_transition; +pub mod document_erase_transition; pub mod document_index_only_delete_transition; pub mod document_purchase_transition; pub mod document_replace_transition; @@ -36,6 +37,7 @@ use crate::state_transition::batch_transition::batched_transition::token_transit use derive_more::Display; pub use document_create_transition::DocumentCreateTransition; pub use document_delete_transition::DocumentDeleteTransition; +pub use document_erase_transition::DocumentEraseTransition; pub use document_index_only_delete_transition::DocumentIndexOnlyDeleteTransition; pub use document_purchase_transition::DocumentPurchaseTransition; pub use document_replace_transition::DocumentReplaceTransition; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index ad8d520cdc5..5e9acff9b05 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -205,6 +205,62 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { } #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 0 => Ok( + BatchTransitionV0::new_document_erase_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await?, + ), + 1 => Ok( + BatchTransitionV1::new_document_erase_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await?, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_document_erase_transition_from_document" + .to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs index 01d042c7a8a..60ad2b2a32c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs @@ -71,6 +71,25 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 options: Option, ) -> Result; + /// Builds a signed erase of one already deleted keep-history document. + /// + /// The transition carries no token payment: the deletion cost was charged + /// when the document was deleted. Whether this erase starts or continues an + /// erasure, and how many revisions it removes, are read from committed + /// state rather than signed here. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index b3abe347bbb..d44ee39e535 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -14,7 +14,8 @@ use crate::{identity::SecurityLevel, state_transition::StateTransitionFieldTypes pub use self::batched_transition::{ document_base_transition, document_create_transition, document_create_transition::DocumentCreateTransition, document_delete_transition, - document_delete_transition::DocumentDeleteTransition, document_index_only_delete_transition, + document_delete_transition::DocumentDeleteTransition, document_erase_transition, + document_erase_transition::DocumentEraseTransition, document_index_only_delete_transition, document_index_only_delete_transition::DocumentIndexOnlyDeleteTransition, document_replace_transition, document_replace_transition::DocumentReplaceTransition, token_base_transition, token_burn_transition, token_burn_transition::TokenBurnTransition, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs index 73f5b7bb738..becdc5a843c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs @@ -23,7 +23,8 @@ use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransi use crate::state_transition::batch_transition::BatchTransitionV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, + DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -182,6 +183,54 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV0 { } #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + // Erase never carries a token payment: it acts on a document whose + // deletion cost was already charged. + let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( + document, + document_type, + None, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )? + .into(); + let documents_batch_transition: BatchTransition = BatchTransitionV0 { + owner_id, + transitions: vec![erase_transition], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs index 0c7247ce76c..73f716117ff 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs @@ -28,7 +28,8 @@ use std::slice::Iter; use crate::state_transition::batch_transition::BatchTransitionV1; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, + DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -257,6 +258,54 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV1 { } #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + // Erase never carries a token payment: it acts on a document whose + // deletion cost was already charged. + let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( + document, + document_type, + None, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )? + .into(); + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Document(erase_transition)], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 83887155a60..b9880c6620f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -4,7 +4,9 @@ use crate::consensus::basic::document::{ }; use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; use crate::consensus::basic::BasicError; -use crate::state_transition::batch_transition::batched_transition::DocumentIndexOnlyDeleteTransition; +use crate::state_transition::batch_transition::batched_transition::{ + DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, +}; use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; @@ -141,6 +143,41 @@ impl BatchTransition { Some(_) => {} } } + + // The erase kind joined the wire at protocol version 14. Old + // software cannot decode it at all, so no historical block can + // contain one — this check exists so that new software agrees + // with old software while a pre-14 protocol version is still + // active. + if let DocumentTransition::Erase(erase) = transition { + let feature_version = match erase { + DocumentEraseTransition::V0(_) => 0, + }; + match &platform_version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + { + None => { + // The kind does not exist at this protocol + // version; the empty supported range (min 1, max 0) + // states exactly that. + result.add_error(BasicError::UnsupportedVersionError( + UnsupportedVersionError::new(feature_version, 1, 0), + )); + } + Some(bounds) if !bounds.bounds.check_version(feature_version) => { + result.add_error(BasicError::UnsupportedVersionError( + UnsupportedVersionError::new( + feature_version, + bounds.bounds.min_version, + bounds.bounds.max_version, + ), + )); + } + Some(_) => {} + } + } } // Make sure we don't have duplicate transitions diff --git a/packages/rs-drive/src/query/filter.rs b/packages/rs-drive/src/query/filter.rs index 5913805cf02..8fc9bd6fa8c 100644 --- a/packages/rs-drive/src/query/filter.rs +++ b/packages/rs-drive/src/query/filter.rs @@ -324,6 +324,10 @@ impl DriveDocumentQueryFilter<'_> { TransitionCheckResult::Fail } } + // An erase carries no document values and acts on a document that + // is already invisible to every document query, so no + // content-based subscription filter can describe it. + DocumentTransition::Erase(_) => TransitionCheckResult::Fail, DocumentTransition::IndexOnlyDelete(index_only_delete) => { if let DocumentActionMatchClauses::Delete { original_document_clauses, diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 47a467910e8..1888979041e 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -503,6 +503,24 @@ impl Drive { .to_string(), ))) } + DocumentTransition::Erase(erase_transition) => { + // An erase acts on a document that was already + // deleted, so the by-id proof shows it absent + // both before and after. The absence is the + // state the transition affected, not evidence + // that this erase ran; the classifier below + // reports it as such. + if document.is_some() { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained a current document for erased id {}", erase_transition.base().id())))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + erase_transition.base().id(), + None, + )])), + )) + } } } BatchedTransitionRef::Token(token_transition) => { @@ -2339,10 +2357,17 @@ impl Drive { data_contract_id ))), )?; - !contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? - .index_only() + // EXCEPT an erase: the document it acts on was already + // deleted, so the by-id proof shows the same absence + // whether or not this erase ran. + if matches!(document_transition, DocumentTransition::Erase(_)) { + false + } else { + !contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? + .index_only() + } } Some(BatchedTransitionRef::Token(token_transition)) => { let data_contract_id = token_transition.data_contract_id(); From d145eec2f73e79933665f5c605a00c7cc814f232 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 10 Sep 2026 23:34:01 +0700 Subject: [PATCH 04/48] feat(drive): give keep-history documents a delete and an erase Deleting a keep-history document now removes its current pointer and the index references that lead to it, leaves every retained revision where it is, and writes a lifecycle record naming the block time it was deleted at. The record lives in a per-type tree at a reserved key next to the history tree, is created on demand so that a type whose documents are never deleted never pays for one, and carries the deleter's flags so its bytes come back to them. Non-keep-history deletes are delegated to the previous implementation unchanged. Erasing removes a bounded chunk of an already deleted document's revisions, newest first, so a partial erasure leaves the oldest content and a contiguous sequence behind. The enumeration asks for one revision more than a chunk may remove, so the operation knows before emitting anything whether it is the last: a terminal chunk also removes the record and the now empty history subtree, which GroveDB accepts only because the revision deletes are already in the same batch, while a non-terminal first chunk instead overwrites the record with the erasure it authorizes. The two never coincide, so one batch never carries two operations on the record's key. A document whose revisions are still retained cannot be created again: the storage writer refuses an insert whose history subtree already exists, which covers writers that never pass through transition validation. A dry run skips the probe and pays for it as a fixed cost so estimation and execution stay aligned. History v1 gains the deleted and erasing states, derived identically in the fetch and the verifier from the same authenticated record. Co-Authored-By: Claude Opus 5 (1M context) --- .../v0/tests/batched_group_drain.rs | 1 + .../contract/update/update_keywords/v0/mod.rs | 2 + .../delete_document_for_contract/v0/mod.rs | 4 + .../mod.rs | 5 + .../v0/mod.rs | 5 + .../delete_document_for_contract_id/v0/mod.rs | 4 + .../mod.rs | 8 +- .../v0/mod.rs | 9 +- .../mod.rs | 40 +- .../v0/mod.rs | 7 + .../v1/mod.rs | 319 +++++++++++++++ .../mod.rs | 5 + .../v0/mod.rs | 5 + .../mod.rs | 76 ++++ .../v0/mod.rs | 381 ++++++++++++++++++ .../mod.rs | 9 +- .../v1/mod.rs | 83 ++++ .../rs-drive/src/drive/document/delete/mod.rs | 81 +++- .../mod.rs | 12 +- .../v1/mod.rs | 109 +++++ ...estimation_costs_for_document_lifecycle.rs | 63 +++ ...add_estimation_costs_for_erase_document.rs | 81 ++++ .../drive/document/estimation_costs/mod.rs | 4 + .../add_document_to_primary_storage/v1/mod.rs | 16 +- .../src/drive/document/lifecycle/fetch/mod.rs | 87 ++++ .../drive/document/lifecycle/fetch/v0/mod.rs | 160 ++++++++ .../src/drive/document/lifecycle/mod.rs | 215 ++++++++++ packages/rs-drive/src/drive/document/mod.rs | 3 + packages/rs-drive/src/drive/document/paths.rs | 15 + .../src/query/document_history_drive_query.rs | 84 +++- packages/rs-drive/src/query/mod.rs | 3 +- .../document/document_delete_transition.rs | 1 + .../src/util/batch/drive_op_batch/document.rs | 53 +++ .../tests/drive_storage_ops_coverage.rs | 1 + 34 files changed, 1914 insertions(+), 37 deletions(-) create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs create mode 100644 packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs create mode 100644 packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs create mode 100644 packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/document/lifecycle/mod.rs diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs index 531272d06aa..f3964a55455 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs @@ -176,6 +176,7 @@ fn delete_op<'a>( document_id: Identifier, ) -> DriveOperation<'a> { DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { + deleter_id: None, document_id, contract_info: DataContractInfo::BorrowedDataContract(contract), document_type_info: DocumentTypeInfo::DocumentTypeNameAsStr(doctype), diff --git a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs index 08f3168acd6..56e0434db41 100644 --- a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs @@ -146,6 +146,8 @@ impl Drive { *doc_id, &contract, document_type, + block_info, + None, None, estimated_costs_only_with_layer_info, block_info.time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs index b82d4b25c51..a9f52838766 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs @@ -37,6 +37,10 @@ impl Drive { document_id, contract, document_type_name, + &block_info, + // These fee-applying wrappers carry no signer, so a lifecycle record + // they write belongs to no identity and refunds nobody. + None, estimated_costs_only_with_layer_info, block_info.time_ms, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs index e1a451d52a1..0192d6284ba 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs @@ -4,6 +4,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -34,6 +35,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, estimated_costs_only_with_layer_info: Option< HashMap, >, @@ -53,6 +56,8 @@ impl Drive { document_id, contract, document_type_name, + block_info, + deleter_id, estimated_costs_only_with_layer_info, block_time_ms, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs index 2a65843e041..36f03c47b63 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs @@ -1,6 +1,7 @@ use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -18,6 +19,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, mut estimated_costs_only_with_layer_info: Option< HashMap, >, @@ -37,6 +40,8 @@ impl Drive { document_id, contract, document_type_name, + block_info, + deleter_id, None, &mut estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs index 50e415bbb06..09f4b28aec9 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs @@ -56,6 +56,10 @@ impl Drive { document_id, contract, document_type_name, + &block_info, + // These fee-applying wrappers carry no signer, so a lifecycle record + // they write belongs to no identity and refunds nobody. + None, estimated_costs_only_with_layer_info, block_info.time_ms, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs index 10d1297bdf0..9c97474dece 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs @@ -4,7 +4,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::epoch::Epoch; +use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; @@ -35,7 +35,8 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - epoch: &Epoch, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -55,7 +56,8 @@ impl Drive { document_id, contract_id, document_type_name, - epoch, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs index 1349e7e07a6..6c15900fad0 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs @@ -10,7 +10,7 @@ use crate::error::document::DocumentError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::epoch::Epoch; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::identifier::Identifier; @@ -25,7 +25,8 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - epoch: &Epoch, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -37,7 +38,7 @@ impl Drive { let mut operations = vec![]; let Some(contract_fetch_info) = self.get_contract_with_fetch_info_and_add_to_operations( contract_id.to_buffer(), - Some(epoch), + Some(&block_info.epoch), true, transaction, &mut operations, @@ -53,6 +54,8 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, 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..536a288859d 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 @@ -1,10 +1,12 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; @@ -21,6 +23,8 @@ impl Drive { /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record's bytes. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. @@ -35,6 +39,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -91,6 +97,19 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_operations_v1( + document_id, + contract, + document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -99,7 +118,7 @@ impl Drive { ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -112,6 +131,8 @@ impl Drive { /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record's bytes. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. @@ -126,6 +147,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -145,6 +168,19 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + 1 => self.force_delete_document_for_contract_operations_v1( + document_id, + contract, + document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -153,7 +189,7 @@ impl Drive { ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "force_delete_document_for_contract_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } 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 0134b878d25..31f3e01d1b7 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 @@ -3,6 +3,7 @@ use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::document_type::DocumentTypeRef; use std::collections::HashMap; @@ -43,6 +44,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, + _block_info: &BlockInfo, + _deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -61,6 +64,8 @@ impl Drive { document_id, contract, document_type, + _block_info, + _deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -77,6 +82,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, + _block_info: &BlockInfo, + _deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs new file mode 100644 index 00000000000..0a0a4c19330 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -0,0 +1,319 @@ +use grovedb::batch::KeyInfoPath; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; + +use dpp::data_contract::document_type::DocumentTypeRef; + +use std::collections::HashMap; + +use crate::drive::constants::DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE; +use crate::drive::document::lifecycle::{DocumentLifecycleRecord, DOCUMENT_LIFECYCLE_RECORD_SIZE}; +use crate::drive::document::paths::{ + contract_documents_primary_key_path, document_lifecycle_path, DOCUMENT_LIFECYCLE_TREE_KEY, +}; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use crate::util::object_size_info::DocumentInfo::{ + DocumentEstimatedAverageSize, DocumentOwnedInfo, +}; +use crate::util::storage_flags::StorageFlags; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; +use dpp::document::Document; + +use crate::drive::Drive; +use crate::util::grove_operations::QueryTarget::QueryTargetValue; +use crate::util::grove_operations::{BatchInsertTreeApplyType, QueryType}; +use crate::util::object_size_info::PathKeyElementInfo::{ + PathKeyElement, PathKeyUnknownElementSize, +}; +use crate::util::object_size_info::PathKeyInfo::PathKey; +use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + +use crate::error::drive::DriveError; + +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::identifier::Identifier; + +use dpp::version::PlatformVersion; +use grovedb::batch::key_info::KeyInfo; + +impl Drive { + /// Prepares the operations for deleting a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, + previous_batch_operations: Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if !document_type.documents_can_be_deleted() { + return Err(Error::Drive(DriveError::UpdatingReadOnlyImmutableDocument( + "this document type is not mutable and can not be deleted", + ))); + } + + self.force_delete_document_for_contract_operations_v1( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ) + } + + /// Prepares the operations for deleting a document. + /// + /// Deleting a keep-history document removes it from every ordinary read + /// without touching a single retained revision: the current pointer and the + /// index references that lead to it go, and a lifecycle record recording the + /// deletion time takes their place. The revisions stay where they are, and + /// only an erase can remove them. + /// + /// Every other document type is deleted exactly as v0 deletes it. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn force_delete_document_for_contract_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, + previous_batch_operations: Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if !document_type.documents_keep_history() { + return self.force_delete_document_for_contract_operations_v0( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ); + } + + let mut batch_operations: Vec = vec![]; + + let contract_documents_primary_key_path = contract_documents_primary_key_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + ); + + let query_type = 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, + )?; + // The read follows the current pointer into the type's history, so + // the dry run has to pay for the reference hop as well as the + // revision it lands on. `DirectQueryType` cannot express that: its + // conversion hardcodes an empty reference-size list. + QueryType::StatelessQuery { + in_tree_type: document_type.primary_key_tree_type(platform_version)?, + query_target: QueryTargetValue( + document_type.estimated_size(platform_version)? as u32 + ), + estimated_reference_sizes: vec![DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE], + } + } else { + QueryType::StatefulQuery + }; + + // Resolves the pointer, so this is the current revision's item, whose + // flags are the ones the index references were written with. + let document_element: Option = self.grove_get( + (&contract_documents_primary_key_path).into(), + document_id.as_slice(), + query_type.clone(), + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + + let document_info = if let QueryType::StatelessQuery { query_target, .. } = query_type { + DocumentEstimatedAverageSize(query_target.len()) + } else if let Some(document_element) = &document_element { + let Element::Item(data, element_flags) = document_element else { + return Err(Error::Drive(DriveError::CorruptedDocumentNotItem( + "the current pointer of a keep-history document did not resolve to an item", + ))); + }; + let document = Document::from_bytes(data.as_slice(), document_type, platform_version)?; + let storage_flags = StorageFlags::map_cow_some_element_flags_ref(element_flags)?; + DocumentOwnedInfo((document, storage_flags)) + } else { + return Err(Error::Drive(DriveError::DeletingDocumentThatDoesNotExist( + "document being deleted does not exist", + ))); + }; + + // The pointer goes; the revisions it named stay in the history tree. + self.remove_document_from_primary_storage( + document_id, + document_type, + contract_documents_primary_key_path, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + let document_and_contract_info = DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info, + owner_id: None, + }, + contract, + document_type, + }; + + self.remove_indices_for_top_index_level_for_contract_operations( + &document_and_contract_info, + &previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + // The record is the deleter's byte, refunded to them when the terminal + // erase chunk removes it. A caller with no signer writes it unflagged, + // and it then refunds nobody, exactly as the unflagged structural bytes + // of a non-deletable contract do. + let record_flags = deleter_id + .map(|id| StorageFlags::new_single_epoch(block_info.epoch.index, Some(id.to_buffer()))); + self.add_lifecycle_record_operations( + document_id, + contract, + document_type, + DocumentLifecycleRecord::deleted_at(block_info.time_ms), + record_flags.as_ref(), + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + Ok(batch_operations) + } + + /// Creates the document type's lifecycle tree if it does not exist yet and + /// writes one record into it. + /// + /// The tree is created on demand so that a document type whose documents are + /// never deleted never pays for one, and the record is flagged to the + /// deleter, who is refunded when the terminal erase chunk removes it. + #[allow(clippy::too_many_arguments)] + pub(crate) fn add_lifecycle_record_operations( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + record: DocumentLifecycleRecord, + storage_flags: Option<&StorageFlags>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + batch_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let lifecycle_path = + document_lifecycle_path(contract.id_ref().as_bytes(), document_type.name().as_str()); + let element_flags = StorageFlags::map_to_some_element_flags(storage_flags); + let flags_len = storage_flags + .map(StorageFlags::serialized_size) + .unwrap_or_default(); + + if let Some(layers) = estimated_costs_only_with_layer_info { + Self::add_estimation_costs_for_lifecycle_record( + contract, + document_type, + layers, + platform_version, + )?; + } + + let tree_apply_type = if estimated_costs_only_with_layer_info.is_some() { + BatchInsertTreeApplyType::StatelessBatchInsertTree { + in_tree_type: TreeType::NormalTree, + tree_type: TreeType::NormalTree, + flags_len, + } + } else { + BatchInsertTreeApplyType::StatefulBatchInsertTree + }; + + let mut document_type_path = crate::drive::document::paths::contract_document_type_path_vec( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + ); + let lifecycle_tree_key = vec![DOCUMENT_LIFECYCLE_TREE_KEY]; + self.batch_insert_empty_tree_if_not_exists::<0>( + PathKey((std::mem::take(&mut document_type_path), lifecycle_tree_key)), + TreeType::NormalTree, + storage_flags, + tree_apply_type, + transaction, + &mut None, + batch_operations, + &platform_version.drive, + )?; + + if estimated_costs_only_with_layer_info.is_some() { + self.batch_insert::<0>( + PathKeyUnknownElementSize(( + KeyInfoPath::from_known_owned_path(lifecycle_path), + KeyInfo::KnownKey(document_id.to_vec()), + Element::required_item_space( + DOCUMENT_LIFECYCLE_RECORD_SIZE, + flags_len, + &platform_version.drive.grove_version, + )?, + )), + batch_operations, + &platform_version.drive, + ) + } else { + self.batch_insert::<0>( + PathKeyElement(( + lifecycle_path, + document_id.to_vec(), + Element::Item(record.serialize(), element_flags), + )), + batch_operations, + &platform_version.drive, + ) + } + } +} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs index 264aaaf7a97..0fddbe97160 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs @@ -4,6 +4,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; @@ -34,6 +35,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -53,6 +56,8 @@ impl Drive { document_id, contract, document_type_name, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs index cddde01f1b0..df691f5c869 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs @@ -4,6 +4,7 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use crate::drive::Drive; @@ -25,6 +26,8 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -38,6 +41,8 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs new file mode 100644 index 00000000000..8a826b25afa --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs @@ -0,0 +1,76 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; + +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Prepares the operations for removing a bounded chunk of the retained + /// revisions of a document that has already been deleted. + /// + /// # Parameters + /// * `document_id`: The document whose revisions are being removed. + /// * `contract`: The contract that contains the document. + /// * `document_type`: The type of the document, which must keep history. + /// * `block_info`: The block this erase belongs to. + /// * `start`: Whether this is the authorized first chunk, which commits the + /// document to erasure, rather than a continuation of one already + /// committed. Derived from the document's committed lifecycle, never from + /// the transition. + /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. + /// * `transaction`: The transaction argument. + /// * `platform_version`: The platform version to select the correct function version to run. + /// + /// # Returns + /// * `Ok(Vec)` if the operation was successful. + /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. + #[allow(clippy::too_many_arguments)] + pub(crate) fn erase_document_for_contract_operations( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + start: bool, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .document + .delete + .erase_document_for_contract_operations + { + 0 => self.erase_document_for_contract_operations_v0( + document_id, + contract, + document_type, + block_info, + start, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "erase_document_for_contract_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs new file mode 100644 index 00000000000..81f488b018c --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -0,0 +1,381 @@ +use std::collections::HashMap; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::key_info::KeyInfo; +use grovedb::batch::KeyInfoPath; +use grovedb::query_result_type::QueryResultType; +use grovedb::{ + Element, EstimatedLayerInformation, MaybeTree, PathQuery, Query, SizedQuery, TransactionArg, + TreeType, +}; + +use crate::drive::document::lifecycle::{DocumentLifecycleRecord, DOCUMENT_LIFECYCLE_RECORD_SIZE}; +use crate::drive::document::paths::{document_history_path, document_lifecycle_path}; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::common::encode::encode_u64; +use crate::util::grove_operations::BatchDeleteApplyType::{ + StatefulBatchDelete, StatelessBatchDelete, +}; +use crate::util::grove_operations::{DirectQueryType, QueryTarget}; +use crate::util::object_size_info::PathKeyElementInfo::{ + PathKeyElement, PathKeyUnknownElementSize, +}; +use crate::util::storage_flags::StorageFlags; + +/// Length of a revision key: a block timestamp followed by a history sequence. +const REVISION_KEY_LENGTH: usize = 16; + +impl Drive { + /// Prepares the operations for removing a bounded chunk of the retained + /// revisions of an already deleted document. + /// + /// Revisions go newest first, so the remnant a partial erasure leaves behind + /// is the document's oldest content and the retained sequence stays + /// contiguous from one. The chunk is bounded by + /// `max_document_revisions_erased_per_transition`, and the enumeration asks + /// for one revision more than it may remove so that it knows, before + /// emitting anything, whether this chunk is the last one. + /// + /// A terminal chunk also removes the lifecycle record and the now empty + /// history subtree; GroveDB establishes that the subtree is empty from the + /// deletes already in this batch and refuses the removal otherwise. A + /// non-terminal first chunk instead overwrites the record with the erasure + /// it authorizes, which is what lets any identity finish the work later. + /// The two never happen together, so one batch never carries two operations + /// on the record's key. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn erase_document_for_contract_operations_v0( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + start: bool, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if !document_type.documents_keep_history() { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "only a document type that keeps history has revisions to erase", + ))); + } + let chunk = platform_version + .system_limits + .max_document_revisions_erased_per_transition + .ok_or(Error::Drive(DriveError::NotSupported( + "erasing retained revisions is not available at this protocol version", + )))?; + + let history_path = document_history_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + document_id.as_slice(), + ); + let lifecycle_path = + document_lifecycle_path(contract.id_ref().as_bytes(), document_type.name().as_str()); + // Worst-case sizing: a chunk may hold the type's largest documents, and + // the admission estimate has to cover that whatever this document + // actually stores. + let revision_size = document_type.max_size(platform_version)? as u32; + + let mut batch_operations: Vec = vec![]; + + if let Some(layers) = estimated_costs_only_with_layer_info { + Self::add_estimation_costs_for_erase_document( + document_id, + contract, + document_type, + layers, + platform_version, + )?; + return self.estimated_erase_document_operations( + document_id, + &history_path, + &lifecycle_path, + chunk, + revision_size, + block_info, + batch_operations, + transaction, + platform_version, + ); + } + + let removable = self.enumerate_newest_revision_keys( + &history_path, + chunk, + &mut batch_operations, + transaction, + platform_version, + )?; + if removable.is_empty() { + return Err(Error::Drive(DriveError::CorruptedDriveState( + "a document with a lifecycle record retains no revisions".to_string(), + ))); + } + let terminal = removable.len() <= chunk as usize; + let to_remove = if terminal { + removable.as_slice() + } else { + &removable[..chunk as usize] + }; + + // A leaf delete does not consult the batch it joins, so each one is + // generated against an empty view and appended: building the whole + // running batch once per revision would be quadratic in the chunk size. + let mut single_operation = Vec::with_capacity(2); + for key in to_remove { + single_operation.clear(); + self.batch_delete( + history_path.as_slice().into(), + key, + StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut single_operation, + &platform_version.drive, + )?; + batch_operations.append(&mut single_operation); + } + + if terminal { + self.batch_delete( + lifecycle_path.as_slice().into(), + document_id.as_slice(), + StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + }, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + let mut history_root = history_path; + history_root.pop(); + // Every revision has already been deleted into this batch, which is + // how GroveDB establishes that the subtree is empty. If any survived + // it refuses the removal rather than orphaning their storage. + self.batch_delete( + history_root.as_slice().into(), + document_id.as_slice(), + StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::Tree( + TreeType::ProvableCountTree, + )), + }, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + } else if start { + let (record, flags) = self.fetch_lifecycle_record_with_flags( + document_id, + &lifecycle_path, + &mut batch_operations, + transaction, + platform_version, + )?; + let (time_ms, revision) = Self::decode_revision_key(&removable[0])?; + // Equal in size to the record it replaces, and carrying the same + // flags, so the deleter stays the beneficiary of its bytes. + self.batch_insert::<0>( + PathKeyElement(( + lifecycle_path, + document_id.to_vec(), + Element::Item( + record + .starting_erase_at(block_info.time_ms, time_ms, revision) + .serialize(), + flags, + ), + )), + &mut batch_operations, + &platform_version.drive, + )?; + } + + Ok(batch_operations) + } + + /// Reads the revision keys of one document newest first, one more than a + /// chunk may remove so the caller can tell a terminal chunk from a partial + /// one before it emits anything. + fn enumerate_newest_revision_keys( + &self, + history_path: &[Vec], + chunk: u16, + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + let mut query = Query::new_with_direction(false); + // Exactly the revision key space: the current pointer lives in the + // primary-key tree and is not in this subtree at all. + query.insert_range_from(encode_u64(0)..); + let path_query = PathQuery::new( + history_path.to_vec(), + SizedQuery::new(query, Some(chunk.saturating_add(1)), None), + ); + let (results, _) = self.grove_get_raw_path_query( + &path_query, + transaction, + QueryResultType::QueryKeyElementPairResultType, + batch_operations, + &platform_version.drive, + )?; + Ok(results + .to_key_elements() + .into_iter() + .map(|(key, _)| key) + .collect()) + } + + /// Reads one lifecycle record together with the flags naming the identity + /// its bytes were charged to. + fn fetch_lifecycle_record_with_flags( + &self, + document_id: Identifier, + lifecycle_path: &[Vec], + batch_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(DocumentLifecycleRecord, Option>), Error> { + let element = self + .grove_get_raw( + lifecycle_path.into(), + document_id.as_slice(), + DirectQueryType::StatefulDirectQuery, + transaction, + batch_operations, + &platform_version.drive, + )? + .ok_or(Error::Drive(DriveError::CorruptedDriveState( + "a document being erased has no lifecycle record".to_string(), + )))?; + let Element::Item(bytes, flags) = element else { + return Err(Error::Drive(DriveError::CorruptedElementType( + "a lifecycle record is not an item", + ))); + }; + Ok((DocumentLifecycleRecord::deserialize(&bytes)?, flags)) + } + + /// Splits a revision key into the block time and history sequence it + /// carries. + fn decode_revision_key(key: &[u8]) -> Result<(u64, u64), Error> { + if key.len() != REVISION_KEY_LENGTH { + return Err(Error::Drive(DriveError::CorruptedDriveState( + "a retained revision key does not carry a time and a sequence".to_string(), + ))); + } + let decode = |bytes: &[u8]| -> u64 { + let mut buffer = [0u8; 8]; + buffer.copy_from_slice(bytes); + // The encoder flips the sign bit so that the ordering of the keys + // matches the ordering of the values. + u64::from_be_bytes(buffer) ^ (1 << 63) + }; + Ok((decode(&key[..8]), decode(&key[8..]))) + } + + /// Prices a full chunk without reading any state. + /// + /// The dry run cannot know how many revisions a document retains, so it + /// charges for a whole chunk of the type's largest documents plus both + /// endings: the record write of a first chunk and the subtree removal of a + /// terminal one. Every erase is therefore admitted against the same + /// worst-case estimate whatever the document's actual history length. + #[allow(clippy::too_many_arguments)] + fn estimated_erase_document_operations( + &self, + document_id: Identifier, + history_path: &[Vec], + lifecycle_path: &[Vec], + chunk: u16, + revision_size: u32, + block_info: &BlockInfo, + mut batch_operations: Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut single_operation = Vec::with_capacity(2); + for sequence in 1..=chunk as u64 { + // Distinct synthetic keys of the real shape: the dry run's batch is + // checked for consistency like any other, so a repeated key would be + // rejected rather than priced. + let mut key = encode_u64(block_info.time_ms); + key.extend(encode_u64(sequence)); + self.grove_get_raw( + history_path.into(), + &key, + DirectQueryType::StatelessDirectQuery { + in_tree_type: TreeType::ProvableCountTree, + query_target: QueryTarget::QueryTargetValue(revision_size), + }, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + single_operation.clear(); + self.batch_delete( + history_path.into(), + &key, + StatelessBatchDelete { + in_tree_type: TreeType::ProvableCountTree, + estimated_key_size: REVISION_KEY_LENGTH as u32, + estimated_value_size: revision_size, + }, + transaction, + &mut single_operation, + &platform_version.drive, + )?; + batch_operations.append(&mut single_operation); + } + + let flags_len = StorageFlags::approximate_size(true, None); + self.batch_insert::<0>( + PathKeyUnknownElementSize(( + KeyInfoPath::from_known_owned_path(lifecycle_path.to_vec()), + KeyInfo::KnownKey(document_id.to_vec()), + Element::required_item_space( + DOCUMENT_LIFECYCLE_RECORD_SIZE, + flags_len, + &platform_version.drive.grove_version, + )?, + )), + &mut batch_operations, + &platform_version.drive, + )?; + + let mut history_root = history_path.to_vec(); + history_root.pop(); + self.batch_delete( + history_root.as_slice().into(), + document_id.as_slice(), + StatelessBatchDelete { + in_tree_type: TreeType::NormalTree, + estimated_key_size: 32, + estimated_value_size: TreeType::ProvableCountTree.inner_node_type().cost() + 3, + }, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + + Ok(batch_operations) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/mod.rs b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/mod.rs index 7fe17619b5d..7bfac71dd9b 100644 --- a/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -41,9 +42,15 @@ impl Drive { estimated_costs_only_with_layer_info, platform_version, ), + 1 => Self::add_estimation_costs_for_remove_document_to_primary_storage_v1( + primary_key_path, + document_type, + estimated_costs_only_with_layer_info, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_estimation_costs_for_remove_document_to_primary_storage".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v1/mod.rs new file mode 100644 index 00000000000..572f0421384 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/internal/add_estimation_costs_for_remove_document_to_primary_storage/v1/mod.rs @@ -0,0 +1,83 @@ +use grovedb::batch::KeyInfoPath; + +use grovedb::EstimatedLayerCount::PotentiallyAtMaxElements; +use grovedb::EstimatedLayerInformation; +use grovedb::EstimatedLayerSizes::{AllReference, Mix}; + +use dpp::data_contract::document_type::DocumentTypeRef; + +use std::collections::HashMap; + +use crate::drive::constants::{ + AVERAGE_NUMBER_OF_UPDATES, AVERAGE_UPDATE_BYTE_COUNT_REQUIRED_SIZE, + DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE, +}; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::storage_flags::StorageFlags; + +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::version::PlatformVersion; + +impl Drive { + /// Adds the estimation layer for removing one entry from a document type's + /// primary-key tree. + /// + /// A keep-history type stores current pointers there rather than document + /// items, so its layer is sized from the reference shape the writer + /// installs; a summable type's pointers additionally carry the document's + /// contribution. Every other document type is estimated exactly as v0 does. + #[inline(always)] + pub(super) fn add_estimation_costs_for_remove_document_to_primary_storage_v1( + primary_key_path: [&[u8]; 5], + document_type: DocumentTypeRef, + estimated_costs_only_with_layer_info: &mut HashMap, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + if !document_type.documents_keep_history() { + return Self::add_estimation_costs_for_remove_document_to_primary_storage_v0( + primary_key_path, + document_type, + estimated_costs_only_with_layer_info, + platform_version, + ); + } + + let approximate_size = if document_type.documents_mutable() { + Some(( + AVERAGE_NUMBER_OF_UPDATES as u16, + AVERAGE_UPDATE_BYTE_COUNT_REQUIRED_SIZE, + )) + } else { + None + }; + let flags_size = Some(StorageFlags::approximate_size(true, approximate_size)); + let references = if document_type.documents_summable().is_some() { + Mix { + subtrees_size: None, + items_size: None, + references_size: None, + items_with_sum_item_size: None, + references_with_sum_item_size: Some(( + 32, + DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE, + flags_size, + 1, + )), + } + } else { + AllReference(32, DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE, flags_size) + }; + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_path(primary_key_path), + EstimatedLayerInformation { + tree_type: document_type.primary_key_tree_type(platform_version)?, + estimated_layer_count: PotentiallyAtMaxElements, + estimated_layer_sizes: references, + }, + ); + + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index e04dc6eb7f7..0a92a23bf7c 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -43,6 +43,7 @@ mod delete_document_for_contract_with_named_type_operations; // This module contains functionality to delete a document for contract operations mod delete_document_for_contract_operations; mod delete_index_only_document_for_contract_operations; +mod erase_document_for_contract_operations; mod internal; @@ -1262,13 +1263,29 @@ mod tests { ); } + /// Protocol 13 refuses the delete outright, and protocol 14 carries it out: + /// the document leaves every ordinary read while its revisions stay where + /// they are. #[test] - fn test_delete_document_keeps_history_returns_error() { + fn test_delete_document_keeps_history_returns_error_at_protocol_13() { + run_keep_history_delete_at_protocol_version(13); + } + + #[test] + fn should_delete_a_keep_history_document_without_touching_its_revisions_at_protocol_14() { + run_keep_history_delete_at_protocol_version(14); + } + + fn run_keep_history_delete_at_protocol_version(protocol_version: u32) { + use crate::drive::document::lifecycle::DocumentLifecycleState; + use dpp::document::DocumentV0Getters; + let drive = setup_drive_with_initial_state_structure(None); let db_transaction = drive.grove.start_transaction(); - let platform_version = PlatformVersion::latest(); + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); let contract = setup_contract( &drive, @@ -1328,24 +1345,55 @@ mod tests { .try_into() .expect("this be 32 bytes"); - // Attempting to delete a document that keeps history should return an error - let err = drive - .delete_document_for_contract( - document_id, + let outcome = drive.delete_document_for_contract( + document_id, + &contract, + "person", + BlockInfo::default(), + true, + None, + platform_version, + Some(&EPOCH_CHANGE_FEE_VERSION_TEST), + ); + + if protocol_version < 14 { + assert!(matches!( + outcome.expect_err("expected deleting a history-keeping document to fail"), + Error::Drive(DriveError::InvalidDeletionOfDocumentThatKeepsHistory(_)) + )); + return; + } + + outcome.expect("expected the delete to succeed"); + + let document_id = Identifier::from(document_id); + let (state, _) = drive + .fetch_document_lifecycle( &contract, - "person", - BlockInfo::default(), - true, + document_type, + document_id, + None, None, platform_version, - Some(&EPOCH_CHANGE_FEE_VERSION_TEST), ) - .expect_err("expected deleting a history-keeping document to fail"); + .expect("expected to read the lifecycle"); + let DocumentLifecycleState::Deleted(retained) = state else { + panic!("expected the document to be deleted, got {state:?}"); + }; + assert_eq!( + retained.id(), + document_id, + "the retained revision must still be readable" + ); - assert!(matches!( - err, - Error::Drive(DriveError::InvalidDeletionOfDocumentThatKeepsHistory(_)) - )); + let query = DriveDocumentQuery::all_items_query(&contract, document_type, None); + let (documents, _, _) = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("expected to query documents"); + assert!( + documents.is_empty(), + "a deleted document must be absent from every ordinary read" + ); } // ---------- Error-path tests (added for coverage) ---------- @@ -1442,7 +1490,8 @@ mod tests { random_document_id, nonexistent_contract_id, "profile", - &epoch, + &BlockInfo::default_with_epoch(epoch), + None, None, &mut estimated_costs_only_with_layer_info, 0, diff --git a/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/mod.rs b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/mod.rs index b158eb7ed33..9d2f5b4b52d 100644 --- a/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -56,9 +57,18 @@ impl Drive { batch_operations, platform_version, ), + 1 => self.remove_document_from_primary_storage_v1( + document_id, + document_type, + contract_documents_primary_key_path, + estimated_costs_only_with_layer_info, + transaction, + batch_operations, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "remove_document_from_primary_storage".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v1/mod.rs new file mode 100644 index 00000000000..72984f48644 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/remove_document_from_primary_storage/v1/mod.rs @@ -0,0 +1,109 @@ +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use grovedb::batch::KeyInfoPath; + +use grovedb::{EstimatedLayerInformation, MaybeTree, TransactionArg}; + +use dpp::data_contract::document_type::DocumentTypeRef; + +use std::collections::HashMap; + +use crate::util::grove_operations::BatchDeleteApplyType::{ + StatefulBatchDelete, StatelessBatchDelete, +}; + +use crate::drive::constants::DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE; +use crate::drive::Drive; + +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::identifier::Identifier; + +use crate::util::type_constants::DEFAULT_HASH_SIZE_U32; +use dpp::version::PlatformVersion; + +impl Drive { + /// Removes one entry from a document type's primary-key tree. + /// + /// A keep-history type's entry is the document's current pointer, a + /// reference rather than an item, so the stateless size the dry run + /// charges comes from the reference shape. Removing it is still removing a + /// leaf: the revisions the pointer names live in the type's history tree, + /// which this never touches. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn remove_document_from_primary_storage_v1( + &self, + document_id: Identifier, + document_type: DocumentTypeRef, + contract_documents_primary_key_path: [&[u8]; 5], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + batch_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let keeps_history = document_type.documents_keep_history(); + if keeps_history + && platform_version + .drive + .methods + .document + .insert + .add_document_to_primary_storage + == 0 + { + // Under the layout that writer produces, a keep-history document's + // primary entry is the subtree holding its revisions, and removing + // it as a leaf would unlink that subtree's storage without + // reclaiming it. This version exists only alongside the writer that + // stores a pointer there instead. + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "a keep-history primary entry can only be removed where the current pointer \ + layout is in use", + ))); + } + + let primary_key_tree_type = document_type.primary_key_tree_type(platform_version)?; + + let apply_type = if estimated_costs_only_with_layer_info.is_some() { + StatelessBatchDelete { + in_tree_type: primary_key_tree_type, + estimated_key_size: DEFAULT_HASH_SIZE_U32, + estimated_value_size: if keeps_history { + DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE + } else { + document_type.estimated_size(platform_version)? as u32 + }, + } + } else { + // The entry is a document item, or a current pointer to one; never + // a tree. + StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::NotTree), + } + }; + self.batch_delete( + (&contract_documents_primary_key_path).into(), + document_id.as_slice(), + apply_type, + transaction, + batch_operations, + &platform_version.drive, + )?; + + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { + Self::add_estimation_costs_for_remove_document_to_primary_storage( + contract_documents_primary_key_path, + document_type, + estimated_costs_only_with_layer_info, + platform_version, + )?; + } + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs new file mode 100644 index 00000000000..b39ca03665a --- /dev/null +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerCount::{ApproximateElements, PotentiallyAtMaxElements}; +use grovedb::EstimatedLayerInformation; +use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees}; +use grovedb::EstimatedSumTrees::NoSumTrees; +use grovedb::TreeType; + +use crate::drive::document::lifecycle::DOCUMENT_LIFECYCLE_RECORD_SIZE; +use crate::drive::document::paths::{contract_document_type_path_vec, document_lifecycle_path}; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::storage_flags::StorageFlags; + +impl Drive { + /// Registers the layers a lifecycle record write or removal touches: the + /// document type itself, whose lifecycle tree may have to be created, and + /// the lifecycle tree holding one fixed-size record per deleted document. + /// + /// A dry run that leaves either layer unregistered fails outright, so both + /// are always registered even when the tree already exists. + pub(crate) fn add_estimation_costs_for_lifecycle_record( + contract: &DataContract, + document_type: DocumentTypeRef, + layers: &mut HashMap, + _platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let flags_size = Some(StorageFlags::approximate_size(true, None)); + layers.insert( + KeyInfoPath::from_known_owned_path(contract_document_type_path_vec( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + )), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + // The primary-key tree, the lifecycle tree, the history tree + // and one tree per index. + estimated_layer_count: ApproximateElements( + document_type.indexes().len() as u32 + 3, + ), + estimated_layer_sizes: AllSubtrees(1, NoSumTrees, flags_size), + }, + ); + layers.insert( + KeyInfoPath::from_known_owned_path(document_lifecycle_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + )), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: PotentiallyAtMaxElements, + estimated_layer_sizes: AllItems(32, DOCUMENT_LIFECYCLE_RECORD_SIZE, flags_size), + }, + ); + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs new file mode 100644 index 00000000000..23d195e3e2f --- /dev/null +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerCount::PotentiallyAtMaxElements; +use grovedb::EstimatedLayerInformation; +use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees}; +use grovedb::EstimatedSumTrees::AllProvableCountTrees; +use grovedb::TreeType; + +use crate::drive::document::paths::{contract_document_type_path_vec, DOCUMENT_HISTORY_TREE_KEY}; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::storage_flags::StorageFlags; + +impl Drive { + /// Registers the layers an erase chunk touches: the type's history tree, + /// the document's own revision tree, and the lifecycle tree the record is + /// written to or removed from. + /// + /// The revision tree is sized for a document that may retain far more + /// revisions than a chunk removes, so its height is bounded rather than + /// derived from the chunk size; an estimate keyed to the chunk would + /// understate the merk path of a long history. + pub(crate) fn add_estimation_costs_for_erase_document( + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + layers: &mut HashMap, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + Self::add_estimation_costs_for_levels_up_to_contract_document_type_excluded( + contract, + layers, + &platform_version.drive, + )?; + Self::add_estimation_costs_for_lifecycle_record( + contract, + document_type, + layers, + platform_version, + )?; + + let flags_size = Some(StorageFlags::approximate_size(true, None)); + let mut history_root = contract_document_type_path_vec( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + ); + history_root.push(vec![DOCUMENT_HISTORY_TREE_KEY]); + layers.insert( + KeyInfoPath::from_known_owned_path(history_root.clone()), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: PotentiallyAtMaxElements, + estimated_layer_sizes: AllSubtrees(32, AllProvableCountTrees, flags_size), + }, + ); + + let mut history_path = history_root; + history_path.push(document_id.to_vec()); + layers.insert( + KeyInfoPath::from_known_owned_path(history_path), + EstimatedLayerInformation { + tree_type: TreeType::ProvableCountTree, + estimated_layer_count: PotentiallyAtMaxElements, + estimated_layer_sizes: AllItems( + 16, + document_type.max_size(platform_version)? as u32, + flags_size, + ), + }, + ); + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/document/estimation_costs/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/mod.rs index 4a3acee1aed..f717c8c5da8 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/mod.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/mod.rs @@ -4,4 +4,8 @@ mod add_estimation_costs_for_add_document_to_primary_storage; mod add_estimation_costs_for_add_contested_document_to_primary_storage; +mod add_estimation_costs_for_document_lifecycle; + +mod add_estimation_costs_for_erase_document; + pub(crate) mod estimated_sum_trees_for_value_tree_type; diff --git a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs index 072102612e1..be96f8f511a 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs @@ -100,7 +100,7 @@ impl Drive { BatchInsertTreeApplyType::StatefulBatchInsertTree }; if let Some(document) = document { - self.batch_insert_empty_tree_if_not_exists::<0>( + let created = self.batch_insert_empty_tree_if_not_exists::<0>( PathKey((history_root, document.id().to_vec())), TreeType::ProvableCountTree, flags, @@ -110,6 +110,20 @@ impl Drive { operations, version, )?; + // An id whose revisions are still retained is taken, even though + // nothing in the primary-key tree says so. Appending to that + // history would silently merge a new document into a deleted one's + // record, so it is refused here rather than only in transition + // validation: this is the guard that covers writers outside it. + // A dry run skips the probe, which reports the tree as absent, and + // pays for it as a fixed cost so estimation and execution agree. + if !created && !insert_without_check { + return Err(Error::Drive( + DriveError::CorruptedDocumentAlreadyExists( + "a document of this id still retains revisions and can not be created until they are erased", + ), + )); + } } else { operations.push( LowLevelDriveOperation::for_estimated_path_key_empty_provable_count_tree( diff --git a/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs new file mode 100644 index 00000000000..7f2d45be9fb --- /dev/null +++ b/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs @@ -0,0 +1,87 @@ +mod v0; + +use dpp::block::epoch::Epoch; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::document::Document; +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; + +/// Where one keep-history document stands in its lifecycle. +/// +/// Ordinary reads cannot tell a deleted document from one that never existed — +/// both are absent from the primary-key tree — so every check that must +/// distinguish them goes through this. +#[derive(Debug, Clone, PartialEq)] +pub enum DocumentLifecycleState { + /// The document is current and visible to ordinary reads. + Active(Box), + /// The document has been deleted and its revisions are retained. Carries + /// the newest of them, which is where a deleted document's owner is read + /// from. + Deleted(Box), + /// An authorized erasure has started and has not finished. Anyone may + /// continue it, so nothing about the document's owner is needed here. + Erasing, + /// No document, no retained revisions and no record: the id is free. + Absent, +} + +impl DocumentLifecycleState { + /// Whether the id is taken, which is what a create has to know. + pub fn is_present(&self) -> bool { + !matches!(self, DocumentLifecycleState::Absent) + } +} + +impl Drive { + /// Classifies one keep-history document as active, deleted, erasing or + /// absent, and returns the fee for the reads it performed. + /// + /// The reads are ordered by how likely they are to settle the question: the + /// ordinary by-id read answers it for every live document, and only a miss + /// pays for the lifecycle record. A deleted document costs one read more, + /// for the newest revision it retains. + /// + /// This is deliberately not folded into the by-id fetch every document + /// action already performs: only stateful validation of an action on a + /// keep-history type needs the extra reads, and every other document fetch + /// would otherwise pay for them. + pub fn fetch_document_lifecycle( + &self, + contract: &DataContract, + document_type: DocumentTypeRef, + document_id: Identifier, + epoch: Option<&Epoch>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(DocumentLifecycleState, FeeResult), Error> { + match platform_version + .drive + .methods + .document + .query + .fetch_document_lifecycle + { + 0 => self.fetch_document_lifecycle_v0( + contract, + document_type, + document_id, + epoch, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "fetch_document_lifecycle".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs new file mode 100644 index 00000000000..6a0177aad6d --- /dev/null +++ b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs @@ -0,0 +1,160 @@ +use dpp::block::epoch::Epoch; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::document::Document; +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::query_result_type::QueryResultType; +use grovedb::{Element, PathQuery, Query, SizedQuery, TransactionArg}; + +use crate::drive::document::lifecycle::fetch::DocumentLifecycleState; +use crate::drive::document::lifecycle::DocumentLifecycleRecord; +use crate::drive::document::paths::{ + contract_documents_primary_key_path, document_history_path, document_lifecycle_path, +}; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::{DirectQueryType, QueryType}; + +impl Drive { + #[inline(always)] + pub(super) fn fetch_document_lifecycle_v0( + &self, + contract: &DataContract, + document_type: DocumentTypeRef, + document_id: Identifier, + epoch: Option<&Epoch>, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(DocumentLifecycleState, FeeResult), Error> { + if !document_type.documents_keep_history() { + return Err(Error::Drive(DriveError::CorruptedCodeExecution( + "only a document type that keeps history has a lifecycle to fetch", + ))); + } + let mut operations: Vec = vec![]; + let state = self.read_document_lifecycle( + contract, + document_type, + document_id, + &mut operations, + transaction, + platform_version, + )?; + let fee = match epoch { + Some(epoch) => Drive::calculate_fee( + None, + Some(operations), + epoch, + self.config.epochs_per_era, + platform_version, + None, + )?, + None => FeeResult::default(), + }; + Ok((state, fee)) + } + + fn read_document_lifecycle( + &self, + contract: &DataContract, + document_type: DocumentTypeRef, + document_id: Identifier, + operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let primary_path = contract_documents_primary_key_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + ); + let pointer = self.grove_get_raw_optional( + (&primary_path).into(), + document_id.as_slice(), + DirectQueryType::StatefulDirectQuery, + transaction, + operations, + &platform_version.drive, + )?; + if pointer.is_some() { + let element = self + .grove_get( + (&primary_path).into(), + document_id.as_slice(), + QueryType::StatefulQuery, + transaction, + operations, + &platform_version.drive, + )? + .ok_or(Error::Drive(DriveError::CorruptedDriveState( + "a current pointer resolved to nothing".to_string(), + )))?; + let Element::Item(bytes, _) = element else { + return Err(Error::Drive(DriveError::CorruptedElementType( + "a current pointer did not resolve to a document", + ))); + }; + return Ok(DocumentLifecycleState::Active(Box::new( + Document::from_bytes(&bytes, document_type, platform_version)?, + ))); + } + + let lifecycle_path = + document_lifecycle_path(contract.id_ref().as_bytes(), document_type.name().as_str()); + // A document type whose documents have never been deleted has no + // lifecycle tree at all, so a missing path means the same as a missing + // key: the id is free. + let record_bytes = self.grove_get_raw_optional_item( + lifecycle_path.as_slice().into(), + document_id.as_slice(), + DirectQueryType::StatefulDirectQuery, + transaction, + operations, + &platform_version.drive, + )?; + let Some(record_bytes) = record_bytes else { + return Ok(DocumentLifecycleState::Absent); + }; + let record = DocumentLifecycleRecord::deserialize(&record_bytes)?; + if record.is_erasing() { + return Ok(DocumentLifecycleState::Erasing); + } + + // A deleted document's owner is read from the newest revision it still + // retains: a plain reverse range over the revision key space, immune to + // the composite-key bounds a time selector has to get right. + let mut query = Query::new_with_direction(false); + query.insert_all(); + let path_query = PathQuery::new( + document_history_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + document_id.as_slice(), + ), + SizedQuery::new(query, Some(1), None), + ); + let (results, _) = self.grove_get_raw_path_query( + &path_query, + transaction, + QueryResultType::QueryKeyElementPairResultType, + operations, + &platform_version.drive, + )?; + let Some((_, Element::Item(bytes, _))) = results.to_key_elements().into_iter().next() + else { + return Err(Error::Drive(DriveError::CorruptedDriveState( + "a deleted document retains no revision to read its owner from".to_string(), + ))); + }; + Ok(DocumentLifecycleState::Deleted(Box::new( + Document::from_bytes(&bytes, document_type, platform_version)?, + ))) + } +} diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs new file mode 100644 index 00000000000..c329d4afcae --- /dev/null +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -0,0 +1,215 @@ +//! The lifecycle record of a keep-history document, and the per-type tree that holds it. +//! +//! A keep-history document that has been deleted keeps its retained revisions +//! but loses its current pointer and its index references, so nothing in the +//! primary-key tree distinguishes it from a document that never existed. The +//! record supplies that distinction: it exists exactly while the document is +//! deleted or erasing, names the block time it was deleted at, and — once an +//! erasure has been authorized — the block time that erasure started and the +//! revision it started from. +//! +//! It lives in its own tree under the document type rather than inside the +//! document's history, so every key in the history stays in the revision +//! domain and deleted documents stay enumerable per type. + +#[cfg(feature = "server")] +mod fetch; + +#[cfg(feature = "server")] +pub use fetch::DocumentLifecycleState; + +use crate::error::{drive::DriveError, Error}; + +/// Byte length of an encoded lifecycle record: one version byte and four +/// big-endian `u64`s. +pub const DOCUMENT_LIFECYCLE_RECORD_SIZE: u32 = 33; + +/// Version byte of the only record layout that exists. +const DOCUMENT_LIFECYCLE_RECORD_V0: u8 = 0; + +/// What the committed state says about one keep-history document that is no +/// longer visible to ordinary reads. +/// +/// The encoding is fixed width so that the deleted and erasing forms occupy +/// exactly the same number of bytes: an erase start overwrites the record in +/// place, and an overwrite of a different size would re-price storage and drop +/// the original writer's flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct DocumentLifecycleRecordV0 { + /// Block time the document was deleted at. + pub deleted_at_ms: u64, + /// Block time an authorized erasure started at, or zero while none has. + pub erasing_started_at_ms: u64, + /// Timestamp component of the newest revision retained when the erasure + /// started, or zero while none has. + pub erasing_from_time_ms: u64, + /// History sequence of the newest revision retained when the erasure + /// started, or zero while none has. + pub erasing_from_revision: u64, +} + +/// A lifecycle record in whichever layout it was written with. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DocumentLifecycleRecord { + /// The original layout. + V0(DocumentLifecycleRecordV0), +} + +impl DocumentLifecycleRecord { + /// Builds the record a delete writes: a deletion time and no erasure. + pub fn deleted_at(deleted_at_ms: u64) -> Self { + DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { + deleted_at_ms, + ..Default::default() + }) + } + + /// Block time the document was deleted at. + pub fn deleted_at_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.deleted_at_ms, + } + } + + /// Block time an authorized erasure started at, or zero while none has. + pub fn erasing_started_at_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_started_at_ms, + } + } + + /// Timestamp component of the newest revision retained at erase start. + pub fn erasing_from_time_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_from_time_ms, + } + } + + /// History sequence of the newest revision retained at erase start. + pub fn erasing_from_revision(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_from_revision, + } + } + + /// Whether an authorized erasure has already started, which is what lets a + /// continuation run without any authorization of its own. + pub fn is_erasing(&self) -> bool { + self.erasing_started_at_ms() != 0 + } + + /// Returns the record an erase start writes over this one, keeping the + /// deletion time and recording the erasure it authorizes. + pub fn starting_erase_at( + &self, + erasing_started_at_ms: u64, + erasing_from_time_ms: u64, + erasing_from_revision: u64, + ) -> Self { + DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { + deleted_at_ms: self.deleted_at_ms(), + erasing_started_at_ms, + erasing_from_time_ms, + erasing_from_revision, + }) + } + + /// Encodes the record as one version byte followed by four big-endian + /// `u64`s. + /// + /// The layout is fixed width rather than the variable-length integer + /// encoding used elsewhere, because a record whose erase fields are zero + /// must occupy exactly as many bytes as one whose erase fields are real + /// timestamps. + pub fn serialize(&self) -> Vec { + let mut bytes = Vec::with_capacity(DOCUMENT_LIFECYCLE_RECORD_SIZE as usize); + match self { + DocumentLifecycleRecord::V0(v0) => { + bytes.push(DOCUMENT_LIFECYCLE_RECORD_V0); + bytes.extend(v0.deleted_at_ms.to_be_bytes()); + bytes.extend(v0.erasing_started_at_ms.to_be_bytes()); + bytes.extend(v0.erasing_from_time_ms.to_be_bytes()); + bytes.extend(v0.erasing_from_revision.to_be_bytes()); + } + } + bytes + } + + /// Decodes a record, requiring the whole input to be consumed. + /// + /// Trailing bytes are a decode failure rather than something to ignore: a + /// record is written only by consensus, so anything else at that key means + /// the state is not what this code believes it is. + pub fn deserialize(bytes: &[u8]) -> Result { + let corrupt = |message: &'static str| { + Error::Drive(DriveError::CorruptedDriveState(message.to_string())) + }; + let Some((version, fields)) = bytes.split_first() else { + return Err(corrupt("document lifecycle record is empty")); + }; + if *version != DOCUMENT_LIFECYCLE_RECORD_V0 { + return Err(corrupt("unknown document lifecycle record version")); + } + if fields.len() != 32 { + return Err(corrupt( + "document lifecycle record does not contain four timestamps", + )); + } + let field = |index: usize| -> u64 { + let mut buffer = [0u8; 8]; + buffer.copy_from_slice(&fields[index * 8..(index + 1) * 8]); + u64::from_be_bytes(buffer) + }; + Ok(DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { + deleted_at_ms: field(0), + erasing_started_at_ms: field(1), + erasing_from_time_ms: field(2), + erasing_from_revision: field(3), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_encode_deleted_and_erasing_records_to_the_same_length() { + let deleted = DocumentLifecycleRecord::deleted_at(1_700_000_000_000); + let erasing = deleted.starting_erase_at(1_700_000_500_000, 1_699_000_000_000, 42); + assert_eq!( + deleted.serialize().len(), + erasing.serialize().len(), + "an erase start overwrites the record in place and must not change its size" + ); + assert_eq!( + deleted.serialize().len(), + DOCUMENT_LIFECYCLE_RECORD_SIZE as usize + ); + } + + #[test] + fn should_round_trip_every_field() { + let record = DocumentLifecycleRecord::deleted_at(7).starting_erase_at(8, 9, 10); + let recovered = + DocumentLifecycleRecord::deserialize(&record.serialize()).expect("round trip"); + assert_eq!(record, recovered); + assert!(recovered.is_erasing()); + } + + #[test] + fn should_report_a_record_without_erase_fields_as_deleted_not_erasing() { + let record = DocumentLifecycleRecord::deleted_at(7); + assert!(!record.is_erasing()); + assert_eq!(record.erasing_from_revision(), 0); + } + + #[test] + fn should_reject_records_that_do_not_consume_their_whole_input() { + let mut bytes = DocumentLifecycleRecord::deleted_at(7).serialize(); + bytes.push(0); + assert!(DocumentLifecycleRecord::deserialize(&bytes).is_err()); + assert!(DocumentLifecycleRecord::deserialize(&[]).is_err()); + assert!(DocumentLifecycleRecord::deserialize(&[9; 33]).is_err()); + } +} diff --git a/packages/rs-drive/src/drive/document/mod.rs b/packages/rs-drive/src/drive/document/mod.rs index 5be0714d739..b808b80ecbf 100644 --- a/packages/rs-drive/src/drive/document/mod.rs +++ b/packages/rs-drive/src/drive/document/mod.rs @@ -34,6 +34,9 @@ mod index_uniqueness; mod insert; #[cfg(any(feature = "server", feature = "fixtures-and-mocks"))] mod insert_contested; +/// The lifecycle record of a deleted keep-history document. +#[cfg(any(feature = "server", feature = "verify"))] +pub mod lifecycle; /// Activation migration and its inventory. #[cfg(feature = "server")] pub mod migration; diff --git a/packages/rs-drive/src/drive/document/paths.rs b/packages/rs-drive/src/drive/document/paths.rs index 893fd219d85..8ec6e70e7a9 100644 --- a/packages/rs-drive/src/drive/document/paths.rs +++ b/packages/rs-drive/src/drive/document/paths.rs @@ -12,9 +12,24 @@ use grovedb::batch::key_info::KeyInfo; #[cfg(feature = "server")] use grovedb::batch::KeyInfoPath; +/// Reserved document-type key containing the lifecycle records of documents +/// that have been deleted but whose revisions are still retained. +/// +/// Sits between the primary-key tree at 0 and the history tree at 2. Index +/// trees are keyed by property names, which cannot start below `0x30`, so no +/// index can collide with a reserved single-byte key. +pub const DOCUMENT_LIFECYCLE_TREE_KEY: u8 = 1; + /// Reserved document-type key containing the revision trees. pub const DOCUMENT_HISTORY_TREE_KEY: u8 = 2; +/// Path to the lifecycle records of one document type. +pub fn document_lifecycle_path(contract_id: &[u8], document_type_name: &str) -> Vec> { + let mut path = contract_document_type_path_vec(contract_id, document_type_name); + path.push(vec![DOCUMENT_LIFECYCLE_TREE_KEY]); + path +} + /// Path to all retained revisions of one document. pub fn document_history_path( contract_id: &[u8], diff --git a/packages/rs-drive/src/query/document_history_drive_query.rs b/packages/rs-drive/src/query/document_history_drive_query.rs index ed2cccf35ad..f1ea0f5f75a 100644 --- a/packages/rs-drive/src/query/document_history_drive_query.rs +++ b/packages/rs-drive/src/query/document_history_drive_query.rs @@ -1,7 +1,10 @@ //! A page of a document's retained revisions with its lifecycle, read from //! whichever history layout the protocol version stores. -use crate::drive::document::paths::{contract_document_type_path_vec, DOCUMENT_HISTORY_TREE_KEY}; +use crate::drive::document::lifecycle::DocumentLifecycleRecord; +use crate::drive::document::paths::{ + contract_document_type_path_vec, DOCUMENT_HISTORY_TREE_KEY, DOCUMENT_LIFECYCLE_TREE_KEY, +}; use crate::drive::document::MAX_DOCUMENT_HISTORY_FETCH_LIMIT; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -58,6 +61,24 @@ pub enum DocumentHistoryState { Active, /// Neither a current document nor a retained history exists. Absent, + /// The document has been deleted; its revisions are still retained. + Deleted, + /// An authorized erasure has started and has not finished. + Erasing, +} + +/// Authenticated lifecycle timestamps independent of the requested page. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DocumentHistoryLifecycleTimes { + /// Block time the document was deleted at, zero while it is active. + pub deleted_at_ms: u64, + /// Block time an authorized erasure started at, zero while none has. + pub erasing_started_at_ms: u64, + /// Timestamp of the newest revision retained when the erasure started. + pub erasing_from_time_ms: u64, + /// History sequence of the newest revision retained when the erasure + /// started. + pub erasing_from_revision: u64, } /// Authenticated history metadata independent of the requested page. @@ -67,6 +88,8 @@ pub struct DocumentHistoryLifecycle { pub state: DocumentHistoryState, /// Count authenticated by the per-document count-tree element. pub remaining_revisions: u64, + /// Times recorded by the lifecycle record, all zero unless one exists. + pub times: DocumentHistoryLifecycleTimes, } /// One retained edit, including its complete pagination cursor. @@ -212,7 +235,12 @@ impl DocumentHistoryDriveQuery { /// Queries the pointer, lifecycle reservation, and raw history tree separately. pub fn metadata_path_query(&self, version: &PlatformVersion) -> Result { self.validate()?; - let queries = [0, 1, DOCUMENT_HISTORY_TREE_KEY].map(|branch| { + let queries = [ + 0, + DOCUMENT_LIFECYCLE_TREE_KEY, + DOCUMENT_HISTORY_TREE_KEY, + ] + .map(|branch| { let mut path = contract_document_type_path_vec(&self.contract_id, &self.document_type_name); path.push(vec![branch]); @@ -238,9 +266,12 @@ impl DocumentHistoryDriveQuery { primary.push(vec![0]); let mut history = primary.clone(); history[4] = vec![DOCUMENT_HISTORY_TREE_KEY]; + let mut lifecycle = primary.clone(); + lifecycle[4] = vec![DOCUMENT_LIFECYCLE_TREE_KEY]; let mut active = false; let mut latest_revision = None; let mut count = None; + let mut record = None; for (path, key, element) in metadata { let Some(element) = element else { continue; @@ -248,7 +279,17 @@ impl DocumentHistoryDriveQuery { if key != self.document_id { return Err(corrupt("history metadata key does not match the document")); } - if path == primary { + if path == lifecycle { + let Element::Item(bytes, _) = element else { + return Err(corrupt("a lifecycle record is not an item")); + }; + if record + .replace(DocumentLifecycleRecord::deserialize(&bytes)?) + .is_some() + { + return Err(corrupt("duplicate lifecycle record")); + } + } else if path == primary { let bytes = match element { Element::Item(bytes, _) | Element::ItemWithSumItem(bytes, _, _) => bytes, _ => { @@ -281,9 +322,19 @@ impl DocumentHistoryDriveQuery { )); } } - if active && count.unwrap_or_default() == 0 || !active && count.unwrap_or_default() > 0 { + if active && record.is_some() { + return Err(corrupt( + "a current document cannot also carry a lifecycle record", + )); + } + if active && count.unwrap_or_default() == 0 + || !active && record.is_none() && count.unwrap_or_default() > 0 + { return Err(corrupt("current document and retained history disagree")); } + if record.is_some() && count.unwrap_or_default() == 0 { + return Err(corrupt("a lifecycle record survives its retained history")); + } if matches!( self.filter, DocumentHistoryFilter::Revision(_) | DocumentHistoryFilter::StartAtRevision(_) @@ -294,14 +345,29 @@ impl DocumentHistoryDriveQuery { "retained history contains a revision gap; use time pagination", )); } + let state = match (active, &record) { + (true, _) => DocumentHistoryState::Active, + (false, Some(record)) if record.is_erasing() => DocumentHistoryState::Erasing, + (false, Some(_)) => DocumentHistoryState::Deleted, + // A history with no record and no current pointer predates the + // lifecycle record; every lifecycle delete writes one, so this is + // retained defensively while old histories are migrated. + (false, None) if count.unwrap_or_default() > 0 => DocumentHistoryState::Deleted, + (false, None) => DocumentHistoryState::Absent, + }; + let times = record + .map(|record| DocumentHistoryLifecycleTimes { + deleted_at_ms: record.deleted_at_ms(), + erasing_started_at_ms: record.erasing_started_at_ms(), + erasing_from_time_ms: record.erasing_from_time_ms(), + erasing_from_revision: record.erasing_from_revision(), + }) + .unwrap_or_default(); Ok(( DocumentHistoryLifecycle { - state: if active { - DocumentHistoryState::Active - } else { - DocumentHistoryState::Absent - }, + state, remaining_revisions: count.unwrap_or_default(), + times, }, count.is_some(), )) diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index d4c2f405f68..4be93d86dd9 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -202,7 +202,8 @@ pub mod document_history_drive_query; #[cfg(any(feature = "server", feature = "verify"))] pub use document_history_drive_query::{ DocumentHistoryDriveQuery, DocumentHistoryDriveQueryExecutionResult, DocumentHistoryEntry, - DocumentHistoryFilter, DocumentHistoryLifecycle, DocumentHistoryState, + DocumentHistoryFilter, DocumentHistoryLifecycle, DocumentHistoryLifecycleTimes, + DocumentHistoryState, }; #[cfg(any(feature = "server", feature = "verify"))] diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs index e0bef2d3979..14d0a3631cc 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs @@ -49,6 +49,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { }), DocumentOperation(DocumentOperationType::DeleteDocument { document_id: base.id(), + deleter_id: Some(owner_id), contract_info: DataContractInfo::DataContractFetchInfo( base.data_contract_fetch_info(), ), 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 07d8e37fa0f..541fdae5552 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 @@ -98,6 +98,26 @@ pub enum DocumentOperationType<'a> { DeleteDocument { /// The document id document_id: Identifier, + /// The identity credited with the bytes of the lifecycle record a + /// keep-history delete writes, and refunded when an erase removes it. + /// `None` where no signer stands behind the delete. + deleter_id: Option, + /// Data Contract info to potentially be resolved if needed + contract_info: DataContractInfo<'a>, + /// Document type + document_type_info: DocumentTypeInfo<'a>, + }, + /// Removes a bounded chunk of the retained revisions of an already deleted + /// keep-history document, dropping the history subtree and the lifecycle + /// record with the terminal chunk. + EraseDocument { + /// The document id + document_id: Identifier, + /// Whether this is the authorized first chunk, which commits the + /// document to erasure, rather than a continuation of one already + /// committed. Derived from the document's committed lifecycle, never + /// from the transition. + start: bool, /// Data Contract info to potentially be resolved if needed contract_info: DataContractInfo<'a>, /// Document type @@ -421,6 +441,7 @@ impl DocumentOperationType<'_> { } DocumentOperationType::DeleteDocument { document_id, + deleter_id, contract_info, document_type_info, } => { @@ -439,6 +460,8 @@ impl DocumentOperationType<'_> { document_id, contract, document_type, + block_info, + deleter_id, None, estimated_costs_only_with_layer_info, block_info.time_ms, @@ -446,6 +469,36 @@ impl DocumentOperationType<'_> { platform_version, ) } + DocumentOperationType::EraseDocument { + document_id, + start, + contract_info, + document_type_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 = document_type_info.resolve(contract)?; + + let mut operations = drive.erase_document_for_contract_operations( + document_id, + contract, + document_type, + block_info, + start, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + drive_operations.append(&mut operations); + Ok(drive_operations) + } DocumentOperationType::DeleteIndexOnlyDocument { document_id, owner_id, diff --git a/packages/rs-drive/tests/drive_storage_ops_coverage.rs b/packages/rs-drive/tests/drive_storage_ops_coverage.rs index 6403e728fec..6db6ef4c404 100644 --- a/packages/rs-drive/tests/drive_storage_ops_coverage.rs +++ b/packages/rs-drive/tests/drive_storage_ops_coverage.rs @@ -870,6 +870,7 @@ mod document_operation_tests { fn document_operation_add_type_construction() { let id = dpp::prelude::Identifier::new([2u8; 32]); let _op = DocumentOperationType::DeleteDocument { + deleter_id: None, document_id: id, contract_info: drive::util::object_size_info::DataContractInfo::DataContractId( dpp::prelude::Identifier::new([3u8; 32]), From 713df6a4218cb36dd43c50330e8d639cc4281c50 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:05:51 +0700 Subject: [PATCH 05/48] feat(drive): route the erase transition through to its storage operation The erase transition action mirrors the delete one: only a base, no token cost of its own, and a nonce bump alongside the storage operation it converts into. It is appended to the document action enum and to the action type mapping. The parser tests are rewritten around the rule generation 3 now applies: a keep-history type may allow deletion, may additionally allow erasure, may not carry a contested index, and may withdraw deletion only while it is not erasable. Co-Authored-By: Claude Opus 5 (1M context) --- .../try_from_schema/v3/keep_history_tests.rs | 356 ++++++++++-------- .../mod.rs | 6 - .../v0/mod.rs | 9 +- .../document/document_erase_transition.rs | 62 +++ .../batch/document/document_transition.rs | 7 + .../batch/document/mod.rs | 1 + .../document_erase_transition_action/mod.rs | 31 ++ .../transformer.rs | 37 ++ .../v0/mod.rs | 19 + .../v0/transformer.rs | 71 ++++ .../document_transition_action_type.rs | 1 + .../document_transition/mod.rs | 9 + .../src/util/batch/drive_op_batch/document.rs | 7 - 13 files changed, 452 insertions(+), 164 deletions(-) create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs index f0ceb8f5dcc..20d0a09a40d 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs @@ -1,6 +1,10 @@ -//! Regression tests for the `documentsKeepHistory` + `canBeDeleted` -//! cross-flag rule added in `try_from_schema` v3 (protocol version 14). +//! The keep-history document lifecycle as generation 3 admits it: a +//! keep-history type may allow deletion, may additionally allow erasure, and +//! may not carry a contested index. use super::*; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, +}; use platform_value::platform_value; /// Parses through the public dispatcher at the given protocol version so @@ -50,159 +54,189 @@ fn keep_history_deletable_schema() -> Value { }) } -/// `documentsKeepHistory: true` + `canBeDeleted: true` is -/// self-contradictory: rs-drive unconditionally refuses to delete -/// a document whose type keeps history -/// (`InvalidDeletionOfDocumentThatKeepsHistory`), so `canBeDeleted: -/// true` advertises a capability the storage layer will always -/// reject. The parser must reject the combination at contract -/// creation time so an SDK user gets a clean validation error -/// instead of the delete failing as an internal error at execution. -/// -/// With the `validation` feature enabled the rejection must surface -/// as `ProtocolError::ConsensusError` (not bare -/// `ProtocolError::DataContractError`) — drive-abci's -/// `transform_into_action_v0` only turns the consensus variant into -/// a clean invalid (paid) transition with a bump action; the -/// data-contract-error variant propagates as an internal execution -/// error in validator mode. +/// The two settings describe different things and no longer contradict each +/// other: history decides what is retained, deletion decides what ordinary +/// reads can still see. A delete on such a type removes the document from +/// every ordinary read and leaves its revisions readable. #[test] -fn doctype_keep_history_with_can_be_deleted_rejected() { - let result = parse(keep_history_deletable_schema()); - assert!( - result.is_err(), - "documentsKeepHistory: true + canBeDeleted: true must be rejected" - ); - let err = result.unwrap_err(); - let msg = format!("{:?}", err); - assert!( - msg.contains("documentsKeepHistory") && msg.contains("canBeDeleted"), - "error must reference both documentsKeepHistory and canBeDeleted; got {msg}" - ); - #[cfg(feature = "validation")] +fn should_accept_a_keep_history_type_that_allows_deletion() { + let document_type = parse(keep_history_deletable_schema()) + .expect("a keep-history type may allow deletion from protocol version 14"); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); assert!( - matches!(err, ProtocolError::ConsensusError(_)), - "with `validation` feature the rejection must be ProtocolError::ConsensusError so \ - drive-abci's transform_into_action turns it into an invalid (paid) transition \ - with a bump action rather than propagating as an internal execution error; got \ - {err:?}" + !document_type.documents_can_be_erased(), + "erasure is not implied by deletion; it has to be asked for" ); } -/// Omitting `canBeDeleted` exercises the contract-config default boundary: -/// the latest config defaults it to `true`, so a keep-history document type -/// remains contradictory and must be rejected during full validation. +/// The contract config defaults `canBeDeleted` to true, so a keep-history type +/// that says nothing about deletion is deletable — the same value it carried +/// before protocol 14, now with a delete that works. #[test] -fn doctype_keep_history_with_can_be_deleted_omitted_rejected() { +fn should_accept_a_keep_history_type_that_omits_the_delete_flag() { let schema = platform_value!({ "type": "object", "properties": { - "label": { - "type": "string", - "maxLength": 50, - "position": 0, - }, + "label": {"type": "string", "maxLength": 50, "position": 0}, }, "additionalProperties": false, "documentsKeepHistory": true, }); - let result = parse(schema); - assert!( - result.is_err(), - "omitted canBeDeleted must default to true and conflict with documentsKeepHistory" - ); - let msg = format!("{:?}", result.unwrap_err()); - assert!( - msg.contains("documentsKeepHistory") && msg.contains("canBeDeleted"), - "error must reference both documentsKeepHistory and defaulted canBeDeleted; got {msg}" - ); + let document_type = parse(schema).expect("the omitted flag defaults to deletable"); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); } -/// `documentsKeepHistory: true` + `canBeDeleted: true` is rejected -/// ONLY when `full_validation: true`. With `full_validation: false` -/// (the restore / migration / cache-warmup path) the same schema must -/// parse cleanly so already-deployed contradictory contracts continue -/// to load at v14+ — the drive-abci delete-transition guard turns -/// their deletes into clean invalid (paid) transitions instead of -/// rejecting them as internal errors at the contract-load layer. +/// A keep-history type that withholds deletion is still valid: its documents +/// are append-only and can never leave ordinary reads. #[test] -fn doctype_keep_history_with_can_be_deleted_accepted_without_full_validation() { - let document_type = parse_at_version(keep_history_deletable_schema(), 14, false).expect( - "documentsKeepHistory: true + canBeDeleted: true must be accepted when \ - full_validation: false so already-deployed contradictory contracts continue to load", - ); +fn should_accept_a_keep_history_type_that_withholds_deletion() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }); + let document_type = parse(schema).expect("an append-only keep-history type is valid"); assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); + assert!(!document_type.documents_can_be_deleted()); } -/// Protocol version 12 routes to `try_from_schema` v2, which has no -/// cross-flag rule — the combination must stay accepted there even under -/// full validation, because v12 is released and consensus-frozen: -/// contracts accepted at v12 must replay identically. +/// The released parsers never saw the rule and must keep parsing the same +/// schemas the same way, so a historical block replays identically. #[test] -fn doctype_keep_history_with_can_be_deleted_accepted_at_protocol_version_12() { - let document_type = parse_at_version(keep_history_deletable_schema(), 12, true).expect( - "documentsKeepHistory: true + canBeDeleted: true must stay accepted at protocol \ - version 12 (consensus-frozen v2 parser) for replay compatibility", - ); - assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); +fn should_accept_a_keep_history_deletable_type_at_released_protocol_versions() { + for protocol in [12, 13] { + let document_type = parse_at_version(keep_history_deletable_schema(), protocol, true) + .unwrap_or_else(|error| { + panic!("protocol {protocol} must still accept the schema: {error:?}") + }); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); + } +} + +fn erasable_schema(keep_history: bool, can_be_deleted: bool, can_be_erased: bool) -> Value { + platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": keep_history, + "canBeDeleted": can_be_deleted, + "canBeErased": can_be_erased, + }) +} + +#[test] +fn should_accept_an_erasable_keep_history_deletable_type() { + let document_type = + parse(erasable_schema(true, true, true)).expect("the admitted combination must parse"); + assert!(document_type.documents_can_be_erased()); +} + +/// Erasure purges retained revisions of a document that has already been +/// deleted, so a type with no history has nothing to purge and a type whose +/// documents can never be deleted can never reach the state erasure acts on. +/// Neither is silently ignored. +#[test] +fn should_reject_erasure_without_history_or_without_deletion() { + for (keep_history, can_be_deleted) in [(false, true), (true, false), (false, false)] { + let error = parse(erasable_schema(keep_history, can_be_deleted, true)).unwrap_err(); + let message = format!("{error:?}"); + assert!( + message.contains("canBeErased"), + "the error must name the flag the author has to change; got {message}" + ); + } +} + +/// Erasure defaults to off: a type that says nothing about it cannot have its +/// revisions purged. +#[test] +fn should_default_erasure_to_off() { + let document_type = parse(keep_history_deletable_schema()).expect("parses"); + assert!(!document_type.documents_can_be_erased()); +} + +/// The keyword does not exist in the earlier meta-schemas, so a contract that +/// declares it is refused there rather than silently parsing without it. +#[test] +fn should_reject_the_erasure_keyword_at_released_protocol_versions() { + for protocol in [12, 13] { + assert!( + parse_at_version(erasable_schema(true, true, true), protocol, true).is_err(), + "protocol {protocol} has no canBeErased keyword" + ); + } } -/// Guard against an over-broad fix: `documentsKeepHistory: true` + -/// `canBeDeleted: false` is consistent (the doctype is append-only) -/// and must continue to parse cleanly. The sibling omitted-key regression -/// covers the distinct default-`true` boundary and therefore expects -/// rejection rather than acceptance. +/// A contested resource is awarded outside transition validation, at an id +/// derived from the winner rather than from the contested values, so that award +/// can land on an id whose retained history already exists. The two are kept +/// apart at registration until the contested machinery can handle it. #[test] -fn doctype_keep_history_with_can_be_deleted_false_accepted() { +fn should_reject_a_keep_history_type_that_carries_a_contested_index() { let schema = platform_value!({ "type": "object", "properties": { - "label": { - "type": "string", - "maxLength": 50, - "position": 0, - }, + "label": {"type": "string", "maxLength": 50, "position": 0}, }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], "additionalProperties": false, + "documentsMutable": false, "documentsKeepHistory": true, "canBeDeleted": false, }); - let document_type = parse(schema) - .expect("documentsKeepHistory: true + canBeDeleted: false is consistent and must parse"); - assert!(document_type.documents_keep_history()); - assert!(!document_type.documents_can_be_deleted()); + let error = parse(schema).expect_err("a contested keep-history type must be refused"); + let message = format!("{error:?}"); + assert!( + message.contains("contested"), + "the error must say which index is the problem; got {message}" + ); } -/// Symmetric guard: `canBeDeleted: true` on a non-keep-history -/// doctype must continue to parse cleanly. Catches a predicate that -/// triggers on `canBeDeleted: true` alone instead of the AND. +/// The same index without history is unaffected: the refusal is about the +/// combination, not about contested indexes. #[test] -fn doctype_can_be_deleted_without_keep_history_accepted() { +fn should_accept_a_contested_index_without_history() { let schema = platform_value!({ "type": "object", "properties": { - "label": { - "type": "string", - "maxLength": 50, - "position": 0, - }, + "label": {"type": "string", "maxLength": 50, "position": 0}, }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], "additionalProperties": false, - "canBeDeleted": true, + "documentsMutable": false, }); - let document_type = - parse(schema).expect("canBeDeleted: true without documentsKeepHistory must parse cleanly"); - assert!(!document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); -} -#[test] -fn should_accept_contradictory_keep_history_schema_at_protocol_13() { - let document_type = parse_at_version(keep_history_deletable_schema(), 13, true) - .expect("released protocol 13 must still accept the schema"); - assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); + parse(schema).expect("a contested index alone is fine"); } fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { @@ -217,46 +251,30 @@ fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { }) } +/// A keep-history type may withdraw deletion, and only in that direction. #[test] -fn should_repair_legacy_keep_history_delete_flag_at_protocol_14() { - let legacy_schemas = [ - repair_schema(true, true), - platform_value!({ - "type": "object", - "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "additionalProperties": false, - "documentsKeepHistory": true, - }), - ]; - for schema in legacy_schemas { - let old = parse_at_version(schema, 13, true).unwrap(); - let repaired = parse_at_version(repair_schema(true, false), 14, true).unwrap(); - let result = old - .as_ref() - .validate_update(repaired.as_ref(), 2, PlatformVersion::get(14).unwrap()) - .expect("repair must reach a consensus result"); - assert!(result.is_valid(), "repair rejected: {:?}", result.errors); - } +fn should_allow_a_keep_history_type_to_withdraw_deletion_at_protocol_14() { + let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); + let new = parse_at_version(repair_schema(true, false), 14, true).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) + .expect("the update must reach a consensus result"); + assert!(result.is_valid(), "rejected: {:?}", result.errors); } #[test] -fn should_preserve_legacy_keep_history_repair_rejection_through_protocol_13() { +fn should_preserve_the_immutable_delete_flag_through_protocol_13() { for protocol in [12, 13] { let old = parse_at_version(repair_schema(true, true), protocol, true).unwrap(); - let repaired = parse_at_version(repair_schema(true, false), protocol, true).unwrap(); + let new = parse_at_version(repair_schema(true, false), protocol, true).unwrap(); let result = old .as_ref() - .validate_update( - repaired.as_ref(), - 2, - PlatformVersion::get(protocol).unwrap(), - ) + .validate_update(new.as_ref(), 2, PlatformVersion::get(protocol).unwrap()) .unwrap(); assert!( !result.is_valid(), - "protocol {protocol} must still reject repair" + "protocol {protocol} must still refuse the change" ); } } @@ -285,8 +303,48 @@ fn should_reject_other_delete_and_history_flag_changes_at_protocol_14() { } } +/// Erasability is immutable in both directions. Widening it hands an +/// irreversible operation to a type registered without it; narrowing it after a +/// first chunk has removed revisions strands a partially erased document. +#[test] +fn should_reject_every_change_to_the_erasure_flag() { + for (before, after) in [(false, true), (true, false)] { + let old = parse_at_version(erasable_schema(true, true, before), 14, false).unwrap(); + let new = parse_at_version(erasable_schema(true, true, after), 14, false).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "unexpectedly accepted canBeErased {before} -> {after}" + ); + } +} + +/// An erasable type can never stop being deletable, whichever way the update is +/// spelled: erasure applies to deleted documents only, and since erasability +/// itself cannot be withdrawn, such a type could never reach a state erasure +/// acts on again. Keeping the flag makes the type unparseable; dropping it +/// changes an immutable flag. +#[test] +fn should_refuse_to_withdraw_deletion_from_an_erasable_type() { + assert!( + parse_at_version(erasable_schema(true, false, true), 14, false).is_err(), + "an erasable type that forbids deletion is not a valid type at all" + ); + + let old = parse_at_version(erasable_schema(true, true, true), 14, false).unwrap(); + let new = parse_at_version(repair_schema(true, false), 14, false).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) + .unwrap(); + assert!(!result.is_valid(), "an erasable type kept its delete flag"); +} + #[test] -fn should_reject_incompatible_properties_during_keep_history_repair() { +fn should_reject_incompatible_properties_during_a_delete_flag_withdrawal() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); let new = parse_at_version( platform_value!({ @@ -308,12 +366,12 @@ fn should_reject_incompatible_properties_during_keep_history_repair() { .unwrap(); assert!( !result.is_valid(), - "repair must not bypass schema compatibility" + "the withdrawal must not bypass schema compatibility" ); } #[test] -fn should_reject_mutability_change_during_keep_history_repair() { +fn should_reject_mutability_change_during_a_delete_flag_withdrawal() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); let mut schema = repair_schema(true, false); schema.set_value("documentsMutable", false.into()).unwrap(); @@ -324,12 +382,12 @@ fn should_reject_mutability_change_during_keep_history_repair() { .unwrap(); assert!( !result.is_valid(), - "repair must not bypass other configuration checks" + "the withdrawal must not bypass other configuration checks" ); } #[test] -fn should_still_validate_property_named_can_be_deleted_during_keep_history_repair() { +fn should_still_validate_a_property_named_can_be_deleted_during_a_withdrawal() { let mut old_schema = repair_schema(true, true); old_schema .set_value( diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs index 8a826b25afa..cdd88a782e5 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs @@ -24,10 +24,6 @@ impl Drive { /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document, which must keep history. /// * `block_info`: The block this erase belongs to. - /// * `start`: Whether this is the authorized first chunk, which commits the - /// document to erasure, rather than a continuation of one already - /// committed. Derived from the document's committed lifecycle, never from - /// the transition. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. /// * `platform_version`: The platform version to select the correct function version to run. @@ -42,7 +38,6 @@ impl Drive { contract: &DataContract, document_type: DocumentTypeRef, block_info: &BlockInfo, - start: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -61,7 +56,6 @@ impl Drive { contract, document_type, block_info, - start, estimated_costs_only_with_layer_info, transaction, platform_version, diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs index 81f488b018c..72ad713d100 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -61,7 +61,6 @@ impl Drive { contract: &DataContract, document_type: DocumentTypeRef, block_info: &BlockInfo, - start: bool, estimated_costs_only_with_layer_info: &mut Option< HashMap, >, @@ -181,7 +180,7 @@ impl Drive { &mut batch_operations, &platform_version.drive, )?; - } else if start { + } else { let (record, flags) = self.fetch_lifecycle_record_with_flags( document_id, &lifecycle_path, @@ -189,6 +188,12 @@ impl Drive { transaction, platform_version, )?; + if record.is_erasing() { + // A continuation: the record already carries the erasure this + // chunk is finishing, and nothing but an authorized first chunk + // may write those fields. + return Ok(batch_operations); + } let (time_ms, revision) = Self::decode_revision_key(&removable[0])?; // Equal in size to the record it replaces, and carrying the same // flags, so the deleter stays the beneficiary of its bytes. diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs new file mode 100644 index 00000000000..0182f23bdec --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs @@ -0,0 +1,62 @@ +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; + +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; +use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; + +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; +use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for DocumentEraseTransitionAction { + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .document_erase_transition + { + 0 => { + let base = self.base_owned(); + let data_contract_id = base.data_contract_id(); + let identity_contract_nonce = base.identity_contract_nonce(); + + // No token operation: erase carries no token cost, and its + // structure validation refuses a transition that offers one. + Ok(vec![ + IdentityOperation(IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }), + DocumentOperation(DocumentOperationType::EraseDocument { + document_id: base.id(), + contract_info: DataContractInfo::DataContractFetchInfo( + base.data_contract_fetch_info(), + ), + document_type_info: DocumentTypeInfo::DocumentTypeName( + base.document_type_name_owned(), + ), + }), + ]) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "DocumentEraseTransitionAction::into_high_level_document_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs index 6a6deca3152..2178cdb16a3 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs @@ -56,6 +56,13 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransitionAction { platform_version, ) } + DocumentTransitionAction::EraseAction(document_erase_transition) => { + document_erase_transition.into_high_level_batch_drive_operations( + epoch, + owner_id, + platform_version, + ) + } DocumentTransitionAction::IndexOnlyDeleteAction( document_index_only_delete_transition, ) => document_index_only_delete_transition.into_high_level_batch_drive_operations( diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/mod.rs index e18cf20ed34..b8c70bd80c2 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/mod.rs @@ -1,5 +1,6 @@ mod document_create_transition; mod document_delete_transition; +mod document_erase_transition; mod document_index_only_delete_transition; mod document_purchase_transition; mod document_replace_transition; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/mod.rs new file mode 100644 index 00000000000..31de99fc39b --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/mod.rs @@ -0,0 +1,31 @@ +use derive_more::From; + +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::{DocumentEraseTransitionActionAccessorsV0, DocumentEraseTransitionActionV0}; + +/// transformer +pub mod transformer; +/// v0 +pub mod v0; + +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; + +/// document erase transition action +#[derive(Debug, Clone, From)] +pub enum DocumentEraseTransitionAction { + /// v0 + V0(DocumentEraseTransitionActionV0), +} + +impl DocumentEraseTransitionActionAccessorsV0 for DocumentEraseTransitionAction { + fn base(&self) -> &DocumentBaseTransitionAction { + match self { + DocumentEraseTransitionAction::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> DocumentBaseTransitionAction { + match self { + DocumentEraseTransitionAction::V0(v0) => v0.base, + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs new file mode 100644 index 00000000000..d603791944a --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs @@ -0,0 +1,37 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::{DocumentEraseTransitionAction, DocumentEraseTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::batched_transition::DocumentEraseTransition; +use dpp::ProtocolError; +use std::sync::Arc; + +impl DocumentEraseTransitionAction { + /// from borrowed + pub fn try_from_document_borrowed_erase_transition_with_contract_lookup( + value: &DocumentEraseTransition, + owner_id: Identifier, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + DocumentEraseTransition::V0(v0) => { + DocumentEraseTransitionActionV0::try_from_borrowed_document_erase_transition_with_contract_lookup( + v0, + owner_id, + user_fee_increase, + get_data_contract, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/mod.rs new file mode 100644 index 00000000000..6b9805f40c1 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/mod.rs @@ -0,0 +1,19 @@ +/// transformer +pub mod transformer; + +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; + +#[derive(Debug, Clone)] +/// document erase transition action v0 +pub struct DocumentEraseTransitionActionV0 { + /// base + pub base: DocumentBaseTransitionAction, +} + +/// document erase transition action accessors v0 +pub trait DocumentEraseTransitionActionAccessorsV0 { + /// base + fn base(&self) -> &DocumentBaseTransitionAction; + /// base owned + fn base_owned(self) -> DocumentBaseTransitionAction; +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..ac575d0f0b1 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs @@ -0,0 +1,71 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; +use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; +use dpp::ProtocolError; +use std::sync::Arc; + +impl DocumentEraseTransitionActionV0 { + /// try from borrowed + pub fn try_from_borrowed_document_erase_transition_with_contract_lookup( + value: &DocumentEraseTransitionV0, + owner_id: Identifier, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let DocumentEraseTransitionV0 { base, .. } = value; + + let base_action_validation_result = + DocumentBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + base, + get_data_contract, + // Erase carries no token cost: the deletion it follows was + // charged when the document was deleted. + |_document_type| None, + "erase", + )?; + + let base = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + FeeResult::default(), + )); + } + }; + + Ok(( + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::EraseAction( + DocumentEraseTransitionActionV0 { base }.into(), + )) + .into(), + FeeResult::default(), + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs index 4a36b30372a..3a88a2ab849 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs @@ -8,6 +8,7 @@ impl DocumentTransitionActionTypeGetter for DocumentTransitionAction { match self { DocumentTransitionAction::CreateAction(_) => DocumentTransitionActionType::Create, DocumentTransitionAction::DeleteAction(_) => DocumentTransitionActionType::Delete, + DocumentTransitionAction::EraseAction(_) => DocumentTransitionActionType::Erase, DocumentTransitionAction::ReplaceAction(_) => DocumentTransitionActionType::Replace, DocumentTransitionAction::TransferAction(_) => DocumentTransitionActionType::Transfer, DocumentTransitionAction::PurchaseAction(_) => DocumentTransitionActionType::Purchase, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs index 3d696150e40..65cbdff1911 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs @@ -4,6 +4,8 @@ pub mod document_base_transition_action; pub mod document_create_transition_action; /// document_delete_transition_action pub mod document_delete_transition_action; +/// document_erase_transition_action +pub mod document_erase_transition_action; /// document_index_only_delete_transition_action pub mod document_index_only_delete_transition_action; /// document_purchase_transition_action @@ -26,6 +28,8 @@ use crate::state_transition_action::batch::batched_transition::document_transiti use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::{DocumentTransferTransitionAction, DocumentTransferTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::{DocumentUpdatePriceTransitionAction, DocumentUpdatePriceTransitionActionAccessorsV0}; @@ -52,6 +56,9 @@ pub enum DocumentTransitionAction { /// indexOnly delete-by-values — carries the document's property /// values, since there is no primary-storage row to fetch them from IndexOnlyDeleteAction(DocumentIndexOnlyDeleteTransitionAction), + /// Removes the retained revisions of an already deleted keep-history + /// document, a chunk at a time. + EraseAction(DocumentEraseTransitionAction), } impl DocumentTransitionAction { @@ -65,6 +72,7 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base(), DocumentTransitionAction::UpdatePriceAction(d) => d.base(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base(), + DocumentTransitionAction::EraseAction(d) => d.base(), } } @@ -78,6 +86,7 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base_owned(), DocumentTransitionAction::UpdatePriceAction(d) => d.base_owned(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base_owned(), + DocumentTransitionAction::EraseAction(d) => d.base_owned(), } } } 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 541fdae5552..1fb0ff2fa22 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 @@ -113,11 +113,6 @@ pub enum DocumentOperationType<'a> { EraseDocument { /// The document id document_id: Identifier, - /// Whether this is the authorized first chunk, which commits the - /// document to erasure, rather than a continuation of one already - /// committed. Derived from the document's committed lifecycle, never - /// from the transition. - start: bool, /// Data Contract info to potentially be resolved if needed contract_info: DataContractInfo<'a>, /// Document type @@ -471,7 +466,6 @@ impl DocumentOperationType<'_> { } DocumentOperationType::EraseDocument { document_id, - start, contract_info, document_type_info, } => { @@ -491,7 +485,6 @@ impl DocumentOperationType<'_> { contract, document_type, block_info, - start, estimated_costs_only_with_layer_info, transaction, platform_version, From 0b156bb1a438641942422f4544bf2eea7c7d092a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:05:52 +0700 Subject: [PATCH 06/48] feat(drive-abci): validate the document lifecycle before deleting or erasing Deleting a keep-history document now consults its lifecycle rather than the ordinary by-id read, which cannot see a document that has already been deleted. A second delete is therefore a paid consensus error rather than a delete of something that never existed, and no delete has any path to removing a revision. Erasing is authorized once. Its structure validation checks what the contract alone decides: the type retains revisions, allows them to be purged, carries no contested index, and offers no token payment, which erase has no use for since the deletion it follows was already charged. Its state validation then reads the document's lifecycle: a current document must be deleted first, a merely deleted one may be committed to erasure only by its owner, and an erasure already committed may be continued by any identity, because the record left in state is itself the evidence that destruction was authorized. An owner who loses their keys, their funds or their permission can no longer strand a half-erased document. Creating over a deleted document's id is refused: the id stays reserved while its revisions are retained, and becomes free again once an erase has removed the last one. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_v2/mod.rs | 41 +++++- .../advanced_structure_v1/mod.rs | 33 ++++- .../document_delete_transition_action/mod.rs | 12 +- .../state_v1/mod.rs | 134 ++++++++++++++++++ .../advanced_structure_v0/mod.rs | 87 ++++++++++++ .../document_erase_transition_action/mod.rs | 86 +++++++++++ .../state_v0/mod.rs | 126 ++++++++++++++++ .../batch/action_validation/document/mod.rs | 1 + .../batch/advanced_structure/v0/mod.rs | 15 ++ .../batch/state/v0/fetch_documents.rs | 49 +++++++ .../state_transitions/batch/state/v0/mod.rs | 10 ++ .../batch/transformer/v0/mod.rs | 11 ++ .../src/query/document_history/v0/mod.rs | 6 + .../verify_state_transitions.rs | 8 ++ 14 files changed, 610 insertions(+), 9 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs index 177acedc9da..782dd41052f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs @@ -1,7 +1,10 @@ use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::InvalidDocumentTypeError; use dpp::consensus::state::document::document_contest_document_with_same_id_already_present_error::DocumentContestDocumentWithSameIdAlreadyPresentError; +use dpp::consensus::state::document::document_already_present_error::DocumentAlreadyPresentError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::identifier::Identifier; use dpp::validation::{ConsensusValidationResult, SimpleConsensusValidationResult}; @@ -19,7 +22,9 @@ use crate::execution::types::state_transition_execution_context::{ }; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::DocumentReferenceValidation; -use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::has_contested_document_with_document_id; +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::{ + fetch_keep_history_document_lifecycle, has_contested_document_with_document_id, +}; use crate::platform_types::platform::PlatformStateRef; pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStateValidationV2 @@ -57,6 +62,40 @@ impl DocumentCreateTransitionActionStateValidationV2 for DocumentCreateTransitio return Ok(validation_result); } + // A keep-history document that has been deleted keeps its revisions and + // keeps its id: nothing in the primary-key tree marks the id as taken, + // so v1's by-id read cannot see the reservation. Creating over it would + // append a new document's revision to a deleted one's record. The id + // becomes free again once an erase has removed the last revision. + let contract_fetch_info = self.base().data_contract_fetch_info(); + let contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + let Some(document_type) = contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), + )); + }; + if document_type.documents_keep_history() { + let lifecycle = fetch_keep_history_document_lifecycle( + platform.drive, + contract, + document_type, + self.base().id(), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )?; + if lifecycle.is_present() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentAlreadyPresentError( + DocumentAlreadyPresentError::new(self.base().id()), + )), + )); + } + } + let reference_result = self.base().validate_document_references( self.data(), None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs index b90d4f2a309..199e88b0850 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs @@ -12,10 +12,15 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: fn validate_structure_v1(&self) -> Result; } impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTransitionAction { - /// Protocol 14 checks both deletion permission and history retention before - /// executing a delete. Legacy history-bearing types may advertise deletion, - /// but Drive refuses it; return the usual paid consensus rejection here. - /// V0 retains the historical InternalError outcome for protocol 13 and earlier. + /// From protocol 14 a keep-history type takes part in the document + /// lifecycle: a delete removes the document from ordinary reads and leaves + /// its revisions readable. What v1 adds over v0 is the refusal of a + /// keep-history type that carries a contested index, which the parser + /// rejects for new contracts but which a contract stored before protocol 14 + /// could still declare. A contested resource is awarded outside transition + /// validation, at an id derived from the winner rather than from the + /// contested values, so that award can land on an id whose retained history + /// already exists. fn validate_structure_v1(&self) -> Result { let contract_fetch_info = self.base().data_contract_fetch_info(); let data_contract = &contract_fetch_info.contract; @@ -30,9 +35,7 @@ impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTrans )); }; - // A legacy contract may enable both flags, but retaining history always - // prevents deletion, regardless of the advertised canBeDeleted value. - if !document_type.documents_can_be_deleted() || document_type.documents_keep_history() { + if !document_type.documents_can_be_deleted() { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidDocumentTransitionActionError::new(format!( "documents of type {} can not be deleted", @@ -42,6 +45,22 @@ impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTrans )); } + if document_type.documents_keep_history() + && document_type + .indexes() + .values() + .any(|index| index.contested_index.is_some()) + { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of keep-history type {} carry a contested index and can not take \ + part in the document lifecycle", + document_type_name + )) + .into(), + )); + } + // Pair the delete KIND with the doctype's storage mode: an // indexOnly document has no primary-storage row a by-id delete // could fetch, so its deletes must come as the indexOnlyDelete diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs index 35b5210125a..0d872a96c59 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs @@ -8,6 +8,7 @@ use crate::error::Error; use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::state_v0::DocumentDeleteTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::state_v1::DocumentDeleteTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::advanced_structure_v0::DocumentDeleteTransitionActionStructureValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::advanced_structure_v1::DocumentDeleteTransitionActionStructureValidationV1; use crate::platform_types::platform::PlatformStateRef; @@ -15,6 +16,7 @@ use crate::platform_types::platform::PlatformStateRef; mod advanced_structure_v0; mod advanced_structure_v1; mod state_v0; +mod state_v1; pub trait DocumentDeleteTransitionActionValidation { fn validate_structure( @@ -79,9 +81,17 @@ impl DocumentDeleteTransitionActionValidation for DocumentDeleteTransitionAction transaction, platform_version, ), + 1 => self.validate_state_v1( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentDeleteTransitionAction::validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs new file mode 100644 index 00000000000..79fff775a3d --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs @@ -0,0 +1,134 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::state_v0::DocumentDeleteTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_keep_history_document_lifecycle; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::InvalidDocumentTypeError; +use dpp::consensus::state::document::document_not_found_error::DocumentNotFoundError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::document::DocumentV0Getters; +use dpp::identifier::Identifier; +use dpp::prelude::ConsensusValidationResult; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::grovedb::TransactionArg; +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_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; + +use dpp::consensus::state::document::document_owner_id_mismatch_error::DocumentOwnerIdMismatchError; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentDeleteTransitionActionStateValidationV1 { + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentDeleteTransitionActionStateValidationV1 for DocumentDeleteTransitionAction { + /// A keep-history document that has already been deleted is invisible to + /// the ordinary by-id read v0 relies on, so deleting it a second time would + /// otherwise look like deleting a document that never existed. The + /// lifecycle read tells the two apart, and a second delete is refused + /// rather than escalating into anything that removes a revision: only an + /// erase does that, and only after this delete has committed the document + /// to the deleted state. + /// + /// Every other document type is validated exactly as v0 validates it. + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let contract_fetch_info = self.base().data_contract_fetch_info(); + let contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + let Some(document_type) = contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), + )); + }; + + if !document_type.documents_keep_history() { + return self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ); + } + + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + "delete", + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let lifecycle = fetch_keep_history_document_lifecycle( + platform.drive, + contract, + document_type, + self.base().id(), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )?; + + let document = match lifecycle { + DocumentLifecycleState::Active(document) => document, + // A deleted or erasing document is gone from every ordinary read, + // which is exactly what a delete asks for, so asking again is the + // same as asking about an id that holds nothing. + DocumentLifecycleState::Deleted(_) + | DocumentLifecycleState::Erasing + | DocumentLifecycleState::Absent => { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentNotFoundError( + DocumentNotFoundError::new(self.base().id()), + )), + )); + } + }; + + // The single ownership call site for a delete. + if document.owner_id() != owner_id { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentOwnerIdMismatchError( + DocumentOwnerIdMismatchError::new( + self.base().id(), + owner_id, + document.owner_id(), + ), + )), + )); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs new file mode 100644 index 00000000000..5e15b74dcdc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs @@ -0,0 +1,87 @@ +use crate::error::Error; +use dpp::consensus::basic::document::{ + InvalidDocumentTransitionActionError, InvalidDocumentTypeError, +}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::validation::SimpleConsensusValidationResult; +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_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentEraseTransitionActionStructureValidationV0 { + fn validate_structure_v0(&self) -> Result; +} + +impl DocumentEraseTransitionActionStructureValidationV0 for DocumentEraseTransitionAction { + /// Checks what the contract alone decides: whether this type has retained + /// revisions at all, whether it allows them to be purged, and whether it is + /// one of the types the lifecycle keeps out. Whether the particular document + /// is in a state that can be erased is a question about committed state and + /// belongs to state validation. + fn validate_structure_v0(&self) -> Result { + let contract_fetch_info = self.base().data_contract_fetch_info(); + let data_contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + + let Some(document_type) = data_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), data_contract.id()) + .into(), + )); + }; + + if !document_type.documents_keep_history() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of type {} do not keep history and can not be erased", + document_type_name + )) + .into(), + )); + } + + if !document_type.documents_can_be_erased() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of type {} can not be erased", + document_type_name + )) + .into(), + )); + } + + // Defensive, for a contract stored before protocol 14: the parser keeps + // the two apart for every contract registered since. + if document_type + .indexes() + .values() + .any(|index| index.contested_index.is_some()) + { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of keep-history type {} carry a contested index and can not take \ + part in the document lifecycle", + document_type_name + )) + .into(), + )); + } + + // Erase has no token cost of its own: the deletion it follows was paid + // for when the document was deleted. Offering one would let a + // continuation, which anyone may submit, move tokens. + if self.base().token_cost().is_some() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "an erase of a document of type {} must not carry token payment information", + document_type_name + )) + .into(), + )); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs new file mode 100644 index 00000000000..2fac85c999b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs @@ -0,0 +1,86 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::advanced_structure_v0::DocumentEraseTransitionActionStructureValidationV0; +use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::state_v0::DocumentEraseTransitionActionStateValidationV0; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; + +mod advanced_structure_v0; +mod state_v0; + +pub trait DocumentEraseTransitionActionValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; + + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentEraseTransitionActionValidation for DocumentEraseTransitionAction { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_erase_transition_structure_validation + { + 0 => self.validate_structure_v0(), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "DocumentEraseTransitionAction::validate_structure".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_erase_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "DocumentEraseTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..ad2412264c4 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs @@ -0,0 +1,126 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_keep_history_document_lifecycle; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::{ + InvalidDocumentTransitionActionError, InvalidDocumentTypeError, +}; +use dpp::consensus::state::document::document_not_found_error::DocumentNotFoundError; +use dpp::consensus::state::document::document_owner_id_mismatch_error::DocumentOwnerIdMismatchError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::DocumentV0Getters; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::grovedb::TransactionArg; +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_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentEraseTransitionActionStateValidationV0 { + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentEraseTransitionActionStateValidationV0 for DocumentEraseTransitionAction { + /// An erasure is authorized once, when it starts. + /// + /// A document that is merely deleted may only be committed to erasure by + /// its owner. Once committed, the record left in state is itself the + /// evidence that destruction was authorized, so any identity may submit the + /// remaining chunks and pay for them: an owner who loses their keys, their + /// funds or their permission can no longer strand a half-erased document. + /// A document that is still current cannot be erased at all — deleting it + /// is a separate intent, with its own permission and its own token cost. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + "erase", + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let contract_fetch_info = self.base().data_contract_fetch_info(); + let contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + let Some(document_type) = contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), + )); + }; + + let lifecycle = fetch_keep_history_document_lifecycle( + platform.drive, + contract, + document_type, + self.base().id(), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )?; + + match lifecycle { + DocumentLifecycleState::Active(_) => { + Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "document {} must be deleted before it can be erased", + self.base().id() + )) + .into(), + )) + } + // The single ownership call site for an erase. + DocumentLifecycleState::Deleted(newest) => { + if newest.owner_id() != owner_id { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentOwnerIdMismatchError( + DocumentOwnerIdMismatchError::new( + self.base().id(), + owner_id, + newest.owner_id(), + ), + )), + )); + } + Ok(SimpleConsensusValidationResult::new()) + } + // A continuation of an erasure that is already committed: nothing + // to authorize, because the committed record already carries the + // authorization. + DocumentLifecycleState::Erasing => Ok(SimpleConsensusValidationResult::new()), + DocumentLifecycleState::Absent => Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentNotFoundError( + DocumentNotFoundError::new(self.base().id()), + )), + )), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs index 3a4130219e7..57f2d3199ad 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs @@ -1,6 +1,7 @@ mod document_base_transaction_action; pub(crate) mod document_create_transition_action; pub(crate) mod document_delete_transition_action; +pub(crate) mod document_erase_transition_action; pub(crate) mod document_index_only_delete_transition_action; pub(crate) mod document_purchase_transition_action; pub(crate) mod document_reference_validation; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs index 0eafc05f797..0fc65514003 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs @@ -20,11 +20,13 @@ use dpp::version::PlatformVersion; use drive::state_transition_action::batch::BatchTransitionAction; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionActionAccessorsV0; @@ -217,6 +219,19 @@ impl DocumentsBatchStateTransitionStructureValidationV0 for BatchTransition { )); } } + DocumentTransitionAction::EraseAction(erase_action) => { + let result = erase_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(erase_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { let result = index_only_delete_action.validate_structure(platform_version)?; 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 2486f1574c4..006e90db6d6 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 @@ -14,6 +14,7 @@ use dpp::state_transition::batch_transition::batched_transition::document_transi }; use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; +use drive::drive::document::lifecycle::DocumentLifecycleState; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::drive::Drive; @@ -213,6 +214,54 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( // fetch_document_with_id // ============================================================================ +/// Versioned facade for `fetch_keep_history_document_lifecycle`. +/// +/// Classifies one keep-history document as active, deleted, erasing or absent +/// and bills the reads it performed. Every stateful check that has to tell a +/// deleted document from one that never existed goes through this; a document +/// type that keeps no history has no lifecycle to read and must not reach it. +pub(crate) fn fetch_keep_history_document_lifecycle( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_keep_history_document_lifecycle + { + 0 => { + let (state, fee_result) = drive + .fetch_document_lifecycle( + contract, + document_type, + id, + Some(epoch), + transaction, + platform_version, + ) + .map_err(Error::Drive)?; + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(state) + } + version => Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "fetch_keep_history_document_lifecycle".to_string(), + known_versions: vec![0], + received: version, + }, + )), + } +} + /// Versioned facade for `fetch_document_with_id`. /// /// Dispatches on the per-helper version field on diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 4fd93181846..c2de31db690 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -17,6 +17,7 @@ use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; @@ -144,6 +145,15 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, + DocumentTransitionAction::EraseAction(erase_action) => erase_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { index_only_delete_action.validate_state( platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 01480cab7fe..69b2943f072 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -56,6 +56,7 @@ use dpp::state_transition::batch_transition::batched_transition::document_purcha use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionAction; use drive::state_transition_action::batch::BatchTransitionAction; @@ -866,6 +867,16 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } + DocumentTransition::Erase(document_erase_transition) => { + let (batched_action, fee_result) = DocumentEraseTransitionAction::try_from_document_borrowed_erase_transition_with_contract_lookup(document_erase_transition, owner_id, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + })?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } DocumentTransition::Transfer(document_transfer_transition) => { let validation_result = Self::find_replaced_document_v0(transition, replaced_documents); diff --git a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs index 94e1505bda1..5eca888f9f5 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs @@ -138,9 +138,15 @@ impl Platform { lifecycle: history.lifecycle.map(|lifecycle| Lifecycle { state: match lifecycle.state { DocumentHistoryState::Active => State::Active, + DocumentHistoryState::Deleted => State::Deleted, + DocumentHistoryState::Erasing => State::Erasing, DocumentHistoryState::Absent => State::Absent, } as i32, remaining_revisions: lifecycle.remaining_revisions, + deleted_at_ms: history.lifecycle.times.deleted_at_ms, + erasing_started_at_ms: history.lifecycle.times.erasing_started_at_ms, + erasing_from_time_ms: history.lifecycle.times.erasing_from_time_ms, + erasing_from_revision: history.lifecycle.times.erasing_from_revision, }), }) }; diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index c45020d0a2b..fb51b793486 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -613,6 +613,14 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( // we expect no document assert!(document.is_none()); } + DocumentTransitionAction::EraseAction(_) => { + // An erase acts on a document that was + // already deleted, so the by-id query this + // harness proves against returns nothing + // whether or not the erase executed; + // strategies do not generate erases today. + assert!(document.is_none()); + } DocumentTransitionAction::IndexOnlyDeleteAction(_) => { // indexOnly documents never have a // primary-storage row, so the by-id query From da53e46d6231735a26567425ad6d2d012f4d0af2 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:05:52 +0700 Subject: [PATCH 07/48] feat(sdk): report the deleted and erasing states through the history query The history response's lifecycle block gains the two new states and the four times the lifecycle record carries. The proof verifier checks every one of them against the proof, so a node cannot claim a deletion time or an erasure that its own state does not authenticate. The batch action enums in both WebAssembly bindings gain the erase kind, keeping the two hand-written discriminant tables in agreement. Co-Authored-By: Claude Opus 5 (1M context) --- .../protos/platform/v0/platform.proto | 11 ++++++ packages/rs-sdk/src/mock/requests.rs | 1 + packages/rs-sdk/src/platform/query.rs | 1 + .../document_transition/mod.rs | 3 ++ .../wasm-dpp2/src/enums/batch/batch_enum.rs | 5 +++ .../batch/document_transition.rs | 1 + packages/wasm-sdk/src/queries/document.rs | 39 ++++++++++++++++++- 7 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 4ac2d07bf38..50b9cb615f5 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -1857,10 +1857,21 @@ message GetDocumentHistoryResponse { enum State { ACTIVE = 0; ABSENT = 1; + DELETED = 2; + ERASING = 3; } State state = 1; // Revisions the history still retains for the document. uint64 remaining_revisions = 2 [ jstype = JS_STRING ]; + // Zero unless the document has been deleted. + uint64 deleted_at_ms = 3 [ jstype = JS_STRING ]; + // Zero unless an authorized erasure has started. + uint64 erasing_started_at_ms = 4 [ jstype = JS_STRING ]; + // Timestamp of the newest revision retained when the erasure started. + uint64 erasing_from_time_ms = 5 [ jstype = JS_STRING ]; + // History sequence of the newest revision retained when the erasure + // started. + uint64 erasing_from_revision = 6 [ jstype = JS_STRING ]; } // A page of retained revisions and the document's lifecycle as of the // same block. diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 1bbf0fe6c15..902a9559792 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -308,6 +308,7 @@ impl MockResponse for drive_proof_verifier::types::DocumentHistory { DocumentHistoryState::Absent }, remaining_revisions, + times: Default::default(), }), } } diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 1e5e1044656..48a25b23613 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -1484,6 +1484,7 @@ mod history_query_tests { lifecycle: Some(DocumentHistoryLifecycle { state: DocumentHistoryState::Active, remaining_revisions: 2, + times: Default::default(), }), }; sdk.mock() diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs index 89e0e5df571..1a5acf13908 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs @@ -68,6 +68,7 @@ impl DocumentTransitionWasm { DocumentTransition::Transfer(_) => JsValue::null(), DocumentTransition::UpdatePrice(_) => JsValue::null(), DocumentTransition::Purchase(_) => JsValue::null(), + DocumentTransition::Erase(_) => JsValue::null(), DocumentTransition::IndexOnlyDelete(index_only_delete) => { let json_value = index_only_delete.data().to_json_value().unwrap(); json_value @@ -118,6 +119,7 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(update_price) => Some(update_price.price()), DocumentTransition::Purchase(purchase) => Some(purchase.price()), DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -131,6 +133,7 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } diff --git a/packages/wasm-dpp2/src/enums/batch/batch_enum.rs b/packages/wasm-dpp2/src/enums/batch/batch_enum.rs index 130c0740906..58ccca1bc70 100644 --- a/packages/wasm-dpp2/src/enums/batch/batch_enum.rs +++ b/packages/wasm-dpp2/src/enums/batch/batch_enum.rs @@ -13,6 +13,7 @@ pub enum BatchTypeWasm { UpdatePrice, IgnoreWhileBumpingRevision, IndexOnlyDelete, + Erase, } impl TryFrom<&JsValue> for BatchTypeWasm { @@ -33,6 +34,7 @@ impl TryFrom<&JsValue> for BatchTypeWasm { "updateprice" => Ok(BatchTypeWasm::UpdatePrice), "ignorewhilebumpingrevision" => Ok(BatchTypeWasm::IgnoreWhileBumpingRevision), "indexonlydelete" => Ok(BatchTypeWasm::IndexOnlyDelete), + "erase" => Ok(BatchTypeWasm::Erase), _ => Err(WasmDppError::invalid_argument(format!( "unknown batch type value: {}", enum_val @@ -53,6 +55,7 @@ impl TryFrom<&JsValue> for BatchTypeWasm { 5 => Ok(BatchTypeWasm::UpdatePrice), 6 => Ok(BatchTypeWasm::IgnoreWhileBumpingRevision), 7 => Ok(BatchTypeWasm::IndexOnlyDelete), + 8 => Ok(BatchTypeWasm::Erase), _ => Err(WasmDppError::invalid_argument(format!( "unknown batch type value: {}", enum_val @@ -82,6 +85,7 @@ impl From for String { BatchTypeWasm::UpdatePrice => String::from("updatePrice"), BatchTypeWasm::IgnoreWhileBumpingRevision => String::from("ignoreWhileBumpingRevision"), BatchTypeWasm::IndexOnlyDelete => String::from("indexOnlyDelete"), + BatchTypeWasm::Erase => String::from("erase"), } } } @@ -99,6 +103,7 @@ impl From for BatchTypeWasm { BatchTypeWasm::IgnoreWhileBumpingRevision } DocumentTransitionActionType::IndexOnlyDelete => BatchTypeWasm::IndexOnlyDelete, + DocumentTransitionActionType::Erase => BatchTypeWasm::Erase, } } } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs index 5b022185001..1e5cce18f0b 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs @@ -53,6 +53,7 @@ impl DocumentTransitionWasm { DocumentTransitionActionType::UpdatePrice => 5, DocumentTransitionActionType::IgnoreWhileBumpingRevision => 6, DocumentTransitionActionType::IndexOnlyDelete => 7, + DocumentTransitionActionType::Erase => 8, } } diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index e1aa60f724f..2ab8d5d61b9 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -307,7 +307,6 @@ impl DocumentHistoryEntryWasm { pub fn time_ms(&self) -> BigInt { BigInt::from(self.time_ms) } - #[wasm_bindgen(getter)] pub fn revision(&self) -> BigInt { BigInt::from(self.revision) @@ -340,6 +339,10 @@ impl DocumentHistoryEntryWasm { pub struct DocumentHistoryLifecycleWasm { state: String, remaining_revisions: u64, + deleted_at_ms: u64, + erasing_started_at_ms: u64, + erasing_from_time_ms: u64, + erasing_from_revision: u64, } #[derive(Serialize)] @@ -347,6 +350,10 @@ pub struct DocumentHistoryLifecycleWasm { struct DocumentHistoryLifecycleSerde { state: String, remaining_revisions: String, + deleted_at_ms: String, + erasing_started_at_ms: String, + erasing_from_time_ms: String, + erasing_from_revision: String, } #[wasm_bindgen(js_class = DocumentHistoryLifecycle)] @@ -361,6 +368,26 @@ impl DocumentHistoryLifecycleWasm { BigInt::from(self.remaining_revisions) } + #[wasm_bindgen(getter = "deletedAtMs")] + pub fn deleted_at_ms(&self) -> BigInt { + BigInt::from(self.deleted_at_ms) + } + + #[wasm_bindgen(getter = "erasingStartedAtMs")] + pub fn erasing_started_at_ms(&self) -> BigInt { + BigInt::from(self.erasing_started_at_ms) + } + + #[wasm_bindgen(getter = "erasingFromTimeMs")] + pub fn erasing_from_time_ms(&self) -> BigInt { + BigInt::from(self.erasing_from_time_ms) + } + + #[wasm_bindgen(getter = "erasingFromRevision")] + pub fn erasing_from_revision(&self) -> BigInt { + BigInt::from(self.erasing_from_revision) + } + #[wasm_bindgen(js_name = toJSON)] pub fn to_json(&self) -> Result { serialization::to_json(&self.to_serde()).map_err(WasmSdkError::from) @@ -372,6 +399,10 @@ impl DocumentHistoryLifecycleWasm { DocumentHistoryLifecycleSerde { state: self.state.clone(), remaining_revisions: self.remaining_revisions.to_string(), + deleted_at_ms: self.deleted_at_ms.to_string(), + erasing_started_at_ms: self.erasing_started_at_ms.to_string(), + erasing_from_time_ms: self.erasing_from_time_ms.to_string(), + erasing_from_revision: self.erasing_from_revision.to_string(), } } } @@ -452,10 +483,16 @@ impl DocumentHistoryResultWasm { .map(|lifecycle| DocumentHistoryLifecycleWasm { state: match lifecycle.state { DocumentHistoryState::Active => "ACTIVE", + DocumentHistoryState::Deleted => "DELETED", + DocumentHistoryState::Erasing => "ERASING", DocumentHistoryState::Absent => "ABSENT", } .to_owned(), remaining_revisions: lifecycle.remaining_revisions, + deleted_at_ms: lifecycle.times.deleted_at_ms, + erasing_started_at_ms: lifecycle.times.erasing_started_at_ms, + erasing_from_time_ms: lifecycle.times.erasing_from_time_ms, + erasing_from_revision: lifecycle.times.erasing_from_revision, }), }) } From 0fb3095a213344ffe160ee18aee92c4c030cbec3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:29:02 +0700 Subject: [PATCH 08/48] fix(drive): write the lifecycle record from a fully known element The batch insert has no support for an element of unknown size, so a dry run of a keep-history delete failed outright. It never needed one: the record is a fixed-width encoding of the deletion time and the deleter is an input to the delete, so both runs write the same element. The erase dry run, which genuinely cannot know the record it overwrites, prices one of the right shape instead. Test: at protocol 14 a keep-history delete now runs end to end through the batch pipeline, leaving the document invisible to ordinary reads and its retained revisions readable. Red before this fix with "inserting unsized documents into a batch is not currently supported", green after. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/document/deletion.rs | 89 ++++++++++++------- .../v1/mod.rs | 44 +++------ .../v0/mod.rs | 20 ++--- .../drive/document/lifecycle/fetch/v0/mod.rs | 1 - 4 files changed, 81 insertions(+), 73 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index 5d9165a286b..dce96efab4d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -5,6 +5,9 @@ mod deletion_tests { use crate::execution::validation::state_transition::tests::create_card_game_internal_token_contract_with_owner_identity_burn_tokens; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::TokenPaymentInfo; + use drive::drive::document::lifecycle::DocumentLifecycleState; + use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::query::DriveDocumentQuery; #[tokio::test] async fn test_document_delete_on_document_type_that_is_mutable_and_can_be_deleted() { @@ -388,16 +391,16 @@ mod deletion_tests { assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); } - /// PROTOCOL_VERSION_14 rejects deletes against contradictory keep-history - /// document types as invalid-paid consensus errors. + /// Protocol 14 carries the delete out: the document leaves every ordinary + /// read and its retained revisions stay readable. #[tokio::test] - async fn test_document_delete_on_document_type_that_keeps_history_is_rejected_protocol_version_14( - ) { + async fn test_document_delete_on_document_type_that_keeps_history_succeeds_protocol_version_14() + { run_document_delete_on_document_type_that_keeps_history_at_protocol_version(14, true).await; } /// PROTOCOL_VERSION_12 preserves the historical InternalError result for - /// replay compatibility. The keep-history structure guard must not run. + /// replay compatibility. The keep-history delete path must not run. #[tokio::test] async fn test_document_delete_on_document_type_that_keeps_history_replays_protocol_version_12() { @@ -412,12 +415,12 @@ mod deletion_tests { .await; } - /// Exercises an already-deployed contradictory contract at both sides of - /// the v14 validation-version boundary. Loading with `full_validation: - /// false` is intentional: reparsing deployed contracts must remain allowed. + /// Exercises an already-deployed keep-history contract at both sides of the + /// v14 validation-version boundary. Loading with `full_validation: false` + /// is intentional: reparsing deployed contracts must remain allowed. async fn run_document_delete_on_document_type_that_keeps_history_at_protocol_version( protocol_version: dpp::version::ProtocolVersion, - expect_invalid_paid: bool, + expect_success: bool, ) { let platform_version = PlatformVersion::get(protocol_version) .expect("expected platform version for the requested protocol_version"); @@ -428,9 +431,9 @@ mod deletion_tests { let contract_path = "tests/supporting_files/contract/note/note-contract-keep-history-and-can-be-deleted.json"; - // `full_validation: false` bypasses the DPP cross-flag check so the - // intentionally-contradictory fixture loads — mirrors the - // already-deployed-contract scenario this guard is meant to handle. + // `full_validation: false` so the same fixture loads at every protocol + // version under test, including the released ones whose parser + // generation predates the keep-history grammar this contract uses. let note_contract = json_document_to_contract(contract_path, false, platform_version) .expect("expected to get data contract"); platform @@ -461,7 +464,7 @@ mod deletion_tests { ); assert!( note_document_type.documents_can_be_deleted(), - "fixture sanity: doctype must advertise canBeDeleted" + "fixture sanity: doctype must allow deletion" ); let entropy = Bytes32::random_with_rng(&mut rng); @@ -479,9 +482,9 @@ mod deletion_tests { let mut altered_document = document.clone(); altered_document.set_revision(Some(1)); + let deleted_document_id = altered_document.id(); - // Create the document (must succeed — keep-history doctypes accept - // creates, the contradiction only bites at delete time). + // Create the document. let documents_batch_create_transition = BatchTransition::new_document_creation_transition_from_document( document, @@ -526,8 +529,8 @@ mod deletion_tests { .unwrap() .expect("expected to commit transaction"); - // V14 rejects during structure validation; v12 and v13 retain the - // historical InternalError classification for replay. + // V14 deletes; v12 and v13 retain the historical InternalError + // classification for replay. let documents_batch_deletion_transition = BatchTransition::new_document_deletion_transition_from_document( altered_document, @@ -569,13 +572,13 @@ mod deletion_tests { .unwrap() .expect("expected to commit transaction"); + assert_eq!(processing_result.invalid_paid_count(), 0); + assert_eq!(processing_result.invalid_unpaid_count(), 0); assert_eq!( - processing_result.invalid_paid_count(), - usize::from(expect_invalid_paid), - "unexpected invalid-paid classification at protocol version {protocol_version}" + processing_result.valid_count(), + usize::from(expect_success), + "unexpected success classification at protocol version {protocol_version}" ); - assert_eq!(processing_result.invalid_unpaid_count(), 0); - assert_eq!(processing_result.valid_count(), 0); let internal_error_count = processing_result .execution_results() .iter() @@ -583,19 +586,41 @@ mod deletion_tests { .count(); assert_eq!( internal_error_count, - usize::from(!expect_invalid_paid), + usize::from(!expect_success), "unexpected InternalError classification at protocol version {protocol_version}" ); - if expect_invalid_paid { - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::BasicError( - dpp::consensus::basic::BasicError::InvalidDocumentTransitionActionError(error) - ), - .. - }] if error.action() == "documents of type note can not be deleted" + + if expect_success { + // The document is gone from ordinary reads, and its retained + // revisions are not. + let documents = platform + .drive + .query_documents( + DriveDocumentQuery::all_items_query(¬e_contract, note_document_type, None), + None, + false, + None, + Some(protocol_version), + ) + .expect("expected to query documents") + .documents_owned(); + assert!( + documents.is_empty(), + "the deleted document is still visible" ); + + let (lifecycle, _) = platform + .drive + .fetch_document_lifecycle( + ¬e_contract, + note_document_type, + deleted_document_id, + None, + None, + platform_version, + ) + .expect("expected to read the lifecycle"); + assert_matches!(lifecycle, DocumentLifecycleState::Deleted(_)); } } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 0a0a4c19330..35e47d1b6a0 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -6,7 +6,7 @@ use dpp::data_contract::document_type::DocumentTypeRef; use std::collections::HashMap; use crate::drive::constants::DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE; -use crate::drive::document::lifecycle::{DocumentLifecycleRecord, DOCUMENT_LIFECYCLE_RECORD_SIZE}; +use crate::drive::document::lifecycle::DocumentLifecycleRecord; use crate::drive::document::paths::{ contract_documents_primary_key_path, document_lifecycle_path, DOCUMENT_LIFECYCLE_TREE_KEY, }; @@ -23,9 +23,7 @@ use dpp::document::Document; use crate::drive::Drive; use crate::util::grove_operations::QueryTarget::QueryTargetValue; use crate::util::grove_operations::{BatchInsertTreeApplyType, QueryType}; -use crate::util::object_size_info::PathKeyElementInfo::{ - PathKeyElement, PathKeyUnknownElementSize, -}; +use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; use crate::util::object_size_info::PathKeyInfo::PathKey; use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; @@ -41,7 +39,6 @@ use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; -use grovedb::batch::key_info::KeyInfo; impl Drive { /// Prepares the operations for deleting a document. @@ -290,30 +287,17 @@ impl Drive { &platform_version.drive, )?; - if estimated_costs_only_with_layer_info.is_some() { - self.batch_insert::<0>( - PathKeyUnknownElementSize(( - KeyInfoPath::from_known_owned_path(lifecycle_path), - KeyInfo::KnownKey(document_id.to_vec()), - Element::required_item_space( - DOCUMENT_LIFECYCLE_RECORD_SIZE, - flags_len, - &platform_version.drive.grove_version, - )?, - )), - batch_operations, - &platform_version.drive, - ) - } else { - self.batch_insert::<0>( - PathKeyElement(( - lifecycle_path, - document_id.to_vec(), - Element::Item(record.serialize(), element_flags), - )), - batch_operations, - &platform_version.drive, - ) - } + // The record's content is fully known whether or not this is a dry + // run: it is a fixed-width encoding of the deletion time, and the + // deleter it is flagged to is an input to the delete. + self.batch_insert::<0>( + PathKeyElement(( + lifecycle_path, + document_id.to_vec(), + Element::Item(record.serialize(), element_flags), + )), + batch_operations, + &platform_version.drive, + ) } } diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs index 72ad713d100..68f55b33a10 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -27,9 +27,7 @@ use crate::util::grove_operations::BatchDeleteApplyType::{ StatefulBatchDelete, StatelessBatchDelete, }; use crate::util::grove_operations::{DirectQueryType, QueryTarget}; -use crate::util::object_size_info::PathKeyElementInfo::{ - PathKeyElement, PathKeyUnknownElementSize, -}; +use crate::util::object_size_info::PathKeyElementInfo::{PathKeyElement, PathKeyElementSize}; use crate::util::storage_flags::StorageFlags; /// Length of a revision key: a block timestamp followed by a history sequence. @@ -351,16 +349,18 @@ impl Drive { batch_operations.append(&mut single_operation); } - let flags_len = StorageFlags::approximate_size(true, None); + // A record of the right shape rather than the real one: the dry run + // cannot know the deletion time it carries or the identity it is + // flagged to, only that both are there and how many bytes they take. + let flags_len = StorageFlags::approximate_size(true, None) as usize; self.batch_insert::<0>( - PathKeyUnknownElementSize(( + PathKeyElementSize(( KeyInfoPath::from_known_owned_path(lifecycle_path.to_vec()), KeyInfo::KnownKey(document_id.to_vec()), - Element::required_item_space( - DOCUMENT_LIFECYCLE_RECORD_SIZE, - flags_len, - &platform_version.drive.grove_version, - )?, + Element::Item( + vec![0u8; DOCUMENT_LIFECYCLE_RECORD_SIZE as usize], + Some(vec![0u8; flags_len]), + ), )), &mut batch_operations, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs index 6a0177aad6d..fb33daf317a 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs @@ -1,7 +1,6 @@ use dpp::block::epoch::Epoch; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; From 519df9778c08b3ab080eabb3893e257f0e49ca19 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:36:12 +0700 Subject: [PATCH 09/48] feat(sdk): expose the document erase across the client surfaces The Rust builder mirrors the delete one, minus the token payment erase has no use for, and the SDK method broadcasts one chunk. Its result says only that the erase was accepted: the proof authenticates that the document is absent by id, which it already was before the erase ran, so it is evidence about the state rather than about this transition having executed. A separately named lifecycle read answers how much history is left, and is named apart on purpose, because its answer describes committed state at the moment of the read and another erase or a re-create may land in between. The WebAssembly and JavaScript SDKs pass the same identifiers through to it; a document that is no longer visible has no values a client could carry anyway. Co-Authored-By: Claude Opus 5 (1M context) --- packages/js-evo-sdk/src/documents/facade.ts | 13 + .../tests/unit/facades/documents.spec.ts | 33 +++ .../platform/documents/transitions/erase.rs | 254 ++++++++++++++++++ .../src/platform/documents/transitions/mod.rs | 2 + .../src/state_transitions/document.rs | 134 ++++++++- 5 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 packages/rs-sdk/src/platform/documents/transitions/erase.rs diff --git a/packages/js-evo-sdk/src/documents/facade.ts b/packages/js-evo-sdk/src/documents/facade.ts index 928c4a68758..a1a70a0392f 100644 --- a/packages/js-evo-sdk/src/documents/facade.ts +++ b/packages/js-evo-sdk/src/documents/facade.ts @@ -121,6 +121,19 @@ export class DocumentsFacade { return w.documentDelete(options); } + /** + * Erases a chunk of the retained revisions of an already deleted document. + * + * A document with more retained revisions than one transition may remove + * needs several calls. The first must be signed by the document's owner and + * commits the document to erasure; any identity may sign the ones after it. + * Read `history` to see how many revisions are left. + */ + async erase(options: wasm.DocumentEraseOptions): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.documentErase(options); + } + async transfer(options: wasm.DocumentTransferOptions): Promise { const w = await this.sdk.getWasmSdkConnected(); return w.documentTransfer(options); diff --git a/packages/js-evo-sdk/tests/unit/facades/documents.spec.ts b/packages/js-evo-sdk/tests/unit/facades/documents.spec.ts index f2dcb4edefa..337c0a67750 100644 --- a/packages/js-evo-sdk/tests/unit/facades/documents.spec.ts +++ b/packages/js-evo-sdk/tests/unit/facades/documents.spec.ts @@ -26,6 +26,7 @@ describe('DocumentsFacade', () => { let documentCreateStub: SinonStub; let documentReplaceStub: SinonStub; let documentDeleteStub: SinonStub; + let documentEraseStub: SinonStub; let documentTransferStub: SinonStub; let documentPurchaseStub: SinonStub; let documentSetPriceStub: SinonStub; @@ -92,6 +93,7 @@ describe('DocumentsFacade', () => { documentCreateStub = this.sinon.stub(wasmSdk, 'documentCreate').resolves(); documentReplaceStub = this.sinon.stub(wasmSdk, 'documentReplace').resolves(); documentDeleteStub = this.sinon.stub(wasmSdk, 'documentDelete').resolves(); + documentEraseStub = this.sinon.stub(wasmSdk, 'documentErase').resolves(); documentTransferStub = this.sinon.stub(wasmSdk, 'documentTransfer').resolves(); documentPurchaseStub = this.sinon.stub(wasmSdk, 'documentPurchase').resolves(); documentSetPriceStub = this.sinon.stub(wasmSdk, 'documentSetPrice').resolves(); @@ -291,6 +293,37 @@ describe('DocumentsFacade', () => { }); }); + describe('erase()', () => { + it('should erase the retained revisions of a deleted document', async () => { + const options = { + document, + identityKey, + signer, + }; + + await client.documents.erase(options); + + expect(documentEraseStub).to.be.calledOnceWithExactly(options); + }); + + it('should accept document identifiers instead of a Document instance', async () => { + const options = { + document: { + id: '4mZmxva49PBb7BE7srw9o3gixvDfj1dAx1K6z4A7P9Ah', + ownerId: '5mjGWa9mruHnLBht3ntBi8CZ6sNk3hZZsQMgTvgQobjS', + dataContractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec', + documentTypeName: 'note', + }, + identityKey, + signer, + }; + + await client.documents.erase(options); + + expect(documentEraseStub).to.be.calledOnceWithExactly(options); + }); + }); + describe('transfer()', () => { it('should transfer document ownership to another identity', async () => { const recipientId = '6o4vL6YpPjamqnnPNpwNSspYJdhPpzYbXvAJ4PYH7Ack'; diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs new file mode 100644 index 00000000000..8a26ee63c08 --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -0,0 +1,254 @@ +use crate::platform::transition::broadcast::BroadcastStateTransition; +use crate::platform::transition::put_settings::PutSettings; +use crate::platform::{Fetch, Identifier}; +use crate::{Error, Sdk}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::DataContract; +use dpp::document::{Document, INITIAL_REVISION}; +use dpp::identity::signer::Signer; +use dpp::identity::IdentityPublicKey; +use dpp::prelude::UserFeeIncrease; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::proof_result::StateTransitionProofResult; +use dpp::state_transition::StateTransition; +use dpp::version::PlatformVersion; +use drive::drive::document::history::{ + DocumentHistoryLifecycle, DocumentHistorySelector, DocumentHistoryState, +}; +use std::sync::Arc; + +/// A builder to configure and broadcast document erase transitions. +/// +/// Erasing purges the retained revisions of a document that has already been +/// deleted. The first erase must come from the document's owner and commits the +/// document to erasure; every erase after that may come from any identity, +/// because the committed state already carries the authorization. +pub struct DocumentEraseTransitionBuilder { + /// The data contract. + pub data_contract: Arc, + /// The name of the document type whose revisions are being erased. + pub document_type_name: String, + /// The document whose revisions are being erased. + pub document_id: Identifier, + /// The identity submitting and paying for this erase, which need not own + /// the document once its erasure has begun. + pub owner_id: Identifier, + /// Settings for broadcasting. + pub settings: Option, + /// A user fee increase. + pub user_fee_increase: Option, + /// State transition creation options. + pub state_transition_creation_options: Option, +} + +impl DocumentEraseTransitionBuilder { + /// Start building an erase request for the provided data contract. + /// + /// There is no token payment: the deletion this erase follows was charged + /// when the document was deleted, and an erase that carried one would let a + /// continuation, which anyone may submit, move tokens. + pub fn new( + data_contract: Arc, + document_type_name: String, + document_id: Identifier, + owner_id: Identifier, + ) -> Self { + Self { + data_contract, + document_type_name, + document_id, + owner_id, + settings: None, + user_fee_increase: None, + state_transition_creation_options: None, + } + } + + /// Adds a user fee increase to the erase transition. + pub fn with_user_fee_increase(mut self, user_fee_increase: UserFeeIncrease) -> Self { + self.user_fee_increase = Some(user_fee_increase); + self + } + + /// Adds settings to the erase transition. + pub fn with_settings(mut self, settings: PutSettings) -> Self { + self.settings = Some(settings); + self + } + + /// Adds creation options to the erase transition. + pub fn with_state_transition_creation_options( + mut self, + creation_options: StateTransitionCreationOptions, + ) -> Self { + self.state_transition_creation_options = Some(creation_options); + self + } + + /// Signs the erase transition. + pub async fn sign( + &self, + sdk: &Sdk, + identity_public_key: &IdentityPublicKey, + signer: &impl Signer, + platform_version: &PlatformVersion, + ) -> Result { + // Validate the target before the nonce fetch below bumps the SDK's + // cached contract nonce: no transition is broadcast on an error path, + // so a rejection after it would leak an increment per failed call. + let document_type = self + .data_contract + .document_type_for_name(&self.document_type_name) + .map_err(|e| Error::Protocol(e.into()))?; + + // The transition carries only the base, so an id is all the builder + // needs; the values of a document that is no longer visible are not + // available to a client anyway. + let document = Document::V0(dpp::document::DocumentV0 { + contract_version: None, + id: self.document_id, + owner_id: self.owner_id, + properties: Default::default(), + revision: Some(INITIAL_REVISION), + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }); + + let identity_contract_nonce = sdk + .get_identity_contract_nonce( + self.owner_id, + self.data_contract.id(), + true, + self.settings, + ) + .await?; + + let state_transition = BatchTransition::new_document_erase_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + self.user_fee_increase.unwrap_or_default(), + signer, + platform_version, + self.state_transition_creation_options, + ) + .await?; + + Ok(state_transition) + } +} + +/// What one erase transition achieved. +#[derive(Debug)] +pub enum DocumentEraseResult { + /// The erase was accepted. Whether it removed the last revision is not + /// something the transition's own proof can show, because the document was + /// already absent from ordinary reads before it ran — read the current + /// lifecycle with [`Sdk::document_current_lifecycle`] to find out. + Accepted(Identifier), +} + +impl Sdk { + /// Erases a chunk of the retained revisions of an already deleted document. + /// + /// The first erase must be signed by the document's owner; any identity may + /// submit the ones after it. A document with more retained revisions than + /// one transition may remove needs several, and this method broadcasts one. + /// + /// The proof this returns authenticates that the document is absent by id, + /// which it already was before the erase ran, so it is evidence about the + /// state, not about this transition having executed. Use + /// [`Sdk::document_current_lifecycle`] to observe how much history is left. + pub async fn document_erase>( + &self, + erase_document_transition_builder: DocumentEraseTransitionBuilder, + signing_key: &IdentityPublicKey, + signer: &S, + ) -> Result { + let platform_version = self.version(); + let put_settings = erase_document_transition_builder.settings; + let document_id = erase_document_transition_builder.document_id; + + let state_transition = erase_document_transition_builder + .sign(self, signing_key, signer, platform_version) + .await?; + + let proof_result = state_transition + .broadcast_and_wait::(self, put_settings) + .await?; + + match proof_result { + StateTransitionProofResult::VerifiedDocuments(documents) => { + if let Some((erased_id, None)) = documents.into_iter().next() { + Ok(DocumentEraseResult::Accepted(erased_id)) + } else { + Err(Error::DriveProofError( + drive::error::proof::ProofError::UnexpectedResultProof( + "expected an absent document in the VerifiedDocuments result for an \ + erase transition" + .to_string(), + ), + vec![], + Default::default(), + )) + } + } + _ => Err(Error::DriveProofError( + drive::error::proof::ProofError::UnexpectedResultProof( + "expected VerifiedDocuments for a document erase transition".to_string(), + ), + vec![], + Default::default(), + )), + } + } + + /// Reads where a document stands in its lifecycle right now. + /// + /// Deliberately named apart from the erase and delete calls: the answer + /// describes committed state at the moment of the read, not the outcome of + /// any particular transition. Another erase, or a re-create of the same id, + /// may land between a transition and this read. + pub async fn document_current_lifecycle( + &self, + data_contract_id: Identifier, + document_type_name: String, + document_id: Identifier, + ) -> Result { + use dash_platform_queries::documents::document_history_query::DocumentHistoryQuery; + use drive_proof_verifier::types::DocumentHistory; + + let history = DocumentHistory::fetch( + self, + DocumentHistoryQuery { + data_contract_id, + document_type_name, + document_id, + // The oldest retained revision, if any: the page is not the + // point, the metadata alongside it is. + selector: DocumentHistorySelector::StartAtTime(0), + limit: Some(1), + }, + ) + .await?; + + Ok(history + .and_then(|history| history.lifecycle) + .unwrap_or(DocumentHistoryLifecycle { + state: DocumentHistoryState::Absent, + remaining_revisions: 0, + times: Default::default(), + })) + } +} diff --git a/packages/rs-sdk/src/platform/documents/transitions/mod.rs b/packages/rs-sdk/src/platform/documents/transitions/mod.rs index 200a2164267..406ac82c0d9 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/mod.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/mod.rs @@ -1,5 +1,6 @@ pub mod create; pub mod delete; +pub mod erase; pub mod purchase; pub mod replace; pub mod set_price; @@ -7,6 +8,7 @@ pub mod transfer; pub use create::{DocumentCreateResult, DocumentCreateTransitionBuilder}; pub use delete::{DocumentDeleteResult, DocumentDeleteTransitionBuilder}; +pub use erase::{DocumentEraseResult, DocumentEraseTransitionBuilder}; pub use purchase::{DocumentPurchaseResult, DocumentPurchaseTransitionBuilder}; pub use replace::{DocumentReplaceResult, DocumentReplaceTransitionBuilder}; pub use set_price::{DocumentSetPriceResult, DocumentSetPriceTransitionBuilder}; diff --git a/packages/wasm-sdk/src/state_transitions/document.rs b/packages/wasm-sdk/src/state_transitions/document.rs index fb75f79180d..52e90e9ac31 100644 --- a/packages/wasm-sdk/src/state_transitions/document.rs +++ b/packages/wasm-sdk/src/state_transitions/document.rs @@ -12,7 +12,9 @@ use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::identity::IdentityPublicKey; use dash_sdk::dpp::platform_value::Identifier; use dash_sdk::dpp::tokens::token_payment_info::TokenPaymentInfo; -use dash_sdk::platform::documents::transitions::DocumentDeleteTransitionBuilder; +use dash_sdk::platform::documents::transitions::{ + DocumentDeleteTransitionBuilder, DocumentEraseTransitionBuilder, +}; use dash_sdk::platform::transition::purchase_document::PurchaseDocument; use dash_sdk::platform::transition::put_document::PutDocument; use dash_sdk::platform::transition::transfer_document::TransferDocument; @@ -506,6 +508,136 @@ impl WasmSdk { } } +// ============================================================================ +// Document Erase +// ============================================================================ + +/// TypeScript interface for document erase options +#[wasm_bindgen(typescript_custom_section)] +const DOCUMENT_ERASE_OPTIONS_TS: &'static str = r#" +/** + * Options for erasing the retained revisions of an already deleted document. + */ +export interface DocumentEraseOptions { + /** + * The document to erase, or the identifiers that name it. The document is + * already invisible to ordinary reads, so its identifiers are all that is + * needed. + */ + document: Document | { + id: IdentifierLike; + ownerId: IdentifierLike; + dataContractId: IdentifierLike; + documentTypeName: string; + }; + + /** + * The identity public key to use for signing the transition. + * The first erase must be signed by the document's owner; any identity may + * sign the ones after it. + */ + identityKey: IdentityPublicKey; + + /** + * Signer containing the private key that corresponds to the identity key. + * Use IdentitySigner to add the private key before calling. + */ + signer: IdentitySigner; + + /** + * Optional settings for the broadcast operation. + * Includes retries, timeouts, userFeeIncrease, etc. + */ + settings?: PutSettings; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "DocumentEraseOptions")] + pub type DocumentEraseOptionsJs; +} + +#[wasm_bindgen] +impl WasmSdk { + /// Erase a chunk of the retained revisions of an already deleted document. + /// + /// A document with more retained revisions than one transition may remove + /// needs several calls. The first must come from the document's owner and + /// commits the document to erasure; any identity may make the ones after + /// it. Read the document's history to see how much is left. + /// + /// @param options - Erase options including the document identifiers, identity key, and signer + /// @returns Promise that resolves when the erase has been accepted + #[wasm_bindgen(js_name = "documentErase")] + pub async fn document_erase( + &self, + options: DocumentEraseOptionsJs, + ) -> Result<(), WasmSdkError> { + let document_js = js_sys::Reflect::get(&options, &JsValue::from_str("document")) + .map_err(|_| WasmSdkError::invalid_argument("document is required"))?; + + if document_js.is_undefined() || document_js.is_null() { + return Err(WasmSdkError::invalid_argument("document is required")); + } + + // The transition carries only the base, so the values of a Document + // instance are not needed and a plain object of identifiers works for + // every document type. + let (document_id, owner_id, contract_id, document_type_name): ( + Identifier, + Identifier, + Identifier, + String, + ) = if get_class_type(&document_js).ok().as_deref() == Some("Document") { + let doc: DocumentWasm = document_js + .to_wasm::("Document") + .map(|boxed| (*boxed).clone())?; + let doc_inner: Document = doc.clone().into(); + ( + doc.id().into(), + doc_inner.owner_id(), + doc.data_contract_id().into(), + doc.document_type_name(), + ) + } else { + ( + IdentifierWasm::try_from_options(&document_js, "id")?.into(), + IdentifierWasm::try_from_options(&document_js, "ownerId")?.into(), + IdentifierWasm::try_from_options(&document_js, "dataContractId")?.into(), + try_from_options_with(&document_js, "documentTypeName", |v| { + try_to_string(v, "documentTypeName") + })?, + ) + }; + + let identity_key_wasm = IdentityPublicKeyWasm::try_from_options(&options, "identityKey")?; + let identity_key: IdentityPublicKey = identity_key_wasm.into(); + let signer = IdentitySignerWasm::try_from_options(&options, "signer")?; + let data_contract = self.get_or_fetch_contract(contract_id).await?; + let settings = + try_from_options_optional::(&options, "settings")?.map(Into::into); + + let builder = DocumentEraseTransitionBuilder::new( + Arc::new(data_contract), + document_type_name, + document_id, + owner_id, + ); + let builder = if let Some(s) = settings { + builder.with_settings(s) + } else { + builder + }; + + self.inner_sdk() + .document_erase(builder, &identity_key, &signer) + .await?; + + Ok(()) + } +} + // ============================================================================ // Document Transfer // ============================================================================ From 1465ea1f40c80358e0f38deca2179362439ec8ea Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:41:20 +0700 Subject: [PATCH 10/48] test(drive): pin the storage behaviour of the document lifecycle Covers what a delete leaves behind and what an erase removes: the document leaves ordinary reads while every revision stays readable and provable, a history shorter than a chunk goes in one transition, a longer one is committed to erasure and finished by the chunks after it, and the record those chunks read is written once and never rewritten. Also pinned: the id stays reserved until the last revision is gone and is reusable afterwards, the history subtree cannot be dropped while a revision survives, each revision is refunded to whoever wrote it, and the admission estimate covers a full chunk whatever the document actually retains. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/drive/document/lifecycle/mod.rs | 5 +- .../src/drive/document/lifecycle/tests.rs | 765 ++++++++++++++++++ 2 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 packages/rs-drive/src/drive/document/lifecycle/tests.rs diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index c329d4afcae..7b173760ce3 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -15,6 +15,9 @@ #[cfg(feature = "server")] mod fetch; +#[cfg(all(test, feature = "server", feature = "verify"))] +mod tests; + #[cfg(feature = "server")] pub use fetch::DocumentLifecycleState; @@ -170,7 +173,7 @@ impl DocumentLifecycleRecord { } #[cfg(test)] -mod tests { +mod record_tests { use super::*; #[test] diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs new file mode 100644 index 00000000000..da6e6600541 --- /dev/null +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -0,0 +1,765 @@ +//! The storage side of the keep-history document lifecycle: what a delete +//! leaves behind, what an erase removes, and what a chunked erasure looks like +//! from one transition to the next. + +use super::fetch::DocumentLifecycleState; +use super::DocumentLifecycleRecord; +use crate::drive::document::history::{ + DocumentHistoryQueryV1, DocumentHistorySelector, DocumentHistoryState, +}; +use crate::drive::document::paths::{document_history_path, document_lifecycle_path}; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::query::{ + DriveDocumentQuery, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus, +}; +use crate::util::grove_operations::BatchDeleteApplyType::StatefulBatchDelete; +use crate::util::object_size_info::{DocumentAndContractInfo, DocumentInfo, OwnedDocumentInfo}; +use crate::util::storage_flags::StorageFlags; +use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::document::{DocumentV0Getters, DocumentV0Setters}; +use dpp::identifier::Identifier; +use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; +use dpp::version::PlatformVersion; +use grovedb::{MaybeTree, TransactionArg, TreeType}; +use std::borrow::Cow; +use std::collections::HashMap; + +const FAMILY_HISTORY_CONTRACT: &str = + "tests/supporting_files/contract/family/family-contract-with-history.json"; +const PERSON: &str = "tests/supporting_files/contract/family/person0.json"; + +fn latest() -> &'static PlatformVersion { + PlatformVersion::latest() +} + +fn chunk_size() -> u64 { + latest() + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the erase chunk") as u64 +} + +/// Applies the family contract and writes `revisions` revisions of one person, +/// each at its own block time so every revision key is distinct. +fn setup_history(revisions: u64, owner: [u8; 32]) -> (Drive, DataContract, Identifier) { + let version = latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract(FAMILY_HISTORY_CONTRACT, false, version) + .expect("expected the family history contract"); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .expect("expected to apply the contract"); + let document_type = contract + .document_type_for_name("person") + .expect("expected the person type"); + let mut document = + json_document_to_document(PERSON, Some(owner.into()), document_type, version) + .expect("expected a person document"); + let flags = Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))); + for revision in 1..=revisions { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, flags.clone())), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .expect("expected to write a revision"); + } + let id = document.id(); + (drive, contract, id) +} + +fn document_type_of(contract: &DataContract) -> DocumentTypeRef<'_> { + contract + .document_type_for_name("person") + .expect("expected the person type") +} + +fn lifecycle_of(drive: &Drive, contract: &DataContract, id: Identifier) -> DocumentLifecycleState { + drive + .fetch_document_lifecycle( + contract, + document_type_of(contract), + id, + None, + None, + latest(), + ) + .expect("expected to read the lifecycle") + .0 +} + +fn record_of( + drive: &Drive, + contract: &DataContract, + id: Identifier, +) -> Option { + let path = document_lifecycle_path(contract.id_ref().as_bytes(), "person"); + drive + .grove_get_raw_optional_item( + path.as_slice().into(), + id.as_slice(), + crate::util::grove_operations::DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &latest().drive, + ) + .expect("expected to read the lifecycle tree") + .map(|bytes| DocumentLifecycleRecord::deserialize(&bytes).expect("expected a valid record")) +} + +fn visible_document_count(drive: &Drive, contract: &DataContract) -> usize { + let query = DriveDocumentQuery::all_items_query(contract, document_type_of(contract), None); + query + .execute_raw_results_no_proof(drive, None, None, latest()) + .expect("expected to query documents") + .0 + .len() +} + +fn history_metadata( + drive: &Drive, + contract: &DataContract, + id: Identifier, +) -> crate::drive::document::history::DocumentHistoryLifecycle { + let query = DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + selector: DocumentHistorySelector::StartAtTime(0), + limit: Some(10), + }; + let (page, proof) = drive + .prove_document_history_v1(&query, document_type_of(contract), None, latest()) + .expect("expected to prove history"); + let (_, verified) = + Drive::verify_document_history_v1(&query, &proof, document_type_of(contract), latest()) + .expect("expected the proof to verify"); + assert_eq!( + verified, page, + "the proved history must match the read history" + ); + verified.lifecycle +} + +fn delete( + drive: &Drive, + contract: &DataContract, + id: Identifier, + deleter: Identifier, + time_ms: u64, +) -> dpp::fee::fee_result::FeeResult { + let mut operations = vec![]; + let block_info = BlockInfo::default_with_time(time_ms); + let batch = drive + .delete_document_for_contract_operations( + id, + contract, + document_type_of(contract), + &block_info, + Some(deleter), + None, + &mut None, + None, + latest(), + ) + .expect("expected to build the delete operations"); + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut operations, &latest().drive) + .expect("expected to apply the delete"); + Drive::calculate_fee( + None, + Some(operations), + &block_info.epoch, + drive.config.epochs_per_era, + latest(), + None, + ) + .expect("expected a fee") +} + +fn erase( + drive: &Drive, + contract: &DataContract, + id: Identifier, + time_ms: u64, +) -> dpp::fee::fee_result::FeeResult { + let mut operations = vec![]; + let block_info = BlockInfo::default_with_time(time_ms); + let batch = drive + .erase_document_for_contract_operations( + id, + contract, + document_type_of(contract), + &block_info, + &mut None, + None, + latest(), + ) + .expect("expected to build the erase operations"); + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut operations, &latest().drive) + .expect("expected to apply the erase"); + Drive::calculate_fee( + None, + Some(operations), + &block_info.epoch, + drive.config.epochs_per_era, + latest(), + None, + ) + .expect("expected a fee") +} + +/// The admission estimate of an erase, which knows nothing about the document +/// it will act on. +fn estimated_erase_fee( + drive: &Drive, + contract: &DataContract, + id: Identifier, +) -> dpp::fee::fee_result::FeeResult { + let mut operations = vec![]; + let mut layers = Some(HashMap::new()); + let block_info = BlockInfo::default_with_time(9_000); + let batch = drive + .erase_document_for_contract_operations( + id, + contract, + document_type_of(contract), + &block_info, + &mut layers, + None, + latest(), + ) + .expect("expected to build the estimated erase operations"); + drive + .apply_batch_low_level_drive_operations( + layers, + None, + batch, + &mut operations, + &latest().drive, + ) + .expect("expected to price the erase"); + Drive::calculate_fee( + None, + Some(operations), + &block_info.epoch, + drive.config.epochs_per_era, + latest(), + None, + ) + .expect("expected a fee") +} + +#[test] +fn should_hide_a_deleted_document_while_keeping_every_revision_readable() { + let owner = [7u8; 32]; + let (drive, contract, id) = setup_history(3, owner); + assert_eq!(visible_document_count(&drive, &contract), 1); + + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + assert_eq!( + visible_document_count(&drive, &contract), + 0, + "a deleted document must be absent from every ordinary read" + ); + let record = record_of(&drive, &contract, id).expect("a delete writes a lifecycle record"); + assert_eq!(record.deleted_at_ms(), 5_000); + assert!(!record.is_erasing()); + + let lifecycle = history_metadata(&drive, &contract, id); + assert_eq!(lifecycle.state, DocumentHistoryState::Deleted); + assert_eq!( + lifecycle.remaining_revisions, 3, + "a delete removes no revision" + ); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000); + assert_eq!(lifecycle.times.erasing_started_at_ms, 0); + + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Deleted(_) + )); +} + +#[test] +fn should_erase_a_history_shorter_than_a_chunk_in_one_transition() { + let owner = [8u8; 32]; + let (drive, contract, id) = setup_history(1, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + erase(&drive, &contract, id, 6_000); + + assert!( + record_of(&drive, &contract, id).is_none(), + "the terminal chunk removes the record" + ); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Absent + )); + let lifecycle = history_metadata(&drive, &contract, id); + assert_eq!(lifecycle.state, DocumentHistoryState::Absent); + assert_eq!(lifecycle.remaining_revisions, 0); +} + +#[test] +fn should_erase_a_history_of_exactly_one_chunk_in_one_transition() { + let chunk = chunk_size(); + let owner = [9u8; 32]; + let (drive, contract, id) = setup_history(chunk, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + erase(&drive, &contract, id, 6_000); + + assert!(record_of(&drive, &contract, id).is_none()); + assert_eq!( + history_metadata(&drive, &contract, id).state, + DocumentHistoryState::Absent + ); +} + +#[test] +fn should_commit_a_longer_history_to_erasure_and_finish_it_in_the_next_chunk() { + let chunk = chunk_size(); + let owner = [10u8; 32]; + let (drive, contract, id) = setup_history(chunk + 1, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + erase(&drive, &contract, id, 6_000); + + let record = record_of(&drive, &contract, id).expect("a partial chunk keeps the record"); + assert!(record.is_erasing(), "the first chunk commits the erasure"); + assert_eq!(record.deleted_at_ms(), 5_000, "the deletion time survives"); + assert_eq!(record.erasing_started_at_ms(), 6_000); + assert_eq!( + record.erasing_from_revision(), + chunk + 1, + "the erasure started from the newest revision" + ); + assert_eq!(record.erasing_from_time_ms(), 1_000 + chunk + 1); + + let lifecycle = history_metadata(&drive, &contract, id); + assert_eq!(lifecycle.state, DocumentHistoryState::Erasing); + assert_eq!( + lifecycle.remaining_revisions, 1, + "the oldest revision is what a partial erasure leaves behind" + ); + assert_eq!(lifecycle.times.erasing_from_revision, chunk + 1); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Erasing + )); + + erase(&drive, &contract, id, 7_000); + + assert!(record_of(&drive, &contract, id).is_none()); + assert_eq!( + history_metadata(&drive, &contract, id).state, + DocumentHistoryState::Absent + ); +} + +#[test] +fn should_leave_the_record_untouched_across_a_continuation() { + let chunk = chunk_size(); + let owner = [11u8; 32]; + let (drive, contract, id) = setup_history(chunk * 2 + 1, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + erase(&drive, &contract, id, 6_000); + let after_start = record_of(&drive, &contract, id).expect("the erasure is committed"); + + erase(&drive, &contract, id, 7_000); + let after_continuation = + record_of(&drive, &contract, id).expect("the erasure is not finished yet"); + + assert_eq!( + after_start, after_continuation, + "a continuation carries no authorization of its own and writes nothing to the record" + ); + assert_eq!( + history_metadata(&drive, &contract, id).remaining_revisions, + 1 + ); + + erase(&drive, &contract, id, 8_000); + assert_eq!( + history_metadata(&drive, &contract, id).state, + DocumentHistoryState::Absent + ); +} + +#[test] +fn should_refuse_to_create_a_document_whose_revisions_are_still_retained() { + let owner = [12u8; 32]; + let (drive, contract, id) = setup_history(2, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let version = latest(); + let document_type = document_type_of(&contract); + let mut document = + json_document_to_document(PERSON, Some(owner.into()), document_type, version).unwrap(); + document.set_revision(Some(1)); + + let error = drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + // The contested award path reaches the writer this way: nothing + // above it has checked whether the id is free. + false, + BlockInfo::default_with_time(6_000), + true, + None, + version, + None, + ) + .expect_err("a reserved id must not be creatable"); + assert!(matches!( + error, + Error::Drive(DriveError::CorruptedDocumentAlreadyExists(_)) + )); + + // Once the revisions are gone the id is free again. + erase(&drive, &contract, id, 7_000); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default_with_time(8_000), + true, + None, + version, + None, + ) + .expect("an erased id is reusable"); + assert_eq!(visible_document_count(&drive, &contract), 1); +} + +#[test] +fn should_refuse_to_drop_a_history_subtree_while_a_revision_survives() { + let owner = [13u8; 32]; + let (drive, contract, id) = setup_history(2, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + // The removal of the subtree on its own, without the revision deletes an + // erase puts in the same batch. GroveDB has nothing to conclude emptiness + // from and must refuse rather than orphan the revisions' storage. + let mut history_root = document_history_path(contract.id_ref().as_bytes(), "person", &[]); + history_root.pop(); + let mut operations = vec![]; + let batch_result = drive.batch_delete( + history_root.as_slice().into(), + id.as_slice(), + StatefulBatchDelete { + is_known_to_be_subtree_with_sum: Some(MaybeTree::Tree(TreeType::ProvableCountTree)), + }, + None, + &mut operations, + &latest().drive, + ); + assert!( + batch_result.is_err(), + "a populated history subtree must not be removable" + ); +} + +#[test] +fn should_refund_each_removed_revision_to_the_identity_that_wrote_it() { + let owner = [14u8; 32]; + let (drive, contract, id) = setup_history(3, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let fee = erase(&drive, &contract, id, 6_000); + + let refunded: u64 = fee + .fee_refunds + .0 + .get(&owner) + .map(|per_epoch| per_epoch.values().sum()) + .unwrap_or_default(); + assert!( + refunded > 0, + "the revisions' writer must be credited for the bytes the erase removed" + ); +} + +#[test] +fn should_price_an_erase_for_a_full_chunk_whatever_the_document_retains() { + let chunk = chunk_size(); + for revisions in [1, chunk, chunk + 1] { + let owner = [15u8; 32]; + let (drive, contract, id) = setup_history(revisions, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let estimated = estimated_erase_fee(&drive, &contract, id); + let actual = erase(&drive, &contract, id, 6_000); + + assert!( + estimated.processing_fee >= actual.processing_fee, + "the admission estimate under-charged a {revisions}-revision erase: {} < {}", + estimated.processing_fee, + actual.processing_fee + ); + } +} + +/// The history a delete leaves behind is still fully readable, page by page and +/// with a proof, including the page that runs past its end. +#[test] +fn should_page_and_prove_the_history_of_a_deleted_document() { + let owner = [16u8; 32]; + let (drive, contract, id) = setup_history(12, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let mut selector = DocumentHistorySelector::StartAtTime(0); + let mut seen = vec![]; + loop { + let query = DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + selector, + limit: Some(10), + }; + let (page, proof) = drive + .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + .expect("expected to prove a page"); + let (_, verified) = Drive::verify_document_history_v1( + &query, + &proof, + document_type_of(&contract), + latest(), + ) + .expect("expected the page proof to verify"); + assert_eq!(verified.lifecycle.state, DocumentHistoryState::Deleted); + assert_eq!(verified.lifecycle.remaining_revisions, 12); + assert_eq!(verified.lifecycle.times.deleted_at_ms, 5_000); + seen.extend(page.entries.iter().map(|entry| entry.revision)); + let Some(last) = page.entries.last() else { + break; + }; + selector = DocumentHistorySelector::StartAfter { + time_ms: last.time_ms, + revision: last.revision, + }; + } + assert_eq!(seen, (1..=12).collect::>()); +} + +/// Every document type parsed by an earlier grammar has no lifecycle tree, so +/// the read has to treat a missing tree the same as a missing key. +#[test] +fn should_report_a_document_with_no_lifecycle_tree_as_absent() { + let owner = [17u8; 32]; + let (drive, contract, _) = setup_history(1, owner); + let unknown = Identifier::new([99u8; 32]); + assert!(matches!( + lifecycle_of(&drive, &contract, unknown), + DocumentLifecycleState::Absent + )); +} + +#[test] +fn should_reject_an_erase_of_a_document_that_retains_nothing() { + let owner = [18u8; 32]; + let (drive, contract, id) = setup_history(1, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + erase(&drive, &contract, id, 6_000); + + let error = drive + .erase_document_for_contract_operations( + id, + &contract, + document_type_of(&contract), + &BlockInfo::default_with_time(7_000), + &mut None, + None as TransactionArg, + latest(), + ) + .expect_err("there is nothing left to erase"); + assert!(matches!( + error, + Error::Drive(DriveError::CorruptedDriveState(_)) + )); +} + +/// The two record shapes must occupy the same number of bytes: an erase start +/// overwrites the record in place, and a different size would re-price its +/// storage and drop the deleter's flags. +#[test] +fn should_keep_the_deleter_as_the_records_beneficiary_across_an_erase_start() { + let chunk = chunk_size(); + let owner = [19u8; 32]; + let deleter = Identifier::new([20u8; 32]); + let (drive, contract, id) = setup_history(chunk + 1, owner); + delete(&drive, &contract, id, deleter, 5_000); + + let path = document_lifecycle_path(contract.id_ref().as_bytes(), "person"); + let flags_before = drive + .grove + .get_raw( + path.as_slice().into(), + id.as_slice(), + None, + &latest().drive.grove_version, + ) + .unwrap() + .expect("the record exists"); + + erase(&drive, &contract, id, 6_000); + + let flags_after = drive + .grove + .get_raw( + path.as_slice().into(), + id.as_slice(), + None, + &latest().drive.grove_version, + ) + .unwrap() + .expect("the record survives a partial erasure"); + let (grovedb::Element::Item(before, before_flags), grovedb::Element::Item(after, after_flags)) = + (flags_before, flags_after) + else { + panic!("a lifecycle record is an item"); + }; + assert_eq!( + before.len(), + after.len(), + "the overwrite must be the same size" + ); + assert_eq!( + before_flags, after_flags, + "the deleter must stay the beneficiary of the record's bytes" + ); +} + +/// A revision written by one identity and a revision written by another are +/// refunded separately, so an erasure credits whoever paid for each byte. +#[test] +fn should_refund_distinct_writers_separately() { + let first = [21u8; 32]; + let second = [22u8; 32]; + let version = latest(); + let (drive, contract, id) = setup_history(1, first); + let document_type = document_type_of(&contract); + let mut document = + json_document_to_document(PERSON, Some(first.into()), document_type, version).unwrap(); + document.set_revision(Some(2)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &document, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(second)))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(2_000), + true, + None, + version, + None, + ) + .expect("expected a second writer's revision"); + + delete(&drive, &contract, id, Identifier::new(first), 5_000); + let fee = erase(&drive, &contract, id, 6_000); + + assert!( + fee.fee_refunds.0.contains_key(&first) && fee.fee_refunds.0.contains_key(&second), + "each writer must be credited for the revision they paid for; got {:?}", + fee.fee_refunds.0.keys().collect::>() + ); +} + +#[test] +fn should_read_the_newest_retained_revision_of_a_deleted_document() { + let owner = [23u8; 32]; + let (drive, contract, id) = setup_history(4, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let DocumentLifecycleState::Deleted(newest) = lifecycle_of(&drive, &contract, id) else { + panic!("expected a deleted document"); + }; + assert_eq!( + newest.revision(), + Some(4), + "an erase start is authorized against the newest revision that survives" + ); + assert_eq!(newest.owner_id().to_buffer(), owner); +} + +/// A `Document` is only ever written under a keep-history type through the +/// history tree, so a deleted document's revisions must not be reachable by id. +#[test] +fn should_not_resolve_a_deleted_document_by_id() { + let owner = [24u8; 32]; + let (drive, contract, id) = setup_history(2, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_type_keeps_history: true, + document_id: id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + }; + let path_query = query + .construct_path_query(latest()) + .expect("expected a by-id path query"); + let (results, _) = drive + .grove_get_raw_path_query( + &path_query, + None, + grovedb::query_result_type::QueryResultType::QueryKeyElementPairResultType, + &mut vec![], + &latest().drive, + ) + .expect("expected the by-id read to run"); + assert!( + results.to_key_elements().is_empty(), + "a deleted document is not fetchable by id" + ); +} From 4b8d08d599b65fef90b67030afe1173cbbd2f6ce Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 00:54:25 +0700 Subject: [PATCH 11/48] test: pin the lifecycle through signed transitions and version slots Covers the validation rules end to end at protocol 14: delete then erase, a second delete refused, a create over a reserved id refused, an erase of a document that has not been deleted refused, an erase of a type that never asked for erasure refused, an erase carrying token payment information refused, and an erase start by an identity that does not own the document refused. The wire gate is pinned at protocols 12 and 13, where the erase kind does not exist, and the version slots carry semantic pins naming what each number selects rather than only its value. The token payment refusal moved from the erase structure validation to the transformer. The base action keeps only the cost the contract sets, not the payment the submitter offered, so by the time structure validation runs there is nothing left to see and the check could never have fired. Red before the move, green after. Co-Authored-By: Claude Opus 5 (1M context) --- .../validate_basic_structure/v0/mod.rs | 45 ++ .../advanced_structure_v0/mod.rs | 15 +- .../batch/tests/document/erase.rs | 542 ++++++++++++++++++ .../batch/tests/document/mod.rs | 1 + .../note-contract-keep-history-erasable.json | 36 ++ .../v0/transformer.rs | 34 ++ .../tests/keep_history_delete_versions.rs | 140 ++++- 7 files changed, 800 insertions(+), 13 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs create mode 100644 packages/rs-drive-abci/tests/supporting_files/contract/note/note-contract-keep-history-erasable.json diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index b9880c6620f..098d86e0142 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -376,6 +376,51 @@ mod tests { /// (`document_index_only_delete_state_transition` is `Some` in /// STATE_TRANSITION_SERIALIZATION_VERSIONS_V3), rejected below (where /// the kind's table entry is `None`). + /// Software that predates the erase kind cannot decode one at all, so no + /// historical block can contain one. The gate exists so that new software + /// agrees with old software while a pre-14 protocol version is still + /// active. + #[test] + fn validate_base_structure_v0_gates_erase_by_protocol_version() { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; + + let erase = + DocumentTransition::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: make_base(1, "note"), + })); + + let batch = make_batch_v0(vec![erase]); + + for protocol in [12, 13] { + let version = PlatformVersion::get(protocol).expect("the protocol version exists"); + let result = batch + .validate_base_structure_v0(version) + .expect("no protocol err"); + assert!( + result.errors.iter().any(|error| matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + )), + "protocol {protocol} must reject an erase as an unsupported version, got {:?}", + result.errors + ); + } + + let pv14 = PlatformVersion::get(14).expect("PV14 exists"); + let result = batch + .validate_base_structure_v0(pv14) + .expect("no protocol err"); + assert!( + !result.errors.iter().any(|error| matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + )), + "PV14 must admit an erase, got {:?}", + result.errors + ); + } + #[test] fn validate_base_structure_v0_gates_index_only_delete_by_protocol_version() { use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::DocumentIndexOnlyDeleteTransitionV0; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs index 5e15b74dcdc..fbcbecc2de9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs @@ -70,18 +70,9 @@ impl DocumentEraseTransitionActionStructureValidationV0 for DocumentEraseTransit } // Erase has no token cost of its own: the deletion it follows was paid - // for when the document was deleted. Offering one would let a - // continuation, which anyone may submit, move tokens. - if self.base().token_cost().is_some() { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidDocumentTransitionActionError::new(format!( - "an erase of a document of type {} must not carry token payment information", - document_type_name - )) - .into(), - )); - } - + // for when the document was deleted. A transition that offers to pay + // one is refused where the offer is still visible, when the transition + // becomes an action. Ok(SimpleConsensusValidationResult::new()) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs new file mode 100644 index 00000000000..7a5449c214e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -0,0 +1,542 @@ +//! Deleting and erasing keep-history documents through signed transitions. +//! +//! Erasure is authorized once. The document's owner commits it, and the record +//! that commitment leaves in state is what lets any identity finish the work, +//! so an owner who loses their keys, their funds or their permission cannot +//! strand a half-erased document. + +use super::*; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::TempPlatform; +use dpp::consensus::basic::BasicError; +use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::document::Document; +use dpp::identifier::Identifier; +use dpp::identity::{Identity, IdentityPublicKey, SecurityLevel}; +use dpp::prelude::IdentityNonce; +use dpp::state_transition::StateTransition; +use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; +use dpp::tokens::token_payment_info::TokenPaymentInfo; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::util::storage_flags::StorageFlags; +use rand::rngs::StdRng; +use rand::SeedableRng; +use simple_signer::signer::SimpleSigner; + +const ERASABLE_CONTRACT: &str = + "tests/supporting_files/contract/note/note-contract-keep-history-erasable.json"; + +fn process( + platform: &mut TempPlatform, + serialized: Vec, +) -> StateTransitionExecutionResult { + let state = platform.state.load(); + let version = state.current_platform_version().unwrap(); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &state, + &BlockInfo::default(), + &transaction, + version, + false, + None, + ) + .expect("expected transition processing"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + assert_eq!(result.execution_results().len(), 1); + result.into_execution_results().remove(0) +} + +fn assert_successful(result: &StateTransitionExecutionResult, context: &str) { + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "{context}" + ); +} + +/// A platform at protocol 14 with the erasable note contract applied, one +/// identity owning a `note` document, and a second identity that owns nothing. +struct Fixture { + platform: TempPlatform, + contract: DataContract, + owner: Identity, + owner_key: IdentityPublicKey, + owner_signer: SimpleSigner, + stranger: Identity, + stranger_key: IdentityPublicKey, + stranger_signer: SimpleSigner, + document: Document, + entropy: Bytes32, + nonce: IdentityNonce, + stranger_nonce: IdentityNonce, +} + +impl Fixture { + async fn new(document_type_name: &str) -> Self { + let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(14) + .build_with_mock_rpc() + .set_initial_state_structure(); + + let contract = json_document_to_contract(ERASABLE_CONTRACT, true, platform_version) + .expect("the erasable note contract must pass full validation at protocol 14"); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the contract"); + + let (owner, owner_signer, owner_key) = + setup_identity(&mut platform, 1001, dash_to_credits!(1.0)); + let (stranger, stranger_signer, stranger_key) = + setup_identity(&mut platform, 1002, dash_to_credits!(1.0)); + + let mut rng = StdRng::seed_from_u64(1291); + let document_type = contract + .document_type_for_name(document_type_name) + .expect("expected the document type"); + let entropy = Bytes32::random_with_rng(&mut rng); + let document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + owner.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random note"); + + let mut fixture = Fixture { + platform, + contract, + owner, + owner_key, + owner_signer, + stranger, + stranger_key, + stranger_signer, + document: document.clone(), + entropy, + nonce: 1, + stranger_nonce: 1, + }; + fixture.create(document, entropy, document_type_name).await; + fixture + } + + fn document_type(&self, name: &str) -> DocumentTypeRef<'_> { + self.contract + .document_type_for_name(name) + .expect("expected the document type") + } + + async fn create(&mut self, document: Document, entropy: Bytes32, document_type_name: &str) { + let platform_version = PlatformVersion::get(14).unwrap(); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + self.document_type(document_type_name), + entropy.0, + &self.owner_key, + self.nonce, + 0, + None, + &self.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a create transition"); + self.nonce += 1; + let result = process( + &mut self.platform, + transition.serialize_to_bytes().expect("serialized"), + ); + assert_successful(&result, "the create must succeed"); + } + + async fn delete_as_owner( + &mut self, + document_type_name: &str, + ) -> StateTransitionExecutionResult { + let platform_version = PlatformVersion::get(14).unwrap(); + let mut document = self.document.clone(); + document.set_revision(Some(1)); + let transition = BatchTransition::new_document_deletion_transition_from_document( + document, + self.document_type(document_type_name), + &self.owner_key, + self.nonce, + 0, + None, + &self.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a delete transition"); + self.nonce += 1; + process( + &mut self.platform, + transition.serialize_to_bytes().expect("serialized"), + ) + } + + async fn erase( + &mut self, + document_type_name: &str, + as_owner: bool, + token_payment_info: Option, + ) -> StateTransitionExecutionResult { + let platform_version = PlatformVersion::get(14).unwrap(); + let mut document = self.document.clone(); + document.set_revision(Some(1)); + if !as_owner { + document.set_owner_id(self.stranger.id()); + } + let transition = if let Some(token_payment_info) = token_payment_info { + // The erase factory refuses to carry token payment information, so + // a transition that does has to be built as a delete and rewritten + // into an erase, exactly as a hostile client would. + let delete = BatchTransition::new_document_deletion_transition_from_document( + document, + self.document_type(document_type_name), + &self.owner_key, + self.nonce, + 0, + Some(token_payment_info), + &self.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a delete transition to rewrite"); + self.nonce += 1; + rewrite_delete_as_erase( + delete, + &self.owner_key, + &self.owner_signer, + self.document_type(document_type_name) + .security_level_requirement(), + ) + .await + } else if as_owner { + let transition = BatchTransition::new_document_erase_transition_from_document( + document, + self.document_type(document_type_name), + &self.owner_key, + self.nonce, + 0, + &self.owner_signer, + platform_version, + None, + ) + .await + .expect("expected an erase transition"); + self.nonce += 1; + transition + } else { + let transition = BatchTransition::new_document_erase_transition_from_document( + document, + self.document_type(document_type_name), + &self.stranger_key, + self.stranger_nonce, + 0, + &self.stranger_signer, + platform_version, + None, + ) + .await + .expect("expected an erase transition"); + self.stranger_nonce += 1; + transition + }; + process( + &mut self.platform, + transition.serialize_to_bytes().expect("serialized"), + ) + } + + fn lifecycle(&self, document_type_name: &str) -> DocumentLifecycleState { + self.platform + .drive + .fetch_document_lifecycle( + &self.contract, + self.document_type(document_type_name), + self.document.id(), + None, + None, + PlatformVersion::get(14).unwrap(), + ) + .expect("expected to read the lifecycle") + .0 + } +} + +/// Rebuilds a signed batch, swapping its single delete transition for an erase +/// carrying the same base — including the token payment information an erase +/// must refuse. +async fn rewrite_delete_as_erase( + transition: StateTransition, + key: &IdentityPublicKey, + signer: &SimpleSigner, + security_level: SecurityLevel, +) -> StateTransition { + use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransition, DocumentEraseTransition, DocumentTransition, + }; + use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; + use dpp::state_transition::batch_transition::BatchTransition; + + let StateTransition::Batch(BatchTransition::V1(mut batch)) = transition else { + panic!("expected a v1 batch transition"); + }; + let BatchedTransition::Document(DocumentTransition::Delete(delete)) = + batch.transitions.remove(0) + else { + panic!("expected a single document delete"); + }; + batch + .transitions + .push(BatchedTransition::Document(DocumentTransition::Erase( + DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: delete.base().clone(), + }), + ))); + let mut rebuilt: StateTransition = BatchTransition::V1(batch).into(); + rebuilt + .sign_external( + key, + signer, + Some(|_: Identifier, _: String| Ok(security_level)), + ) + .await + .expect("expected to re-sign the rewritten batch"); + rebuilt +} + +#[tokio::test] +async fn should_delete_then_erase_a_keep_history_document() { + let mut fixture = Fixture::new("note").await; + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Active(_)); + + assert_successful( + &fixture.delete_as_owner("note").await, + "the delete must succeed", + ); + assert_matches!( + fixture.lifecycle("note"), + DocumentLifecycleState::Deleted(_) + ); + + assert_successful( + &fixture.erase("note", true, None).await, + "the erase must succeed", + ); + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Absent); +} + +/// A delete never escalates into anything that removes a revision, so asking +/// twice is a paid error rather than a second, deeper removal. +#[tokio::test] +async fn should_reject_a_second_delete_of_the_same_document() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the first delete"); + + let result = fixture.delete_as_owner("note").await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentNotFoundError(_)), + .. + } + ); +} + +/// The id stays taken while the revisions are retained, even though nothing in +/// the primary-key tree says so. +#[tokio::test] +async fn should_reject_a_create_over_a_deleted_documents_id() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let platform_version = PlatformVersion::get(14).unwrap(); + let document = fixture.document.clone(); + // The same id the deleted document had: a document id is derived from its + // creator, contract, type and entropy, so re-creating with the same inputs + // targets exactly the reserved id. + let entropy = fixture.entropy; + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + fixture.document_type("note"), + entropy.0, + &fixture.owner_key, + fixture.nonce, + 0, + None, + &fixture.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a create transition"); + let result = process( + &mut fixture.platform, + transition.serialize_to_bytes().expect("serialized"), + ); + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentAlreadyPresentError(_)), + .. + } + ); +} + +/// Delete and erase stay two intents. Erasing a document that is still current +/// would let one signature do the work of two. +#[tokio::test] +async fn should_reject_an_erase_of_a_document_that_has_not_been_deleted() { + let mut fixture = Fixture::new("note").await; + + let result = fixture.erase("note", true, None).await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError(BasicError::InvalidDocumentTransitionActionError(_)), + .. + } + ); + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Active(_)); +} + +/// A type that keeps history but never opted into erasure keeps its deleted +/// documents' revisions forever. +#[tokio::test] +async fn should_reject_an_erase_of_a_type_that_did_not_ask_for_it() { + let mut fixture = Fixture::new("permanentNote").await; + assert!(!fixture + .document_type("permanentNote") + .documents_can_be_erased()); + assert_successful( + &fixture.delete_as_owner("permanentNote").await, + "the delete must still succeed", + ); + + let result = fixture.erase("permanentNote", true, None).await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError(BasicError::InvalidDocumentTransitionActionError(_)), + .. + } + ); + assert_matches!( + fixture.lifecycle("permanentNote"), + DocumentLifecycleState::Deleted(_) + ); +} + +/// Erase has no token cost of its own. Accepting one would let a continuation, +/// which any identity may submit, move somebody else's tokens. +#[tokio::test] +async fn should_reject_an_erase_that_carries_token_payment_information() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let token_payment_info = TokenPaymentInfo::V0(TokenPaymentInfoV0 { + payment_token_contract_id: Some(Identifier::new([5u8; 32])), + token_contract_position: 0, + minimum_token_cost: None, + maximum_token_cost: Some(10), + gas_fees_paid_by: Default::default(), + }); + let result = fixture.erase("note", true, Some(token_payment_info)).await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError(BasicError::InvalidDocumentTransitionActionError(_)), + .. + } + ); + assert_matches!( + fixture.lifecycle("note"), + DocumentLifecycleState::Deleted(_) + ); +} + +/// The first erase is the authorized act, so it must come from the owner. +#[tokio::test] +async fn should_reject_an_erase_start_by_an_identity_that_does_not_own_the_document() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let result = fixture.erase("note", false, None).await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentOwnerIdMismatchError(_)), + .. + } + ); + assert_matches!( + fixture.lifecycle("note"), + DocumentLifecycleState::Deleted(_) + ); +} + +/// A document already invisible to every read is not there to be deleted +/// again, whether or not its erasure has begun. +#[tokio::test] +async fn should_reject_a_delete_of_a_document_that_has_already_been_deleted() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + assert_successful(&fixture.erase("note", true, None).await, "the erase"); + + let result = fixture.delete_as_owner("note").await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentNotFoundError(_)), + .. + } + ); +} + +/// An erase of an id that holds nothing is not silently accepted. +#[tokio::test] +async fn should_reject_an_erase_of_an_id_that_holds_nothing() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + assert_successful(&fixture.erase("note", true, None).await, "the erase"); + + let result = fixture.erase("note", true, None).await; + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentNotFoundError(_)), + .. + } + ); +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs index a1a23bbcbe9..d77ea19e15d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs @@ -1,6 +1,7 @@ mod creation; mod deletion; mod dpns; +mod erase; mod index_only; mod keep_history; mod nft; diff --git a/packages/rs-drive-abci/tests/supporting_files/contract/note/note-contract-keep-history-erasable.json b/packages/rs-drive-abci/tests/supporting_files/contract/note/note-contract-keep-history-erasable.json new file mode 100644 index 00000000000..eb9cc08e987 --- /dev/null +++ b/packages/rs-drive-abci/tests/supporting_files/contract/note/note-contract-keep-history-erasable.json @@ -0,0 +1,36 @@ +{ + "$formatVersion": "1", + "id": "5Bqs6itzfoDXzmgQibYZQABbqYsXmawVf7SKe3mKDQVf", + "ownerId": "2b994p95akyNFKtkDnDvBRUotDbkH54MHwGbhQLr5gcU", + "version": 1, + "keywords": [], + "documentSchemas": { + "note": { + "type": "object", + "documentsKeepHistory": true, + "documentsMutable": true, + "canBeDeleted": true, + "canBeErased": true, + "properties": { + "message": { "type": "string", "maxLength": 256, "position": 0 } + }, + "required": ["message"], + "additionalProperties": false, + "$comment": "A keep-history type whose deleted documents may also have their retained revisions purged. Deleting a note removes it from ordinary reads and leaves its revisions readable; erasing removes those revisions a bounded chunk at a time." + }, + "permanentNote": { + "type": "object", + "documentsKeepHistory": true, + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "message": { "type": "string", "maxLength": 256, "position": 0 } + }, + "required": ["message"], + "additionalProperties": false, + "$comment": "The same type without canBeErased: its deleted documents keep their revisions forever, which is what the erase refusal is checked against." + } + }, + "groups": {}, + "tokens": {} +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs index ac575d0f0b1..5f1262561ae 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs @@ -5,7 +5,12 @@ use crate::state_transition_action::batch::batched_transition::document_transiti use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::consensus::basic::document::InvalidDocumentTransitionActionError; +use dpp::consensus::basic::BasicError; +use dpp::consensus::ConsensusError; use dpp::fee::fee_result::FeeResult; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::platform_value::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; @@ -28,6 +33,35 @@ impl DocumentEraseTransitionActionV0 { > { let DocumentEraseTransitionV0 { base, .. } = value; + // Erase carries no token cost, so an offer to pay one has nothing to + // buy. Refused here rather than in structure validation because the + // base action keeps only the cost the contract sets, not the payment + // the submitter offered: by then there is nothing left to see. + // Accepting it would let a continuation, which any identity may submit, + // move somebody else's tokens. + if base.token_payment_info_ref().is_some() { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( + base, + owner_id, + user_fee_increase, + ); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + vec![ConsensusError::BasicError( + BasicError::InvalidDocumentTransitionActionError( + InvalidDocumentTransitionActionError::new(format!( + "an erase of document {} must not carry token payment information", + base.id() + )), + ), + )], + ), + FeeResult::default(), + )); + } + let base_action_validation_result = DocumentBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( base, diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index 81491b87f87..53184d151f2 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -1,5 +1,15 @@ +//! Semantic pins for the version slots the keep-history document lifecycle +//! rides on. +//! +//! A numeric pin alone would not say what the number means, so each assertion +//! below names the behaviour the slot selects and the released protocol +//! versions it must leave alone. + use platform_version::version::PlatformVersion; +/// The released protocol versions parse contracts with the generation that +/// predates the lifecycle grammar and refuse a keep-history delete before it +/// reaches storage. Both must replay identically forever. #[test] fn should_preserve_released_keep_history_validation_versions() { for protocol in [12, 13] { @@ -22,11 +32,49 @@ fn should_preserve_released_keep_history_validation_versions() { .batch_state_transition .document_delete_transition_structure_validation, 0, - "delete validation at protocol {protocol}" + "delete structure validation at protocol {protocol}" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_delete_transition_state_validation, + 0, + "delete state validation at protocol {protocol}" + ); + assert!( + version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .is_none(), + "the erase kind must not exist on the wire at protocol {protocol}" + ); + assert!( + version + .system_limits + .max_document_revisions_erased_per_transition + .is_none(), + "no erase can exist at protocol {protocol}, so no chunk bounds one" + ); + assert_eq!( + version + .drive + .methods + .document + .delete + .delete_document_for_contract_operations, + 0, + "the keep-history delete branch must not be selected at protocol {protocol}" ); } } +/// Protocol 14 selects the lifecycle: the parser generation that admits the new +/// keywords, the delete that consults the lifecycle, and the erase kind with +/// the bound on how much one transition may remove. #[test] fn should_activate_keep_history_validation_at_protocol_14() { let version = PlatformVersion::get(14).unwrap(); @@ -48,4 +96,94 @@ fn should_activate_keep_history_validation_at_protocol_14() { .document_delete_transition_structure_validation, 1 ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_delete_transition_state_validation, + 1 + ); + assert_eq!( + version + .drive + .methods + .document + .delete + .delete_document_for_contract_operations, + 1, + "protocol 14 selects the keep-history delete branch" + ); + + let bounds = version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .as_ref() + .expect("the erase kind joins the wire at protocol 14"); + assert_eq!(bounds.bounds.min_version, 0); + assert_eq!(bounds.bounds.max_version, 0); + assert_eq!(bounds.bounds.default_current_version, 0); + + let chunk = version + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the erase chunk"); + assert_eq!( + chunk, 100, + "the chunk bounds the work one erase can demand and the balance every \ + erase needs up front; changing it changes both" + ); +} + +/// The lifecycle read and the erase storage operation exist at exactly one +/// version each, so no table can select an implementation that is not there. +#[test] +fn should_expose_one_implementation_of_each_new_lifecycle_slot() { + for protocol in 1..=PlatformVersion::latest().protocol_version { + let Ok(version) = PlatformVersion::get(protocol) else { + continue; + }; + assert_eq!( + version + .drive + .methods + .document + .query + .fetch_document_lifecycle, + 0, + "lifecycle read at protocol {protocol}" + ); + assert_eq!( + version + .drive + .methods + .document + .delete + .erase_document_for_contract_operations, + 0, + "erase operation at protocol {protocol}" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_erase_transition_structure_validation, + 0, + "erase structure validation at protocol {protocol}" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_erase_transition_state_validation, + 0, + "erase state validation at protocol {protocol}" + ); + } } From 7fa1b614549450e2e91a0178a93e82b1a5338a16 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 01:04:27 +0700 Subject: [PATCH 12/48] fix(drive): read the erasing sentinel off the revision, not the time A lifecycle record says an erasure has begun by carrying the revision it began from and the time it began at. Reading that off the time made a block whose clock reads zero indistinguishable from a document whose erasure had never started, so the first chunk of such a block committed nothing and every chunk after it re-authorized as a fresh start. A history sequence is one or more for every real revision, so zero there can only mean the field was never written. Test: a document retaining more than one chunk is committed to erasure by its owner and finished by an identity that owns nothing, and a delete of it in between is refused. Red before this fix, where the erase start left the record saying merely deleted, green after. Also covered: two erases sharing one block, and a delete and the erase that follows it sharing one block, each seeing the previous transition's effect through the block transaction. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/document/erase.rs | 293 ++++++++++++++++++ .../src/drive/document/lifecycle/mod.rs | 15 +- 2 files changed, 307 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 7a5449c214e..8d936375d6d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -21,6 +21,9 @@ use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::TokenPaymentInfo; use dpp::version::PlatformVersion; use dpp::ProtocolError; +use drive::drive::document::history::{ + DocumentHistoryQueryV1, DocumentHistorySelector, DocumentHistoryState, +}; use drive::drive::document::lifecycle::DocumentLifecycleState; use drive::util::storage_flags::StorageFlags; use rand::rngs::StdRng; @@ -277,6 +280,123 @@ impl Fixture { ) } + /// Writes revisions straight into storage so the document retains more than + /// one erase chunk can remove. How they got there does not matter to an + /// erase: it reads what the history holds. + fn retain_revisions(&mut self, document_type_name: &str, revisions: u64) { + let platform_version = PlatformVersion::get(14).unwrap(); + let contract = self.contract.clone(); + let document_type = contract + .document_type_for_name(document_type_name) + .expect("expected the document type"); + // The same flags the create wrote, so overwriting the current pointer + // replaces exactly as many bytes as it holds. + let flags = Some(std::borrow::Cow::Owned(StorageFlags::new_single_epoch( + 0, + Some(self.owner.id().to_buffer()), + ))); + let mut document = self.document.clone(); + for revision in 2..=revisions { + document.set_revision(Some(revision)); + self.platform + .drive + .add_document_for_contract( + drive::util::object_size_info::DocumentAndContractInfo { + owned_document_info: drive::util::object_size_info::OwnedDocumentInfo { + document_info: + drive::util::object_size_info::DocumentInfo::DocumentRefInfo(( + &document, + flags.clone(), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + platform_version, + None, + ) + .expect("expected to retain a revision"); + } + } + + /// Builds an erase without processing it, so several can share one block. + async fn erase_transition(&mut self, document_type_name: &str, as_owner: bool) -> Vec { + let platform_version = PlatformVersion::get(14).unwrap(); + let mut document = self.document.clone(); + document.set_revision(Some(1)); + let (key, signer, nonce) = if as_owner { + let nonce = self.nonce; + self.nonce += 1; + (&self.owner_key, &self.owner_signer, nonce) + } else { + document.set_owner_id(self.stranger.id()); + let nonce = self.stranger_nonce; + self.stranger_nonce += 1; + (&self.stranger_key, &self.stranger_signer, nonce) + }; + BatchTransition::new_document_erase_transition_from_document( + document, + self.document_type(document_type_name), + key, + nonce, + 0, + signer, + platform_version, + None, + ) + .await + .expect("expected an erase transition") + .serialize_to_bytes() + .expect("serialized") + } + + fn remaining_revisions(&self, document_type_name: &str) -> u64 { + let query = DocumentHistoryQueryV1 { + contract_id: self.contract.id().to_buffer(), + document_type_name: document_type_name.to_string(), + document_id: self.document.id().to_buffer(), + selector: DocumentHistorySelector::StartAtTime(0), + limit: Some(1), + }; + self.platform + .drive + .fetch_document_history_v1( + &query, + self.document_type(document_type_name), + None, + PlatformVersion::get(14).unwrap(), + ) + .expect("expected to read the history") + .lifecycle + .remaining_revisions + } + + fn history_state(&self, document_type_name: &str) -> DocumentHistoryState { + let query = DocumentHistoryQueryV1 { + contract_id: self.contract.id().to_buffer(), + document_type_name: document_type_name.to_string(), + document_id: self.document.id().to_buffer(), + selector: DocumentHistorySelector::StartAtTime(0), + limit: Some(1), + }; + self.platform + .drive + .fetch_document_history_v1( + &query, + self.document_type(document_type_name), + None, + PlatformVersion::get(14).unwrap(), + ) + .expect("expected to read the history") + .lifecycle + .state + } + fn lifecycle(&self, document_type_name: &str) -> DocumentLifecycleState { self.platform .drive @@ -540,3 +660,176 @@ async fn should_reject_an_erase_of_an_id_that_holds_nothing() { } ); } + +/// Once an erasure is committed, the committed record is the authorization, so +/// an identity that owns nothing can finish the work. An owner who loses their +/// keys, their funds or their permission cannot strand a half-erased document. +#[tokio::test] +async fn should_let_any_identity_finish_an_erasure_its_owner_started() { + let chunk = PlatformVersion::get(14) + .unwrap() + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the chunk") as u64; + + let mut fixture = Fixture::new("note").await; + fixture.retain_revisions("note", chunk + 1); + assert_eq!( + fixture.remaining_revisions("note"), + chunk + 1, + "the fixture must retain more than one chunk can remove" + ); + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + // A stranger cannot start one. + assert_matches!( + fixture.erase("note", false, None).await, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentOwnerIdMismatchError(_)), + .. + } + ); + + assert_successful( + &fixture.erase("note", true, None).await, + "the owner commits the erasure", + ); + assert_eq!(fixture.history_state("note"), DocumentHistoryState::Erasing); + + // And now the same stranger can finish it. + assert_successful( + &fixture.erase("note", false, None).await, + "a continuation needs no authorization of its own", + ); + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Absent); +} + +/// A document whose erasure has begun is not there to be deleted again. +#[tokio::test] +async fn should_reject_a_delete_of_a_document_whose_erasure_has_begun() { + let chunk = PlatformVersion::get(14) + .unwrap() + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the chunk") as u64; + + let mut fixture = Fixture::new("note").await; + fixture.retain_revisions("note", chunk + 1); + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + assert_successful(&fixture.erase("note", true, None).await, "the erase start"); + assert_eq!(fixture.history_state("note"), DocumentHistoryState::Erasing); + + assert_matches!( + fixture.delete_as_owner("note").await, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentNotFoundError(_)), + .. + } + ); +} + +/// Two erases in one block each see the previous one's effect, because each +/// transition is applied into the block transaction before the next is +/// validated. +#[tokio::test] +async fn should_finish_an_erasure_across_two_transitions_in_one_block() { + let chunk = PlatformVersion::get(14) + .unwrap() + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the chunk") as u64; + + let mut fixture = Fixture::new("note").await; + fixture.retain_revisions("note", chunk + 1); + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let first = fixture.erase_transition("note", true).await; + let second = fixture.erase_transition("note", false).await; + + let state = fixture.platform.state.load(); + let version = state.current_platform_version().unwrap(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let result = fixture + .platform + .platform + .process_raw_state_transitions( + &[first, second], + &state, + &BlockInfo::default(), + &transaction, + version, + false, + None, + ) + .expect("expected transition processing"); + fixture + .platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + + assert_eq!(result.valid_count(), 2, "both erases must execute"); + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Absent); +} + +/// A delete and the erase that follows it can share a block: the erase reads +/// the lifecycle record the delete has already written into the same +/// transaction. +#[tokio::test] +async fn should_delete_and_erase_in_one_block() { + let mut fixture = Fixture::new("note").await; + + let platform_version = PlatformVersion::get(14).unwrap(); + let mut document = fixture.document.clone(); + document.set_revision(Some(1)); + let delete = BatchTransition::new_document_deletion_transition_from_document( + document.clone(), + fixture.document_type("note"), + &fixture.owner_key, + fixture.nonce, + 0, + None, + &fixture.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a delete transition") + .serialize_to_bytes() + .expect("serialized"); + fixture.nonce += 1; + let erase = fixture.erase_transition("note", true).await; + + let state = fixture.platform.state.load(); + let version = state.current_platform_version().unwrap(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let result = fixture + .platform + .platform + .process_raw_state_transitions( + &[delete, erase], + &state, + &BlockInfo::default(), + &transaction, + version, + false, + None, + ) + .expect("expected transition processing"); + fixture + .platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + + assert_eq!( + result.valid_count(), + 2, + "the erase must see the delete that shares its block" + ); + assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Absent); +} diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index 7b173760ce3..3c26b8fef6e 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -97,8 +97,13 @@ impl DocumentLifecycleRecord { /// Whether an authorized erasure has already started, which is what lets a /// continuation run without any authorization of its own. + /// + /// Read off the revision the erasure started from rather than the time it + /// started at: a history sequence is one or more for every real revision, + /// so zero can only mean the field was never written, while a block time of + /// zero is a value a clock could in principle produce. pub fn is_erasing(&self) -> bool { - self.erasing_started_at_ms() != 0 + self.erasing_from_revision() != 0 } /// Returns the record an erase start writes over this one, keeping the @@ -207,6 +212,14 @@ mod record_tests { assert_eq!(record.erasing_from_revision(), 0); } + /// A block whose time is zero must not make a committed erasure look like + /// one that never started. + #[test] + fn should_report_an_erasure_started_in_a_block_at_time_zero_as_erasing() { + let record = DocumentLifecycleRecord::deleted_at(0).starting_erase_at(0, 0, 1); + assert!(record.is_erasing()); + } + #[test] fn should_reject_records_that_do_not_consume_their_whole_input() { let mut bytes = DocumentLifecycleRecord::deleted_at(7).serialize(); From b50990d62343849680eb5beffb02d48037ab2632 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 01:36:54 +0700 Subject: [PATCH 13/48] test: exercise the lifecycle over migrated state and end to end The protocol 13 to 14 upgrade test now deletes and erases the document the activation migrated, so the lifecycle is pinned against revision keys and trees the migration produced rather than only against history written after activation. A contract registered before protocol 14 cannot declare canBeErased, since the grammar that admits the keyword did not exist then, so this exercises the storage operations directly. A functional spec covers the whole story against a running network: register a keep-history erasable contract, edit a note into a history, delete it and watch it leave ordinary reads while its revisions stay readable, fail to delete it twice, then erase it. Its erase and history calls go through the Evo SDK, which is where those surfaces live. The stale reference-validation comment is corrected: a contract update may withdraw deletion but never grant it, which is what keeps a validated reference from ever dangling. Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/KeepHistoryDocument.spec.js | 227 ++++++++++++++++++ .../document_reference_validation/v0/mod.rs | 12 +- .../batch/state/v0/fetch_documents.rs | 1 + .../test_cases/upgrade_fork_tests.rs | 71 ++++++ .../rs-drive/src/drive/document/delete/mod.rs | 1 - .../platform/documents/transitions/erase.rs | 1 - packages/wasm-sdk/src/queries/document.rs | 20 ++ 7 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js diff --git a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js new file mode 100644 index 00000000000..0901c587db9 --- /dev/null +++ b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js @@ -0,0 +1,227 @@ +const Dash = require('dash'); +const { expect } = require('chai'); + +const createClientWithFundedWallet = require('../../../lib/test/createClientWithFundedWallet'); +const waitForSTPropagated = require('../../../lib/waitForSTPropagated'); +const { getEvoSdkForNetwork } = require('../../../lib/test/createPlatformProofVerifier'); + +const { + Errors: { + StateTransitionBroadcastError, + }, +} = Dash; + +/** + * The identity's first key and a signer holding its private key, in the shape + * the Evo SDK's write methods expect. The legacy client owns the wallet, so the + * key has to be lifted out of it. + * + * @param {Object} evo + * @param {Object} evoSdk + * @param {Object} client + * @param {Object} identity + * @returns {Promise<{ identityKey: Object, signer: Object }>} + */ +async function evoSignerFor(evo, evoSdk, client, identity) { + const account = await client.getWalletAccount(); + const { privateKey } = account.identities.getIdentityHDKeyById( + identity.getId().toString(), + 0, + ); + + const signer = new evo.IdentitySigner(); + signer.addKeyFromWif(privateKey.toWIF()); + + const fetched = await evoSdk.identities.fetch(identity.getId().toString()); + const identityKey = fetched.getPublicKeyById(0); + + return { identityKey, signer }; +} + +describe('Platform', () => { + describe('KeepHistoryDocument', function main() { + this.timeout(900000); + + let client; + let evo; + let evoSdk; + let identity; + let dataContract; + let note; + + /** + * The revision count and lifecycle metadata Platform authenticates for the + * note, read through the Evo SDK's history query. + */ + const readHistory = async () => { + const result = await evoSdk.documents.history({ + dataContractId: dataContract.getId().toString(), + documentTypeName: 'note', + documentId: note.getId().toString(), + startAtMs: 0, + limit: 10, + }); + + return result.lifecycle; + }; + + before(async () => { + client = await createClientWithFundedWallet(400000000); // 4 Dash + + identity = await client.platform.identities.register(200000000); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + + ({ evo, sdk: evoSdk } = await getEvoSdkForNetwork(client.getNetwork())); + }); + + after(async () => { + if (client) { + await client.disconnect(); + } + }); + + it('should register a contract whose notes keep history and may be erased', async () => { + dataContract = await client.platform.contracts.create({ + note: { + type: 'object', + documentsKeepHistory: true, + documentsMutable: true, + canBeDeleted: true, + canBeErased: true, + properties: { + message: { + type: 'string', + maxLength: 256, + position: 0, + }, + }, + required: ['message'], + additionalProperties: false, + }, + }, identity); + + await client.platform.contracts.publish(dataContract, identity); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + + client.getApps().set('notes', { + contractId: dataContract.getId(), + contract: dataContract, + }); + + const fetched = await client.platform.contracts.get(dataContract.getId()); + expect(fetched.toObject().documentSchemas.note.canBeErased).to.be.true(); + }); + + it('should keep every revision of a note that is edited', async () => { + note = await client.platform.documents.create( + 'notes.note', + identity, + { message: 'first' }, + ); + + await client.platform.documents.broadcast({ create: [note] }, identity); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + + for (const message of ['second', 'third']) { + note.set('message', message); + await client.platform.documents.broadcast({ replace: [note] }, identity); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + } + + const lifecycle = await readHistory(); + expect(lifecycle.state).to.equal('ACTIVE'); + expect(lifecycle.remainingRevisions).to.equal(3n); + expect(lifecycle.deletedAtMs).to.equal(0n); + }); + + it('should hide a deleted note while keeping its revisions readable', async () => { + await client.platform.documents.broadcast({ delete: [note] }, identity); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + + const [found] = await client.platform.documents.get( + 'notes.note', + { where: [['$id', '==', note.getId()]] }, + ); + expect(found).to.be.undefined(); + + const lifecycle = await readHistory(); + expect(lifecycle.state).to.equal('DELETED'); + expect( + lifecycle.remainingRevisions, + 'a delete removes no revision', + ).to.equal(3n); + expect(lifecycle.deletedAtMs > 0n).to.be.true(); + expect(lifecycle.erasingStartedAtMs).to.equal(0n); + }); + + it('should refuse to delete the same note twice', async () => { + let broadcastError; + + try { + await client.platform.documents.broadcast({ delete: [note] }, identity); + } catch (e) { + broadcastError = e; + } + + expect(broadcastError).to.be.an.instanceOf(StateTransitionBroadcastError); + // DocumentNotFoundError: a document nothing can read is not there to be + // deleted again, and a delete never escalates into removing a revision. + expect(broadcastError.code).to.equal(40101); + }); + + it('should erase the retained revisions of a deleted note', async () => { + const { identityKey, signer } = await evoSignerFor(evo, evoSdk, client, identity); + + await evoSdk.documents.erase({ + document: { + id: note.getId().toString(), + ownerId: identity.getId().toString(), + dataContractId: dataContract.getId().toString(), + documentTypeName: 'note', + }, + identityKey, + signer, + }); + + // Additional wait time to mitigate testnet latency + await waitForSTPropagated(); + + const lifecycle = await readHistory(); + expect(lifecycle.state).to.equal('ABSENT'); + expect(lifecycle.remainingRevisions).to.equal(0n); + }); + + it('should refuse to erase a note that has already been erased', async () => { + const { identityKey, signer } = await evoSignerFor(evo, evoSdk, client, identity); + + let eraseError; + + try { + await evoSdk.documents.erase({ + document: { + id: note.getId().toString(), + ownerId: identity.getId().toString(), + dataContractId: dataContract.getId().toString(), + documentTypeName: 'note', + }, + identityKey, + signer, + }); + } catch (e) { + eraseError = e; + } + + expect(eraseError, 'an id that holds nothing is not erasable').to.exist(); + }); + }); +}); 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 9ca6a929874..f5c818b6b3e 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 @@ -252,10 +252,14 @@ fn validate_document_type_references_v0( )); }; - // Only document types whose documents can never be deleted may be - // referenced: `canBeDeleted` is immutable on contract updates and - // document types can not be removed, so a reference validated here - // can never dangle + // Only document types whose documents can never be deleted may + // be referenced. A contract update may withdraw deletion but + // never grant it, and document types can not be removed, so a + // type that is referenceable when a reference is validated + // stays referenceable and the reference can never dangle. + // Erasure is out of the question for the same reason: it + // applies to deleted documents only, and a type that allows it + // must allow deletion. if referenced_document_type.documents_can_be_deleted() { return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedDocumentTypeDeletableError::new( 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 006e90db6d6..96708df49e8 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 @@ -220,6 +220,7 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( /// and bills the reads it performed. Every stateful check that has to tell a /// deleted document from one that never existed goes through this; a document /// type that keeps no history has no lifecycle to read and must not reach it. +#[allow(clippy::too_many_arguments)] pub(crate) fn fetch_keep_history_document_lifecycle( drive: &Drive, contract: &DataContract, diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs index 99a14319d3f..f78e7a8371e 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs @@ -1343,6 +1343,77 @@ mod tests { .unwrap(), root ); + + // The lifecycle works over migrated storage, not only over history + // written after activation. A contract registered before protocol 14 + // cannot declare `canBeErased` — the grammar that admits the keyword + // did not exist then — so this exercises the storage layer directly: + // whether a delete and an erase read and rewrite revision keys the + // migration produced. + use drive::drive::document::lifecycle::DocumentLifecycleState; + use drive::util::batch::{DocumentOperationType, DriveOperation}; + use drive::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; + + let document_id = dpp::identifier::Identifier::from_bytes(&document_id).unwrap(); + let document_type = contract.document_type_for_name("note").unwrap(); + let contract_info = || DataContractInfo::BorrowedDataContract(&contract); + let document_type_info = || DocumentTypeInfo::DocumentTypeName("note".to_string()); + let apply = |operation: DriveOperation, time_ms: u64| { + restarted + .drive + .apply_drive_operations( + vec![operation], + true, + &dpp::block::block_info::BlockInfo::default_with_time(time_ms), + None, + new_version, + None, + ) + .expect("expected to apply the operation") + }; + let lifecycle = || { + restarted + .drive + .fetch_document_lifecycle( + &contract, + document_type, + document_id, + None, + None, + new_version, + ) + .expect("expected to read the lifecycle") + .0 + }; + + apply( + DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { + document_id, + deleter_id: Some(dpp::identifier::Identifier::new([3u8; 32])), + contract_info: contract_info(), + document_type_info: document_type_info(), + }), + 1_681_094_400_000, + ); + let state = lifecycle(); + assert!( + matches!(state, DocumentLifecycleState::Deleted(_)), + "a migrated document must be deletable, got {state:?}" + ); + + apply( + DriveOperation::DocumentOperation(DocumentOperationType::EraseDocument { + document_id, + contract_info: contract_info(), + document_type_info: document_type_info(), + }), + 1_681_094_500_000, + ); + let state = lifecycle(); + assert!( + matches!(state, DocumentLifecycleState::Absent), + "erasing every migrated revision must free the id, got {state:?}" + ); } #[stack_size(4 * 1024 * 1024)] diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 0a92a23bf7c..669cba5a0a7 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1366,7 +1366,6 @@ mod tests { outcome.expect("expected the delete to succeed"); - let document_id = Identifier::from(document_id); let (state, _) = drive .fetch_document_lifecycle( &contract, diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index 8a26ee63c08..e5b92ca3f94 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -178,7 +178,6 @@ impl Sdk { ) -> Result { let platform_version = self.version(); let put_settings = erase_document_transition_builder.settings; - let document_id = erase_document_transition_builder.document_id; let state_transition = erase_document_transition_builder .sign(self, signing_key, signer, platform_version) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 2ab8d5d61b9..787d9a76d2f 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -185,6 +185,26 @@ export interface DocumentHistoryQuery { /** Maximum ten entries. A single revision requires limit one. */ limit?: number; } +export interface DocumentHistoryResult { + entries: { timeMs: bigint; revision: bigint; document: Document }[]; + lifecycle: { + /** + * ACTIVE while the document is visible to ordinary reads, DELETED once it + * has been deleted and its revisions are retained, ERASING once an + * authorized erasure has begun, ABSENT when nothing is left. + */ + state: "ACTIVE" | "DELETED" | "ERASING" | "ABSENT"; + remainingRevisions: bigint; + /** Zero unless the document has been deleted. */ + deletedAtMs: bigint; + /** Zero unless an authorized erasure has begun. */ + erasingStartedAtMs: bigint; + /** Timestamp of the newest revision retained when the erasure began. */ + erasingFromTimeMs: bigint; + /** History sequence of that revision. */ + erasingFromRevision: bigint; + }; +} "#; #[wasm_bindgen] From aee2225338b704ab69d57c159a8769426dd66df7 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 01:52:13 +0700 Subject: [PATCH 14/48] test(wasm-sdk): pin the erasing state and its times as exact BigInts A millisecond timestamp does not survive a JavaScript number, so every lifecycle time crosses the boundary the same way the counts and revisions beside it already do. The TypeScript declaration names the two new states and the four times a caller can now read. Co-Authored-By: Claude Opus 5 (1M context) --- packages/wasm-sdk/src/queries/document.rs | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 787d9a76d2f..4fbbe155ad3 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -1653,6 +1653,7 @@ mod history_wasm_tests { lifecycle: Some(DocumentHistoryLifecycle { state: DocumentHistoryState::Active, remaining_revisions: count, + times: Default::default(), }), }; let result = JsValue::from( @@ -1677,6 +1678,47 @@ mod history_wasm_tests { ); } + /// Every lifecycle time crosses into JavaScript as an exact BigInt, like + /// the counts and revisions beside them: a millisecond timestamp does not + /// survive a JavaScript number. + #[wasm_bindgen_test] + fn should_report_the_erasing_state_and_its_times_exactly_in_javascript() { + use drive::drive::document::history::DocumentHistoryLifecycleTimes; + + let started_at = (1u64 << 53) + 3; + let history = DocumentHistory { + entries: vec![], + lifecycle: Some(DocumentHistoryLifecycle { + state: DocumentHistoryState::Erasing, + remaining_revisions: 7, + times: DocumentHistoryLifecycleTimes { + deleted_at_ms: 1_700_000_000_001, + erasing_started_at_ms: started_at, + erasing_from_time_ms: 1_700_000_000_002, + erasing_from_revision: 42, + }, + }), + }; + let result = document_history_to_js(history, [1; 32].into(), "note").unwrap(); + let lifecycle = Reflect::get(&result, &"lifecycle".into()).unwrap(); + assert_eq!( + Reflect::get(&lifecycle, &"state".into()).unwrap(), + JsValue::from_str("ERASING") + ); + for (key, expected) in [ + ("deletedAtMs", 1_700_000_000_001u64), + ("erasingStartedAtMs", started_at), + ("erasingFromTimeMs", 1_700_000_000_002), + ("erasingFromRevision", 42), + ] { + assert_eq!( + Reflect::get(&lifecycle, &key.into()).unwrap(), + JsValue::from(expected), + "{key} must survive as an exact BigInt" + ); + } + } + #[wasm_bindgen_test] fn should_serialize_history_data_in_a_proof_metadata_response() { let history = DocumentHistory { From a48194000a0a245a2fb97b195b85af46599bc500 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 01:58:43 +0700 Subject: [PATCH 15/48] test(drive): pin the aggregates and index entries a delete releases The counted and summed entry of a keep-history type is its current pointer, so removing it decrements both aggregates by construction and a deleted document stops contributing without any bookkeeping of its own. Its index entries go with it, freeing the values it held for another document to take, while every revision stays where it is. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/drive/document/lifecycle/tests.rs | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index da6e6600541..964f9b0355b 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -763,3 +763,149 @@ fn should_not_resolve_a_deleted_document_by_id() { "a deleted document is not fetchable by id" ); } + +/// The counted and summed entry of a keep-history type is its current pointer, +/// so removing it decrements both aggregates by construction and a deleted +/// document stops contributing without any bookkeeping of its own. +#[test] +fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_deleted() { + use crate::drive::document::paths::contract_document_type_path_vec; + use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; + use dpp::data_contract::DataContractFactory; + use dpp::document::document_factory::DocumentFactory; + use dpp::platform_value::platform_value; + use grovedb::Element; + + let version = latest(); + let schema = platform_value!({ + "type": "object", + "documentsKeepHistory": true, + "canBeDeleted": true, + "documentsCountable": true, + "documentsSummable": "amount", + "properties": { + "amount": {"type": "integer", "minimum": 0, "maximum": 4294967295i64, "position": 0}, + }, + "required": ["amount"], + "additionalProperties": false, + "indices": [{"name": "amount", "properties": [{"amount": "asc"}]}], + }); + let contract = DataContractFactory::new(version.protocol_version) + .expect("expected a contract factory") + .create_with_value_config( + [7; 32].into(), + 0, + platform_value!({ "tip": schema }), + None, + None, + ) + .expect("a countable summable keep-history type must parse") + .data_contract_owned(); + + let drive = setup_drive_with_initial_state_structure(None); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .expect("expected to apply the contract"); + let document_type = contract + .document_type_for_name("tip") + .expect("expected the tip type"); + assert!(document_type.documents_countable()); + + let mut document = DocumentFactory::new(version.protocol_version) + .expect("expected a document factory") + .create_document( + &contract, + [7; 32].into(), + "tip".into(), + platform_value!({"amount": 5}), + ) + .expect("expected a tip"); + document.set_id([9; 32].into()); + for revision in 1..=3u64 { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .expect("expected to write a revision"); + } + + let type_path = contract_document_type_path_vec(contract.id_ref().as_bytes(), "tip"); + let aggregates = || { + let element = drive + .grove + .get_raw( + type_path.as_slice().into(), + &[0], + None, + &latest().drive.grove_version, + ) + .value + .expect("expected the primary-key tree"); + let Element::CountSumTree(_, count, sum, _) = element else { + panic!("a countable summable type stores its documents in a count-sum tree"); + }; + (count, sum) + }; + assert_eq!( + aggregates(), + (1, 5), + "one live document contributing its amount, whatever its history holds" + ); + + let batch = drive + .delete_document_for_contract_operations( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(5_000), + Some(Identifier::new([7; 32])), + None, + &mut None, + None, + version, + ) + .expect("expected to delete"); + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut vec![], &version.drive) + .expect("expected to apply the delete"); + + assert_eq!( + aggregates(), + (0, 0), + "a deleted document contributes to neither aggregate, though its revisions remain" + ); + + // Its index entries are gone too, so the value it held is free for another + // document to take. + let mut index_path = type_path.clone(); + index_path.push(b"amount".to_vec()); + let mut query = grovedb::Query::new(); + query.insert_all(); + let (results, _) = drive + .grove_get_raw_path_query( + &grovedb::PathQuery::new(index_path, grovedb::SizedQuery::new(query, Some(10), None)), + None, + grovedb::query_result_type::QueryResultType::QueryKeyElementPairResultType, + &mut vec![], + &version.drive, + ) + .expect("expected to read the index"); + assert!( + results.to_key_elements().is_empty(), + "a delete removes every index reference that led to the document" + ); +} From 47284163bf95866cc367ee4ccd55b776c8de447c Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 03:11:45 +0700 Subject: [PATCH 16/48] fix(sdk): wait for an erase's affected state, not for proved execution An erase acts on a document that was already deleted, so its by-id proof shows the same absence whether or not it ran, and the proof classifier reports it as affected state. The strict wait refuses exactly that, so every valid erase came back to the caller as ExecutionNotProved: the transition had executed, revisions were gone, and the SDK reported a failure. Retrying on that error submits another chunk or hits not found after a terminal one. It now takes the affected-state wait, and the result is named for what it is: the document is absent by id as of the proof's block, which it already was, so it is an observation of the state the erase affected rather than evidence that this erase executed. The lifecycle read stays the way to learn how much history is left. Test: the outcome an erase produces, driven through both gates. Red before the fix, where the strict wait the erase used refuses its own valid outcome; green after. A companion test pins the classification itself, asserting an erase is affected state while a delete over the very same proof stays execution-proved. Co-Authored-By: Claude Opus 5 (1M context) --- .../v0/mod.rs | 154 ++++++++++++++++++ .../platform/documents/transitions/erase.rs | 109 +++++++++---- .../src/platform/transition/broadcast.rs | 35 +++- 3 files changed, 262 insertions(+), 36 deletions(-) diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 1888979041e..7a80fb40d4f 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -3442,6 +3442,160 @@ mod tests { } } + /// An erase acts on a document that was already deleted, so the by-id + /// proof shows the same absence whether or not it ran. The classifier must + /// report that as affected state, while a delete over the very same proof + /// stays execution-proved: presence before and absence after do bind a + /// delete's execution. + #[test] + fn verify_batch_document_erase_is_affected_state_not_execution_proved() { + use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + use dpp::document::DocumentV0Setters; + use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::{ + DocumentEraseTransition, DocumentEraseTransitionV0, + }; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::{ + DocumentDeleteTransition, DocumentDeleteTransitionV0, + }; + use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV0}; + use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract( + "tests/supporting_files/contract/family/family-contract-with-history.json", + false, + platform_version, + ) + .expect("expected the family history contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + None, + None, + platform_version, + ) + .expect("expected to apply the contract"); + let document_type = contract + .document_type_for_name("person") + .expect("expected the person type"); + let mut document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some([4u8; 32].into()), + document_type, + platform_version, + ) + .expect("expected a person"); + for revision in 1..=2u64 { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + platform_version, + None, + ) + .expect("expected to write a revision"); + } + let doc_id = document.id(); + drive + .delete_document_for_contract( + doc_id, + &contract, + "person", + BlockInfo::default_with_time(5_000), + true, + None, + platform_version, + None, + ) + .expect("expected the delete to succeed"); + + let path_query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".to_string(), + document_type_keeps_history: true, + document_id: doc_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + } + .construct_path_query(platform_version) + .expect("expected a by-id path query"); + let proof = drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("expected an absence proof"); + + let base = || { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: doc_id, + identity_contract_nonce: 1, + document_type_name: "person".to_string(), + data_contract_id: contract.id(), + }) + }; + let batch = |transition: DocumentTransition| { + StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![transition], + ..Default::default() + })) + }; + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + let verify = |state_transition: StateTransition| { + Drive::verify_state_transition_was_executed_with_proof( + &state_transition, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed") + .1 + }; + + let erase_outcome = verify(batch(DocumentTransition::Erase( + DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: base() }), + ))); + match erase_outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedDocuments(documents), + ) => { + assert_eq!(documents.len(), 1); + let (id, found) = documents.into_iter().next().unwrap(); + assert_eq!(id, doc_id); + assert!(found.is_none(), "the document is absent by id"); + } + other => panic!("an erase must be classified as affected state, got {other:?}"), + } + + let delete_outcome = verify(batch(DocumentTransition::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { base: base() }), + ))); + assert!( + matches!( + delete_outcome, + StateTransitionProofOutcome::ExecutionProved(_) + ), + "a delete over the same proof stays execution-proved, got {delete_outcome:?}" + ); + } + // ----------------------------------------------------------------------- // Batch: document create happy path // ----------------------------------------------------------------------- diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index e5b92ca3f94..4d09c549476 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -149,14 +149,48 @@ impl DocumentEraseTransitionBuilder { } } -/// What one erase transition achieved. +/// What one erase transition left behind. #[derive(Debug)] pub enum DocumentEraseResult { - /// The erase was accepted. Whether it removed the last revision is not - /// something the transition's own proof can show, because the document was - /// already absent from ordinary reads before it ran — read the current - /// lifecycle with [`Sdk::document_current_lifecycle`] to find out. - Accepted(Identifier), + /// The document is absent by id as of the proof's block. It already was + /// before the erase ran, so this is an observation of the state the erase + /// affected, not evidence that this erase executed — read the current + /// lifecycle with [`Sdk::document_current_lifecycle`] to find out how much + /// history is left. + AbsentAsOfProof(Identifier), +} + +/// Reads the observation an erase leaves behind out of the verified result. +/// +/// Its own function so a test can drive it with the outcome an erase actually +/// produces: the proof classifier reports an erase as affected state, which the +/// strict wait refuses, so the erase has to take the affected-state wait and +/// this has to accept what that wait returns. +fn erase_observation(result: StateTransitionProofResult) -> Result { + match result { + StateTransitionProofResult::VerifiedDocuments(documents) => { + if let Some((erased_id, None)) = documents.into_iter().next() { + Ok(DocumentEraseResult::AbsentAsOfProof(erased_id)) + } else { + Err(Error::DriveProofError( + drive::error::proof::ProofError::UnexpectedResultProof( + "expected an absent document in the VerifiedDocuments result for an \ + erase transition" + .to_string(), + ), + vec![], + Default::default(), + )) + } + } + _ => Err(Error::DriveProofError( + drive::error::proof::ProofError::UnexpectedResultProof( + "expected VerifiedDocuments for a document erase transition".to_string(), + ), + vec![], + Default::default(), + )), + } } impl Sdk { @@ -167,8 +201,10 @@ impl Sdk { /// one transition may remove needs several, and this method broadcasts one. /// /// The proof this returns authenticates that the document is absent by id, - /// which it already was before the erase ran, so it is evidence about the - /// state, not about this transition having executed. Use + /// which it already was before the erase ran, so it is an observation of + /// the state the erase affected rather than evidence that this erase + /// executed. That is why it takes the affected-state wait: the strict wait + /// refuses exactly this classification. Use /// [`Sdk::document_current_lifecycle`] to observe how much history is left. pub async fn document_erase>( &self, @@ -184,33 +220,10 @@ impl Sdk { .await?; let proof_result = state_transition - .broadcast_and_wait::(self, put_settings) + .broadcast_and_wait_for_affected_state::(self, put_settings) .await?; - match proof_result { - StateTransitionProofResult::VerifiedDocuments(documents) => { - if let Some((erased_id, None)) = documents.into_iter().next() { - Ok(DocumentEraseResult::Accepted(erased_id)) - } else { - Err(Error::DriveProofError( - drive::error::proof::ProofError::UnexpectedResultProof( - "expected an absent document in the VerifiedDocuments result for an \ - erase transition" - .to_string(), - ), - vec![], - Default::default(), - )) - } - } - _ => Err(Error::DriveProofError( - drive::error::proof::ProofError::UnexpectedResultProof( - "expected VerifiedDocuments for a document erase transition".to_string(), - ), - vec![], - Default::default(), - )), - } + erase_observation(proof_result) } /// Reads where a document stands in its lifecycle right now. @@ -251,3 +264,33 @@ impl Sdk { })) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + /// The erase reads its observation out of exactly the result the + /// affected-state wait hands back, and refuses anything else. + #[test] + fn should_read_an_absent_document_as_the_erases_observation() { + let id = Identifier::from([9u8; 32]); + let observation = erase_observation(StateTransitionProofResult::VerifiedDocuments( + BTreeMap::from([(id, None)]), + )) + .expect("an absent document is what an erase leaves behind"); + assert!(matches!( + observation, + DocumentEraseResult::AbsentAsOfProof(seen) if seen == id + )); + + let document = dpp::document::Document::V0(Default::default()); + erase_observation(StateTransitionProofResult::VerifiedDocuments( + BTreeMap::from([(id, Some(document))]), + )) + .expect_err("a document that is still there did not survive an erase"); + + erase_observation(StateTransitionProofResult::VerifiedTokenBalanceAbsence(id)) + .expect_err("only a document result can describe an erase"); + } +} diff --git a/packages/rs-sdk/src/platform/transition/broadcast.rs b/packages/rs-sdk/src/platform/transition/broadcast.rs index 8dfc8152d61..8c2b1b83360 100644 --- a/packages/rs-sdk/src/platform/transition/broadcast.rs +++ b/packages/rs-sdk/src/platform/transition/broadcast.rs @@ -196,9 +196,7 @@ impl BroadcastStateTransition for StateTransition { settings: Option, ) -> Result<(T, ResponseMetadata), Error> { let (outcome, metadata) = self.wait_for_outcome_with_metadata(sdk, settings).await?; - // An execution-proved outcome carries a strictly stronger guarantee - // than the requested snapshot, so both tags are accepted here. - convert_proof_result::(outcome.into_result()).map(|converted| (converted, metadata)) + convert_proof_result::(accept_affected_state(outcome)).map(|c| (c, metadata)) } async fn broadcast_and_wait_for_affected_state< @@ -277,6 +275,12 @@ fn require_execution_proved( } } +/// Accept either tag for the affected-state wait APIs: an execution-proved +/// outcome carries a strictly stronger guarantee than the snapshot requested. +fn accept_affected_state(outcome: StateTransitionProofOutcome) -> StateTransitionProofResult { + outcome.into_result() +} + /// Convert the verified inner result into the caller's expected type. fn convert_proof_result>( result: StateTransitionProofResult, @@ -476,4 +480,29 @@ mod tests { require_execution_proved(StateTransitionProofOutcome::ExecutionProved(proved)) .expect("execution-proved outcomes must pass the strict wait"); } + + /// The outcome an erase produces, run through both gates. A transition + /// family the classifier reports as affected state cannot use the strict + /// wait: its own valid proof would come back as an error. + #[test] + fn affected_state_wait_accepts_what_the_strict_wait_refuses() { + use std::collections::BTreeMap; + + let erased = || { + StateTransitionProofResult::VerifiedDocuments(BTreeMap::from([( + Identifier::from([9u8; 32]), + None, + )])) + }; + + let err = require_execution_proved(StateTransitionProofOutcome::AffectedState(erased())) + .expect_err("the strict wait must refuse an erase's own outcome"); + assert!(matches!(err, Error::ExecutionNotProved(_))); + + assert_eq!( + accept_affected_state(StateTransitionProofOutcome::AffectedState(erased())), + erased(), + "the affected-state wait must return the erase's observation unchanged" + ); + } } From e2770279f921a8a758a59b5ce6d5e97da263ed51 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 03:27:29 +0700 Subject: [PATCH 17/48] fix(dpp): make the lifecycle contract refusals paid consensus errors The two new cross-flag guards, erasure asked for by a type with no history or no deletion and a contested index on a type that keeps history, returned a bare data-contract error. Only the consensus variant becomes a paid rejection with a nonce bump when a signed contract create or update carries the combination; the bare one escapes the transformation as an internal execution error, so the submitter pays nothing, learns nothing, and can replay the same transition. Test: a signed create and a signed update whose wire schema is rewritten into each refused combination, which is all a client has to do since the parser will not build one. Both must be invalid-paid with the nonce bump persisted. Red before the fix, where the create is classified as an internal error and no nonce is written; green after. The parser tests now also assert the exact error variant rather than only the message. Co-Authored-By: Claude Opus 5 (1M context) --- .../try_from_schema/common/mod.rs | 12 +- .../try_from_schema/v3/keep_history_tests.rs | 37 +++ .../tests/document/lifecycle_contracts.rs | 303 ++++++++++++++++++ .../batch/tests/document/mod.rs | 1 + 4 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 106513f7747..5302abbc724 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -1991,8 +1991,14 @@ pub(super) fn apply_can_be_erased( return Ok(()); } + // A consensus error, not a bare data-contract error: only the consensus + // variant becomes a paid rejection with a nonce bump when a signed contract + // create or update carries the combination. The bare variant would escape as + // an internal execution error and cost the submitter nothing. let structure_error = |message: String| { - ProtocolError::DataContractError(DataContractError::InvalidContractStructure(message)) + consensus_or_protocol_data_contract_error(DataContractError::InvalidContractStructure( + message, + )) }; if !document_type.documents_keep_history { @@ -2035,7 +2041,9 @@ pub(super) fn reject_contested_keep_history( .iter() .find(|(_, index)| index.contested_index.is_some()) { - return Err(ProtocolError::DataContractError( + // Consensus error for the same reason as `apply_can_be_erased`: a + // signed contract carrying this combination must be a paid rejection. + return Err(consensus_or_protocol_data_contract_error( DataContractError::InvalidContractStructure(format!( "document type \"{}\" sets `documentsKeepHistory: true` and declares the \ contested index \"{}\": a contested resource is awarded outside transition \ diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs index 20d0a09a40d..a97cf7f28f2 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs @@ -5,6 +5,7 @@ use super::*; use crate::data_contract::document_type::accessors::{ DocumentTypeV0Getters, DocumentTypeV2Getters, }; +use crate::data_contract::errors::DataContractError; use platform_value::platform_value; /// Parses through the public dispatcher at the given protocol version so @@ -153,9 +154,44 @@ fn should_reject_erasure_without_history_or_without_deletion() { message.contains("canBeErased"), "the error must name the flag the author has to change; got {message}" ); + assert_contract_structure_consensus_error(&error); } } +/// A signed contract carrying a refused combination must be a paid rejection +/// with a nonce bump, and only the consensus variant becomes one: the bare +/// data-contract variant escapes the transformation as an internal execution +/// error, which costs the submitter nothing and reports nothing useful. +#[cfg(feature = "validation")] +fn assert_contract_structure_consensus_error(error: &ProtocolError) { + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + + let ProtocolError::ConsensusError(consensus) = error else { + panic!("expected a consensus error, got {error:?}"); + }; + assert!( + matches!( + consensus.as_ref(), + ConsensusError::BasicError(BasicError::ContractError( + DataContractError::InvalidContractStructure(_) + )) + ), + "expected an invalid-contract-structure basic error, got {consensus:?}" + ); +} + +#[cfg(not(feature = "validation"))] +fn assert_contract_structure_consensus_error(error: &ProtocolError) { + assert!( + matches!( + error, + ProtocolError::DataContractError(DataContractError::InvalidContractStructure(_)) + ), + "without the validation feature the structural variant is all there is, got {error:?}" + ); +} + /// Erasure defaults to off: a type that says nothing about it cannot have its /// revisions purged. #[test] @@ -210,6 +246,7 @@ fn should_reject_a_keep_history_type_that_carries_a_contested_index() { message.contains("contested"), "the error must say which index is the problem; got {message}" ); + assert_contract_structure_consensus_error(&error); } /// The same index without history is unaffected: the refusal is about the diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs new file mode 100644 index 00000000000..854bfe5cad6 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs @@ -0,0 +1,303 @@ +//! Signed contracts carrying a combination the lifecycle refuses. +//! +//! The parser cannot produce a `DataContract` holding one, so these build a +//! valid contract, rewrite the schema the transition carries on the wire, and +//! re-sign — which is all a hostile client has to do. What the tests pin is the +//! error category: only a consensus error becomes a paid rejection with a nonce +//! bump, while a bare data-contract error escapes as an internal execution +//! error that costs the submitter nothing. + +use super::*; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::TempPlatform; +use dpp::data_contract::accessors::v0::DataContractV0Setters; +use dpp::data_contract::DataContractFactory; +use dpp::identifier::Identifier; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::SecurityLevel; +use dpp::platform_value::platform_value; +use dpp::prelude::IdentityNonce; +use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; +use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; +use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::state_transition::StateTransition; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use drive::util::storage_flags::StorageFlags; + +/// A keep-history type the parser does admit, which every test below starts +/// from before rewriting the schema on the wire. +fn admissible_schema() -> Value { + platform_value!({ + "type": "object", + "documentsKeepHistory": true, + "documentsMutable": true, + "canBeDeleted": true, + "properties": { + "message": {"type": "string", "maxLength": 64, "position": 0}, + }, + "required": ["message"], + "additionalProperties": false, + }) +} + +/// Erasure asked for by a type with no history to erase. +fn erasure_without_history() -> Value { + platform_value!({ + "type": "object", + "documentsKeepHistory": false, + "documentsMutable": true, + "canBeDeleted": true, + "canBeErased": true, + "properties": { + "message": {"type": "string", "maxLength": 64, "position": 0}, + }, + "required": ["message"], + "additionalProperties": false, + }) +} + +/// A contested resource whose type also retains history. +fn contested_keep_history() -> Value { + platform_value!({ + "type": "object", + "documentsKeepHistory": true, + "documentsMutable": false, + "canBeDeleted": true, + "indices": [ + { + "name": "byMessage", + "properties": [{"message": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "message", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "properties": { + "message": {"type": "string", "maxLength": 64, "position": 0}, + }, + "required": ["message"], + "additionalProperties": false, + }) +} + +async fn refused_create( + schema: Value, +) -> (TempPlatform, Identifier, IdentityNonce) { + let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(14) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_state = platform.state.load(); + let (identity, signer, key) = setup_identity(&mut platform, 4001, dash_to_credits!(10.0)); + + let contract = DataContractFactory::new(14) + .expect("expected a contract factory") + .create_with_value_config( + identity.id(), + 1, + platform_value!({ "note": admissible_schema() }), + None, + None, + ) + .expect("the admissible type must parse") + .data_contract_owned(); + + let mut transition = DataContractCreateTransition::new_from_data_contract( + contract, + 1, + &identity.clone().into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("expected a contract create transition"); + + // Rewrite the schema the wire carries; the parser refuses to build one. + let StateTransition::DataContractCreate(DataContractCreateTransition::V0(create)) = + &mut transition + else { + panic!("expected a v0 contract create"); + }; + create + .data_contract + .document_schemas_mut() + .insert("note".to_string(), schema); + transition + .sign_external( + &key, + &signer, + None:: Result>, + ) + .await + .expect("expected to re-sign the rewritten contract"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().expect("serialized")], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected transition processing"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + + assert_eq!( + processing_result.invalid_paid_count(), + 1, + "a refused contract must cost its submitter, not vanish into an internal error" + ); + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { .. }] + ); + + let nonce = platform + .drive + .fetch_identity_nonce(identity.id().to_buffer(), true, None, platform_version) + .expect("expected to read the identity nonce") + .expect("the nonce must be persisted"); + + (platform, identity.id(), nonce) +} + +#[tokio::test] +async fn should_charge_for_a_signed_contract_asking_for_erasure_without_history() { + let (_platform, _identity, nonce) = refused_create(erasure_without_history()).await; + assert_eq!( + nonce, 1, + "the refusal must persist the nonce bump, so the same transition cannot be replayed" + ); +} + +#[tokio::test] +async fn should_charge_for_a_signed_contested_keep_history_contract() { + let (_platform, _identity, nonce) = refused_create(contested_keep_history()).await; + assert_eq!(nonce, 1, "the refusal must persist the nonce bump"); +} + +/// The same refusal on the update path, which has its own error-category split. +#[tokio::test] +async fn should_charge_for_a_signed_contract_update_asking_for_erasure_without_history() { + let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(14) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_state = platform.state.load(); + let (identity, signer, key) = setup_identity(&mut platform, 4002, dash_to_credits!(1.0)); + + let mut contract = DataContractFactory::new(14) + .expect("expected a contract factory") + .create_with_value_config( + identity.id(), + 1, + platform_value!({ "note": admissible_schema() }), + None, + None, + ) + .expect("the admissible type must parse") + .data_contract_owned(); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the contract"); + + let contract_id = contract.id(); + contract.set_version(2); + let mut transition = DataContractUpdateTransition::new_from_data_contract( + contract, + &identity.clone().into_partial_identity_info(), + key.id(), + 1, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("expected a contract update transition"); + + let StateTransition::DataContractUpdate(DataContractUpdateTransition::V0(update)) = + &mut transition + else { + panic!("expected a v0 contract update"); + }; + update + .data_contract + .document_schemas_mut() + .insert("note".to_string(), erasure_without_history()); + transition + .sign_external( + &key, + &signer, + None:: Result>, + ) + .await + .expect("expected to re-sign the rewritten update"); + + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().expect("serialized")], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected transition processing"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + + assert_eq!(processing_result.invalid_paid_count(), 1); + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { .. }] + ); + + let nonce = platform + .drive + .fetch_identity_contract_nonce( + identity.id().to_buffer(), + contract_id.to_buffer(), + true, + None, + platform_version, + ) + .expect("expected to read the identity contract nonce"); + assert_eq!( + nonce, + Some(1), + "the refusal must persist the contract nonce bump" + ); +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs index d77ea19e15d..2ffb2fb5513 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs @@ -4,6 +4,7 @@ mod dpns; mod erase; mod index_only; mod keep_history; +mod lifecycle_contracts; mod nft; mod ranked_group_drain; mod replacement; From b92e4f74eaaea89190b96f30d5b19da5c204d6c2 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 03:35:02 +0700 Subject: [PATCH 18/48] fix(drive): unflag the lifecycle container and dispatch its erase estimate The per-type lifecycle container was being flagged to whoever deleted first. It is shared by every document of the type and outlives all of them, since an erase removes records and per-document history trees and never this, so flagging it charged the first deleter for structure nobody refunds and made the committed encoding depend on which document went first. It is unflagged now; the record inside it keeps the deleter's flags, which is the byte an erase does refund. Its insert also now consults the running batch, so two deletes of different documents of the same type prepared before either is applied insert the shared key once rather than twice. The erase estimation slot was declared in every version table but never read: the implementation was called directly, so selecting an unsupported version silently kept using the old estimate. It has the normal dispatcher now, with the body in v0 and an unknown-version refusal. Tests: the raw container element is unflagged while the record names its deleter; only the first delete of a type pays for the container; and a table selecting an unimplemented estimation version is refused. Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/mod.rs | 26 +++- .../mod.rs | 57 +++++++ .../v0/mod.rs} | 3 +- .../src/drive/document/lifecycle/tests.rs | 144 ++++++++++++++++++ 4 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs rename packages/rs-drive/src/drive/document/estimation_costs/{add_estimation_costs_for_erase_document.rs => add_estimation_costs_for_erase_document/v0/mod.rs} (97%) diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 35e47d1b6a0..7db9034b833 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -248,10 +248,6 @@ impl Drive { let lifecycle_path = document_lifecycle_path(contract.id_ref().as_bytes(), document_type.name().as_str()); let element_flags = StorageFlags::map_to_some_element_flags(storage_flags); - let flags_len = storage_flags - .map(StorageFlags::serialized_size) - .unwrap_or_default(); - if let Some(layers) = estimated_costs_only_with_layer_info { Self::add_estimation_costs_for_lifecycle_record( contract, @@ -265,7 +261,9 @@ impl Drive { BatchInsertTreeApplyType::StatelessBatchInsertTree { in_tree_type: TreeType::NormalTree, tree_type: TreeType::NormalTree, - flags_len, + // The container is unflagged, so its element carries no + // beneficiary payload to size. + flags_len: 0, } } else { BatchInsertTreeApplyType::StatefulBatchInsertTree @@ -276,16 +274,28 @@ impl Drive { document_type.name().as_str(), ); let lifecycle_tree_key = vec![DOCUMENT_LIFECYCLE_TREE_KEY]; + // The container is shared by every document of the type and outlives + // any one of them: an erase removes records and per-document history + // trees, never this. Flagging it to whoever happened to delete first + // would charge them for structure nobody refunds, and would make the + // committed encoding depend on which document went first. The record + // inside it keeps the deleter's flags, which is what an erase refunds. + // + // The running batch is consulted so that two deletes of different + // documents of the same type, prepared before either is applied, insert + // this one key once rather than twice. + let mut container_operations = Vec::new(); self.batch_insert_empty_tree_if_not_exists::<0>( PathKey((std::mem::take(&mut document_type_path), lifecycle_tree_key)), TreeType::NormalTree, - storage_flags, + None, tree_apply_type, transaction, - &mut None, - batch_operations, + &mut Some(batch_operations), + &mut container_operations, &platform_version.drive, )?; + batch_operations.append(&mut container_operations); // The record's content is fully known whether or not this is a dry // run: it is a fixed-width encoding of the deletion time, and the diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs new file mode 100644 index 00000000000..6694ad99d0c --- /dev/null +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs @@ -0,0 +1,57 @@ +mod v0; + +use std::collections::HashMap; + +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerInformation; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; + +impl Drive { + /// Registers the layers an erase chunk touches. + /// + /// # Parameters + /// * `document_id`: The document whose revisions are being removed. + /// * `contract`: The contract that contains the document. + /// * `document_type`: The type of the document, which must keep history. + /// * `layers`: The estimated layer information the dry run builds. + /// * `platform_version`: The platform 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(crate) fn add_estimation_costs_for_erase_document( + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + layers: &mut HashMap, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .document + .delete + .add_estimation_costs_for_erase_document + { + 0 => Self::add_estimation_costs_for_erase_document_v0( + document_id, + contract, + document_type, + layers, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "add_estimation_costs_for_erase_document".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/v0/mod.rs similarity index 97% rename from packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs rename to packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/v0/mod.rs index 23d195e3e2f..9f75ab787d1 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/v0/mod.rs @@ -28,7 +28,8 @@ impl Drive { /// revisions than a chunk removes, so its height is bounded rather than /// derived from the chunk size; an estimate keyed to the chunk would /// understate the merk path of a long history. - pub(crate) fn add_estimation_costs_for_erase_document( + #[inline(always)] + pub(super) fn add_estimation_costs_for_erase_document_v0( document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 964f9b0355b..932b97bb5b1 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -909,3 +909,147 @@ fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_delete "a delete removes every index reference that led to the document" ); } + +/// The per-type container is shared by every document of the type and outlives +/// any one of them, so it belongs to nobody: an erase removes records and +/// per-document history trees, never this. The record inside it is the byte an +/// erase does refund, and that one names the deleter. +#[test] +fn should_leave_the_lifecycle_container_unflagged_while_the_record_names_the_deleter() { + use crate::drive::document::paths::contract_document_type_path_vec; + + let owner = [25u8; 32]; + let deleter = Identifier::new([26u8; 32]); + let (drive, contract, id) = setup_history(2, owner); + delete(&drive, &contract, id, deleter, 5_000); + + let mut type_path = contract_document_type_path_vec(contract.id_ref().as_bytes(), "person"); + let container = drive + .grove + .get_raw( + type_path.as_slice().into(), + &[crate::drive::document::paths::DOCUMENT_LIFECYCLE_TREE_KEY], + None, + &latest().drive.grove_version, + ) + .value + .expect("the first delete creates the container"); + assert_eq!( + container.get_flags(), + &None, + "the shared container must belong to nobody" + ); + + type_path.push(vec![ + crate::drive::document::paths::DOCUMENT_LIFECYCLE_TREE_KEY, + ]); + let record = drive + .grove + .get_raw( + type_path.as_slice().into(), + id.as_slice(), + None, + &latest().drive.grove_version, + ) + .value + .expect("the record exists"); + let flags = StorageFlags::map_cow_some_element_flags_ref(record.get_flags()) + .expect("the record's flags must decode") + .expect("the record names its deleter"); + assert_eq!( + flags.owner_id(), + Some(&deleter.to_buffer()), + "the record is the byte an erase refunds, and it belongs to the deleter" + ); +} + +/// Only the first delete of a type pays for the container. A second one finds +/// it already there and pays for its record alone, so the storage difference +/// between the two is exactly the container. +#[test] +fn should_charge_the_container_to_the_first_delete_of_a_type_only() { + let owner = [27u8; 32]; + let version = latest(); + let (drive, contract, first_id) = setup_history(1, owner); + let document_type = document_type_of(&contract); + + // A second document of the same type, so the second delete finds the + // container already in place. + let mut second = + json_document_to_document(PERSON, Some(owner.into()), document_type, version).unwrap(); + second.set_id(Identifier::new([28u8; 32])); + second.set_revision(Some(1)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &second, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default_with_time(2_000), + true, + None, + version, + None, + ) + .expect("expected a second document"); + + let deleter = Identifier::new([29u8; 32]); + let first = delete(&drive, &contract, first_id, deleter, 5_000); + let later = delete(&drive, &contract, second.id(), deleter, 6_000); + + assert!( + first.storage_fee > later.storage_fee, + "the first delete pays for the container as well as its record: {} vs {}", + first.storage_fee, + later.storage_fee + ); +} + +/// The declared feature slot has to actually route: a table that selects a +/// version this code does not implement must fail loudly rather than silently +/// keep using the old estimate. +#[test] +fn should_reject_an_unsupported_erase_estimation_version() { + use std::collections::HashMap; + + let owner = [30u8; 32]; + let (_drive, contract, id) = setup_history(1, owner); + let mut version = latest().clone(); + assert_eq!( + version + .drive + .methods + .document + .delete + .add_estimation_costs_for_erase_document, + 0, + "protocol 14 selects the only implementation there is" + ); + version + .drive + .methods + .document + .delete + .add_estimation_costs_for_erase_document = 1; + + let error = Drive::add_estimation_costs_for_erase_document( + id, + &contract, + document_type_of(&contract), + &mut HashMap::new(), + &version, + ) + .expect_err("an unimplemented version must be refused"); + assert!(matches!( + error, + Error::Drive(DriveError::UnknownVersionMismatch { .. }) + )); +} From a9f886d5bf43f7ab29f2e4f9e274f03f498ad830 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 03:45:43 +0700 Subject: [PATCH 19/48] feat(drive-abci): charge an erase for the refunds it hands out Removing revisions credits whoever paid for each of them, and those balance updates are applied after the transition's fee result is formed, against identities that had nothing to do with it. The chunk bound limits how many there can be but pays for none of them, so a small erase could demand a hundred balance reads and writes that appeared in neither its admission accounting nor its charged work. A Drive helper now prices that work: it measures one balance update through the ordinary estimation path, which mutates nothing and touches no real identity, and scales it by the beneficiaries one erase can credit -- a full chunk of revisions plus the lifecycle record, the per-document history subtree and the current pointer, all of which a terminal chunk removes. Erase validation bills it through the execution context, so it reaches both the estimate that admits the transition and the fee actually charged. Test: an erase's charged fee and its admission estimate must both cover the priced recipient work. Red before this change, where the whole erase costs 1,149,580 against 450,944,300 of unpriced recipient work; green after. A Drive test pins that the price scales with the bound and creates no balance of its own. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_v0/mod.rs | 19 ++++- .../batch/tests/document/erase.rs | 44 ++++++++++ .../src/drive/document/lifecycle/mod.rs | 3 + .../document/lifecycle/refund_recipients.rs | 84 +++++++++++++++++++ .../src/drive/document/lifecycle/tests.rs | 55 ++++++++++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs index ad2412264c4..a719aec67a8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs @@ -1,5 +1,8 @@ use crate::error::Error; -use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_keep_history_document_lifecycle; use crate::platform_types::platform::PlatformStateRef; @@ -87,6 +90,20 @@ impl DocumentEraseTransitionActionStateValidationV0 for DocumentEraseTransitionA platform_version, )?; + // Removing revisions credits whoever paid for each of them, and those + // balance updates are applied after this transition's fee result is + // formed, against identities that had nothing to do with it. The chunk + // bound limits how many there can be; this is what pays for them. It is + // billed on every path the erase can take, including the refusals, + // because the estimate that admits the transition is formed before the + // lifecycle is known. + execution_context.add_operation(ValidationOperation::PrecalculatedOperation( + platform + .drive + .erase_refund_recipient_cost(&block_info.epoch, platform_version) + .map_err(Error::Drive)?, + )); + match lifecycle { DocumentLifecycleState::Active(_) => { Ok(SimpleConsensusValidationResult::new_with_error( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 8d936375d6d..a4af21b24a4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -833,3 +833,47 @@ async fn should_delete_and_erase_in_one_block() { ); assert_matches!(fixture.lifecycle("note"), DocumentLifecycleState::Absent); } + +/// The balance updates an erase's refunds cause land outside its own fee +/// result, against identities that had nothing to do with it. The chunk bound +/// limits how many there can be; this pins that the submitter pays for them, +/// in the estimate that admits the transition and in the fee actually charged. +#[tokio::test] +async fn should_charge_an_erase_for_the_refund_recipients_it_can_credit() { + use dpp::block::epoch::Epoch; + + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let platform_version = PlatformVersion::get(14).unwrap(); + let recipient_cost = fixture + .platform + .drive + .erase_refund_recipient_cost(&Epoch::new(0).unwrap(), platform_version) + .expect("expected the recipient work to be priced"); + assert!(recipient_cost.processing_fee > 0); + + let result = fixture.erase("note", true, None).await; + let StateTransitionExecutionResult::SuccessfulExecution { + estimated_fees, + fee_result, + .. + } = result + else { + panic!("expected the erase to succeed, got {result:?}"); + }; + let estimated_fees = estimated_fees.expect("an erase is admitted against an estimate"); + + assert!( + fee_result.processing_fee >= recipient_cost.processing_fee, + "the charged fee must include the recipient work: {} is below {}", + fee_result.processing_fee, + recipient_cost.processing_fee + ); + assert!( + estimated_fees.processing_fee >= recipient_cost.processing_fee, + "so must the estimate that admits it: {} is below {}", + estimated_fees.processing_fee, + recipient_cost.processing_fee + ); +} diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index 3c26b8fef6e..e864de37ffd 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -15,6 +15,9 @@ #[cfg(feature = "server")] mod fetch; +#[cfg(feature = "server")] +mod refund_recipients; + #[cfg(all(test, feature = "server", feature = "verify"))] mod tests; diff --git a/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs b/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs new file mode 100644 index 00000000000..f2f2f117b4f --- /dev/null +++ b/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs @@ -0,0 +1,84 @@ +//! Pricing for the balance work an erase's refunds cause outside its own fee. +//! +//! Refund credits are applied after the fee result is formed, as separate +//! balance operations against identities that had nothing to do with the +//! transition. A bound on how many revisions one erase removes limits that work +//! but does not pay for it, so it is priced here and billed to the submitter +//! through the execution context, which feeds both the admission estimate and +//! the fee actually charged. + +use std::collections::HashMap; + +use dpp::block::epoch::Epoch; +use dpp::fee::fee_result::FeeResult; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerInformation; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +/// Beneficiaries an erase can credit beyond the revisions themselves: the +/// lifecycle record's writer, the per-document history subtree's creator, and +/// the current pointer's, all of which a terminal chunk removes. +const STRUCTURAL_REFUND_RECIPIENTS: u64 = 3; + +impl Drive { + /// Prices the worst-case number of third-party balance updates one erase + /// can cause. + /// + /// Every recipient does the same shape of work — read the balance, read the + /// debt, write one or both — so the cost of one is measured through the + /// ordinary estimation path and multiplied by how many there can be. The + /// measurement performs no balance mutation and touches no real identity: + /// it runs the balance update in estimation mode, where the reads are + /// average-case costs rather than state. + pub fn erase_refund_recipient_cost( + &self, + epoch: &Epoch, + platform_version: &PlatformVersion, + ) -> Result { + let chunk = platform_version + .system_limits + .max_document_revisions_erased_per_transition + .ok_or(Error::Drive(DriveError::NotSupported( + "erasing retained revisions is not available at this protocol version", + )))?; + let recipients = chunk as u64 + STRUCTURAL_REFUND_RECIPIENTS; + + let mut layers: Option> = + Some(HashMap::new()); + let batch = self.add_to_identity_balance_operations( + [0u8; 32], + 1, + &mut layers, + None, + platform_version, + )?; + let mut operations: Vec = vec![]; + self.apply_batch_low_level_drive_operations( + layers, + None, + batch, + &mut operations, + &platform_version.drive, + )?; + let one = Drive::calculate_fee( + None, + Some(operations), + epoch, + self.config.epochs_per_era, + platform_version, + None, + )?; + + Ok(FeeResult { + storage_fee: one.storage_fee.saturating_mul(recipients), + processing_fee: one.processing_fee.saturating_mul(recipients), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + }) + } +} diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 932b97bb5b1..3cf44111f80 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -1053,3 +1053,58 @@ fn should_reject_an_unsupported_erase_estimation_version() { Error::Drive(DriveError::UnknownVersionMismatch { .. }) )); } + +/// The recipient work is priced from the ordinary balance-update path and +/// scales with the number of beneficiaries one erase can credit, without +/// touching any identity's balance. +#[test] +fn should_price_the_refund_recipients_an_erase_can_credit() { + use dpp::block::epoch::Epoch; + + let drive = setup_drive_with_initial_state_structure(None); + let epoch = Epoch::new(0).unwrap(); + let version = latest(); + + let priced = drive + .erase_refund_recipient_cost(&epoch, version) + .expect("expected a price for the recipient work"); + assert!( + priced.processing_fee > 0, + "crediting a beneficiary is work somebody has to pay for" + ); + assert!( + priced.fee_refunds.0.is_empty() && priced.storage_fee == 0, + "pricing must not invent a refund or a storage charge of its own" + ); + + // The same shape at half the bound costs proportionally less, which is what + // makes this a price for the bound rather than a constant. + let mut halved = version.clone(); + halved + .system_limits + .max_document_revisions_erased_per_transition = Some(chunk_size() as u16 / 2); + let smaller = drive + .erase_refund_recipient_cost(&epoch, &halved) + .expect("expected a price at the smaller bound"); + assert!( + smaller.processing_fee < priced.processing_fee, + "a smaller chunk credits fewer beneficiaries: {} is not below {}", + smaller.processing_fee, + priced.processing_fee + ); + + // Every balance in the drive is untouched: this is a measurement, not a + // mutation. + assert!( + drive + .fetch_identity_balance(default_owner_id(), None, version) + .expect("expected the balance read to run") + .is_none(), + "pricing must not create the identity it measures against" + ); +} + +/// The synthetic identity the price is measured against. +fn default_owner_id() -> [u8; 32] { + [0u8; 32] +} From 6576027ad05c568ddae952b049604f1b738f9084 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 04:02:33 +0700 Subject: [PATCH 20/48] test: close the lifecycle's accounting and proof evidence gaps The erase estimate now prices what the enumeration actually does. GroveDB exposes no key-only result shape over a range, so the revision bodies come back whether or not the caller wants them; the estimate charges for one revision more than the chunk removes, at the full body size, and for the record read a non-terminal chunk performs. The comment says so rather than claiming a key-only read. New evidence: refunds are keyed under the epoch each byte was written in, with two writers six epochs apart compared like for like; a refund is a credit a beneficiary's balance actually gains; a one-revision erase is admitted only against the full chunk estimate; an erase commits the same state and costs the same with GroveDB's batch consistency checking on and off; the erasing state survives the proof round trip for populated, empty and by-revision pages, as does the terminal absence, and a claim that differs in any of the four times or in the count is not what the verifier derives; revisions still number from one after a partial erasure; and a removed revision is not answered with its neighbour. At the transition level: refused erases persist their nonce bumps on both refusal paths, a create of a deleted id is refused when it shares the delete's block, and a replace of a deleted document is not found. Two deletes of one type combined into a single batch before either is applied both emit the shared container's insert, because each document operation is converted without sight of its siblings. GroveDB refuses the batch rather than committing one of them, and that fail-closed behaviour is now pinned along with the one-transition cap that keeps consensus away from the shape. Also: the indexOnly parser documentation is back on the indexOnly parser, the erase converter names where the token offer is really refused, an error message loses a run of literal spaces, and the unreachable history-metadata arm is gone. Co-Authored-By: Claude Opus 5 (1M context) --- .../try_from_schema/common/mod.rs | 14 +- .../batch/tests/document/erase.rs | 176 ++++++ .../v0/mod.rs | 42 +- .../add_document_to_primary_storage/v1/mod.rs | 8 +- .../src/drive/document/lifecycle/tests.rs | 531 ++++++++++++++++++ .../src/query/document_history_drive_query.rs | 6 +- .../document/document_erase_transition.rs | 5 +- 7 files changed, 759 insertions(+), 23 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 5302abbc724..821d753cebc 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -1942,13 +1942,6 @@ pub(super) fn apply_doctype_aggregates( Ok(()) } -/// Read the doctype-level `indexOnly` keyword off the raw schema. -/// -/// Runs before the core parse because the core takes `schema` by value — -/// same shape as [`parse_doctype_aggregate_keywords`]. Only the generation-3 -/// driver calls this; earlier generations ignore the keyword exactly as they -/// ignore every doctype-level keyword they predate (their meta-schemas still -/// reject it under `full_validation`). /// Read the doctype-level `canBeErased` flag out of the raw schema. /// /// Runs before the core parse for the same reason as @@ -2055,6 +2048,13 @@ pub(super) fn reject_contested_keep_history( Ok(()) } +/// Read the doctype-level `indexOnly` keyword off the raw schema. +/// +/// Runs before the core parse because the core takes `schema` by value — +/// same shape as [`parse_doctype_aggregate_keywords`]. Only the generation-3 +/// driver calls this; earlier generations ignore the keyword exactly as they +/// ignore every doctype-level keyword they predate (their meta-schemas still +/// reject it under `full_validation`). pub(super) fn parse_index_only_keyword(schema: &Value) -> Result { let schema_map_opt = schema.to_map().ok(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index a4af21b24a4..f300d67ea5a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -877,3 +877,179 @@ async fn should_charge_an_erase_for_the_refund_recipients_it_can_credit() { recipient_cost.processing_fee ); } + +/// A paid failure has to leave its nonce bump behind, or the same rejected +/// transition can be replayed for free. The refusals the lifecycle adds are no +/// exception. +#[tokio::test] +async fn should_persist_the_nonce_bump_on_a_refused_erase() { + let mut fixture = Fixture::new("note").await; + + let nonce_after = |fixture: &Fixture| { + fixture + .platform + .drive + .fetch_identity_contract_nonce( + fixture.owner.id().to_buffer(), + fixture.contract.id().to_buffer(), + true, + None, + PlatformVersion::get(14).unwrap(), + ) + .expect("expected to read the identity contract nonce") + .expect("the create already wrote one") + }; + let before = nonce_after(&fixture); + + // An erase of a document that has not been deleted. + assert_matches!( + fixture.erase("note", true, None).await, + StateTransitionExecutionResult::PaidConsensusError { .. } + ); + let after_refusal = nonce_after(&fixture); + assert_eq!( + after_refusal, + before + 1, + "a refused erase must still consume its nonce" + ); + + // And an erase whose token payment is refused in the transformer, which is + // a different refusal path with its own nonce-bump action. + let token_payment_info = TokenPaymentInfo::V0(TokenPaymentInfoV0 { + payment_token_contract_id: Some(Identifier::new([5u8; 32])), + token_contract_position: 0, + minimum_token_cost: None, + maximum_token_cost: Some(10), + gas_fees_paid_by: Default::default(), + }); + assert_matches!( + fixture.erase("note", true, Some(token_payment_info)).await, + StateTransitionExecutionResult::PaidConsensusError { .. } + ); + assert_eq!( + nonce_after(&fixture), + after_refusal + 1, + "the transformer's refusal must consume its nonce too" + ); +} + +/// A delete and a create of the same id in one block: the create sees the +/// delete's reservation through the block transaction and is refused, so an id +/// cannot be recycled while its revisions are retained. +#[tokio::test] +async fn should_reject_a_create_that_follows_a_delete_of_the_same_id_in_one_block() { + let mut fixture = Fixture::new("note").await; + + let platform_version = PlatformVersion::get(14).unwrap(); + let mut document = fixture.document.clone(); + document.set_revision(Some(1)); + let delete = BatchTransition::new_document_deletion_transition_from_document( + document, + fixture.document_type("note"), + &fixture.owner_key, + fixture.nonce, + 0, + None, + &fixture.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a delete transition") + .serialize_to_bytes() + .expect("serialized"); + fixture.nonce += 1; + + let create = BatchTransition::new_document_creation_transition_from_document( + fixture.document.clone(), + fixture.document_type("note"), + fixture.entropy.0, + &fixture.owner_key, + fixture.nonce, + 0, + None, + &fixture.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a create transition") + .serialize_to_bytes() + .expect("serialized"); + fixture.nonce += 1; + + let state = fixture.platform.state.load(); + let version = state.current_platform_version().unwrap(); + let transaction = fixture.platform.drive.grove.start_transaction(); + let result = fixture + .platform + .platform + .process_raw_state_transitions( + &[delete, create], + &state, + &BlockInfo::default(), + &transaction, + version, + false, + None, + ) + .expect("expected transition processing"); + fixture + .platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .unwrap(); + + assert_eq!(result.valid_count(), 1, "only the delete may execute"); + assert_eq!(result.invalid_paid_count(), 1, "the create must be refused"); + assert_matches!( + fixture.lifecycle("note"), + DocumentLifecycleState::Deleted(_) + ); +} + +/// Replacing a deleted document is refused for the same reason a second delete +/// is: nothing an ordinary read can see is there any more. This is the existing +/// not-found path, pinned here because the lifecycle is what makes the document +/// invisible while its revisions survive. +#[tokio::test] +async fn should_reject_a_replace_of_a_deleted_document() { + let mut fixture = Fixture::new("note").await; + assert_successful(&fixture.delete_as_owner("note").await, "the delete"); + + let platform_version = PlatformVersion::get(14).unwrap(); + let mut replacement = fixture.document.clone(); + replacement.set_revision(Some(2)); + let transition = BatchTransition::new_document_replacement_transition_from_document( + replacement, + fixture.document_type("note"), + &fixture.owner_key, + fixture.nonce, + 0, + None, + &fixture.owner_signer, + platform_version, + None, + ) + .await + .expect("expected a replace transition"); + fixture.nonce += 1; + + let result = process( + &mut fixture.platform, + transition.serialize_to_bytes().expect("serialized"), + ); + assert_matches!( + result, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::DocumentNotFoundError(_)), + .. + } + ); + assert_matches!( + fixture.lifecycle("note"), + DocumentLifecycleState::Deleted(_) + ); +} diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs index 68f55b33a10..1457c520835 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -217,6 +217,13 @@ impl Drive { /// Reads the revision keys of one document newest first, one more than a /// chunk may remove so the caller can tell a terminal chunk from a partial /// one before it emits anything. + /// + /// The keys are what this needs, but GroveDB's query surface has no + /// key-only result shape over a range: every result type it exposes carries + /// elements. The revision bodies therefore come back and are dropped, and + /// the estimate below prices that read for what it is. What this does avoid + /// is the running-batch snapshot the general delete-by-query helper rebuilds + /// for every element, which is quadratic in the chunk size. fn enumerate_newest_revision_keys( &self, history_path: &[Vec], @@ -302,6 +309,11 @@ impl Drive { /// endings: the record write of a first chunk and the subtree removal of a /// terminal one. Every erase is therefore admitted against the same /// worst-case estimate whatever the document's actual history length. + /// + /// The enumeration is priced at one revision more than a chunk removes, + /// because that is what it reads, and at the full body size, because the + /// query surface returns bodies whether or not the caller wants them. The + /// record read a non-terminal chunk performs is priced too. #[allow(clippy::too_many_arguments)] fn estimated_erase_document_operations( &self, @@ -315,11 +327,9 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { - let mut single_operation = Vec::with_capacity(2); - for sequence in 1..=chunk as u64 { - // Distinct synthetic keys of the real shape: the dry run's batch is - // checked for consistency like any other, so a repeated key would be - // rejected rather than priced. + // The enumeration reads one revision more than the chunk removes, and + // reads each of them whole. + for sequence in 1..=chunk as u64 + 1 { let mut key = encode_u64(block_info.time_ms); key.extend(encode_u64(sequence)); self.grove_get_raw( @@ -333,6 +343,28 @@ impl Drive { &mut batch_operations, &platform_version.drive, )?; + } + + // A non-terminal chunk reads the record before overwriting it. + self.grove_get_raw( + lifecycle_path.into(), + document_id.as_slice(), + DirectQueryType::StatelessDirectQuery { + in_tree_type: TreeType::NormalTree, + query_target: QueryTarget::QueryTargetValue(DOCUMENT_LIFECYCLE_RECORD_SIZE), + }, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + + let mut single_operation = Vec::with_capacity(2); + for sequence in 1..=chunk as u64 { + // Distinct synthetic keys of the real shape: the dry run's batch is + // checked for consistency like any other, so a repeated key would be + // rejected rather than priced. + let mut key = encode_u64(block_info.time_ms); + key.extend(encode_u64(sequence)); single_operation.clear(); self.batch_delete( history_path.into(), diff --git a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs index be96f8f511a..181e822c5ee 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs @@ -118,11 +118,9 @@ impl Drive { // A dry run skips the probe, which reports the tree as absent, and // pays for it as a fixed cost so estimation and execution agree. if !created && !insert_without_check { - return Err(Error::Drive( - DriveError::CorruptedDocumentAlreadyExists( - "a document of this id still retains revisions and can not be created until they are erased", - ), - )); + return Err(Error::Drive(DriveError::CorruptedDocumentAlreadyExists( + "a document of this id still retains revisions and can not be created until they are erased", + ))); } } else { operations.push( diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 3cf44111f80..abe90ce3313 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -1108,3 +1108,534 @@ fn should_price_the_refund_recipients_an_erase_can_credit() { fn default_owner_id() -> [u8; 32] { [0u8; 32] } + +/// Revisions written in different epochs are refunded under the epoch each was +/// written in, and the payout tail decides the amount: a byte written recently +/// has more of its one-time charge still unspent than one written long ago, so +/// it refunds more. +#[test] +fn should_refund_each_writer_under_the_epoch_they_wrote_in() { + use dpp::block::epoch::Epoch; + + let early_writer = [40u8; 32]; + let late_writer = [41u8; 32]; + let version = latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract(FAMILY_HISTORY_CONTRACT, false, version).unwrap(); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .expect("expected to apply the contract"); + let document_type = document_type_of(&contract); + let mut document = + json_document_to_document(PERSON, Some(early_writer.into()), document_type, version) + .unwrap(); + + let write = |revision: u64, writer: [u8; 32], epoch_index: u16, first: bool| { + let mut document = document.clone(); + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &document, + Some(Cow::Owned(StorageFlags::new_single_epoch( + epoch_index, + Some(writer), + ))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + !first, + BlockInfo { + epoch: Epoch::new(epoch_index).unwrap(), + ..BlockInfo::default_with_time(1_000 + revision) + }, + true, + None, + version, + None, + ) + .expect("expected to write a revision"); + }; + // The first revision also creates the document's history subtree and its + // index references, so it is not the same amount of storage as an appended + // one. A third identity pays for it, leaving the two compared below as one + // appended revision each and differing only in the epoch they landed in. + write(1, [39u8; 32], 0, true); + write(2, early_writer, 0, false); + write(3, late_writer, 6, false); + document.set_revision(Some(3)); + let id = document.id(); + + let block_info = BlockInfo { + epoch: Epoch::new(6).unwrap(), + ..BlockInfo::default_with_time(5_000) + }; + let batch = drive + .delete_document_for_contract_operations( + id, + &contract, + document_type, + &block_info, + Some(Identifier::new(early_writer)), + None, + &mut None, + None, + version, + ) + .expect("expected to delete"); + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut vec![], &version.drive) + .expect("expected to apply the delete"); + + let fee = erase_at(&drive, &contract, id, &block_info); + + let early = fee + .fee_refunds + .0 + .get(&early_writer) + .expect("the first writer must be credited"); + let late = fee + .fee_refunds + .0 + .get(&late_writer) + .expect("the second writer must be credited"); + // Every byte is refunded under the epoch it was written in, so the first + // writer appears twice: once for the revision it wrote in epoch 0, and once + // for the lifecycle record it wrote as the deleter in epoch 6. + assert_eq!( + early.keys().copied().collect::>(), + vec![0, 6], + "the first writer's revision and the record it wrote as deleter" + ); + assert_eq!( + late.keys().copied().collect::>(), + vec![6], + "the second writer wrote only its revision, in epoch 6" + ); + + let early_revision = early[&0]; + let late_revision = late[&6]; + assert!( + late_revision > early_revision, + "the same revision written six epochs later has more of its one-time \ + charge still unspent, so it refunds more: {late_revision} is not above {early_revision}" + ); +} + +/// Erases at a caller-chosen block, so a test can put the write and the erase +/// in different epochs. +fn erase_at( + drive: &Drive, + contract: &DataContract, + id: Identifier, + block_info: &BlockInfo, +) -> dpp::fee::fee_result::FeeResult { + let mut operations = vec![]; + let batch = drive + .erase_document_for_contract_operations( + id, + contract, + document_type_of(contract), + block_info, + &mut None, + None, + latest(), + ) + .expect("expected to build the erase operations"); + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut operations, &latest().drive) + .expect("expected to apply the erase"); + Drive::calculate_fee( + None, + Some(operations), + &block_info.epoch, + drive.config.epochs_per_era, + latest(), + None, + ) + .expect("expected a fee") +} + +/// A refund is a credit to a real identity, not just a map entry: the amount +/// the fee result names is the amount the beneficiary's balance gains. +#[test] +fn should_credit_a_refund_to_the_beneficiarys_balance() { + let owner = [42u8; 32]; + let version = latest(); + let (drive, contract, id) = setup_history(3, owner); + drive + .add_new_identity( + dpp::identity::Identity::create_basic_identity(Identifier::new(owner), version) + .expect("expected a basic identity"), + false, + &BlockInfo::default(), + true, + None, + version, + ) + .expect("expected to create the beneficiary"); + let before = drive + .fetch_identity_balance(owner, None, version) + .expect("expected to read the balance") + .expect("the beneficiary exists"); + + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + let fee = erase(&drive, &contract, id, 6_000); + let refunded: u64 = fee + .fee_refunds + .0 + .get(&owner) + .map(|per_epoch| per_epoch.values().sum()) + .expect("the writer must be credited"); + + drive + .add_to_identity_balance(owner, refunded, &BlockInfo::default(), true, None, version) + .expect("expected to credit the refund"); + let after = drive + .fetch_identity_balance(owner, None, version) + .expect("expected to read the balance") + .expect("the beneficiary exists"); + assert_eq!( + after - before, + refunded, + "the credited amount is exactly what the fee result named" + ); +} + +/// The estimate an erase is admitted against is sized for a full chunk, so a +/// one-revision erase needs the same balance as a hundred-revision one. That is +/// the point of the bound: it is what an erase can cost, not what this one will. +#[test] +fn should_admit_a_one_revision_erase_only_against_the_full_chunk_estimate() { + let owner = [43u8; 32]; + let (drive, contract, id) = setup_history(1, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let estimated = estimated_erase_fee(&drive, &contract, id); + let actual = erase(&drive, &contract, id, 6_000); + assert!( + estimated.processing_fee > actual.processing_fee * 4, + "the estimate must be sized for the bound, not for the one revision this document holds: {} against {}", + estimated.processing_fee, + actual.processing_fee + ); +} + +/// GroveDB's batch consistency checking is off by default and on in some +/// deployments. A lifecycle batch has to produce the same state and the same +/// fees either way, or the two disagree about consensus. +#[test] +fn should_erase_identically_with_batch_consistency_checking_on() { + let chunk = chunk_size(); + let mut roots = vec![]; + let mut fees = vec![]; + for verify in [false, true] { + let owner = [44u8; 32]; + let version = latest(); + let directory = tempfile::TempDir::new().unwrap(); + let (drive, _) = Drive::open( + directory.path(), + Some(crate::config::DriveConfig { + batching_consistency_verification: verify, + ..Default::default() + }), + ) + .unwrap(); + drive.create_initial_state_structure(None, version).unwrap(); + let contract = json_document_to_contract(FAMILY_HISTORY_CONTRACT, false, version).unwrap(); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .expect("expected to apply the contract"); + let document_type = document_type_of(&contract); + let mut document = + json_document_to_document(PERSON, Some(owner.into()), document_type, version).unwrap(); + let flags = Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))); + for revision in 1..=chunk { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &document, + flags.clone(), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .expect("expected to write a revision"); + } + let id = document.id(); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + fees.push(erase(&drive, &contract, id, 6_000).processing_fee); + roots.push( + drive + .grove + .root_hash(None, &version.drive.grove_version) + .value + .unwrap(), + ); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Absent + )); + } + assert_eq!( + roots[0], roots[1], + "consistency checking must not change the state a lifecycle batch commits" + ); + assert_eq!(fees[0], fees[1], "nor what it costs"); +} + +/// Two deletes of different documents of one type, combined into one batch +/// before either is applied and before the type has a lifecycle container, each +/// emit the insert of the same shared key. Every document operation is +/// converted on its own and cannot see its siblings, so neither can tell that +/// the other is already creating it. +/// +/// GroveDB refuses the batch rather than committing one of the two, so nothing +/// is written and the caller is told. That fail-closed behaviour is what this +/// pins. Consensus cannot reach it: a batch state transition carries exactly +/// one document transition at every protocol version, so two document +/// operations never share a batch by that route, and no other caller combines +/// keep-history deletes. +#[test] +fn should_refuse_rather_than_half_apply_two_deletes_sharing_a_new_container() { + assert_eq!( + latest().system_limits.max_transitions_in_documents_batch, + 1, + "the cap is what keeps consensus away from this shape" + ); + use crate::util::batch::{DocumentOperationType, DriveOperation}; + use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; + + let owner = [45u8; 32]; + let version = latest(); + let (drive, contract, first_id) = setup_history(1, owner); + let document_type = document_type_of(&contract); + let mut second = + json_document_to_document(PERSON, Some(owner.into()), document_type, version).unwrap(); + second.set_id(Identifier::new([46u8; 32])); + second.set_revision(Some(1)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &second, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default_with_time(2_000), + true, + None, + version, + None, + ) + .expect("expected a second document"); + + let delete_operation = |id: Identifier| { + DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { + document_id: id, + deleter_id: Some(Identifier::new(owner)), + contract_info: DataContractInfo::BorrowedDataContract(&contract), + document_type_info: DocumentTypeInfo::DocumentTypeName("person".to_string()), + }) + }; + drive + .apply_drive_operations( + vec![delete_operation(first_id), delete_operation(second.id())], + true, + &BlockInfo::default_with_time(5_000), + None, + version, + None, + ) + .expect_err("the duplicated container insert must be refused, not half applied"); + + for id in [first_id, second.id()] { + assert!( + matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Active(_) + ), + "a refused batch must leave both documents exactly as they were" + ); + } + + // One at a time is the supported shape, and the second finds the container + // the first created. + for id in [first_id, second.id()] { + drive + .apply_drive_operations( + vec![delete_operation(id)], + true, + &BlockInfo::default_with_time(6_000), + None, + version, + None, + ) + .expect("expected one delete at a time to succeed"); + } + for id in [first_id, second.id()] { + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Deleted(_) + )); + } +} + +/// Every state the lifecycle can be in has to survive the proof round trip, and +/// each of the four claimed times has to be the one the proof authenticates: a +/// node that reports a different deletion time, a different erasure start, or a +/// different remaining count must fail verification rather than be believed. +#[test] +fn should_prove_the_erasing_state_and_reject_a_tampered_claim() { + let chunk = chunk_size(); + let owner = [47u8; 32]; + let (drive, contract, id) = setup_history(chunk + 2, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + erase(&drive, &contract, id, 6_000); + + let query = |selector| DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + // A single-revision read is capped at one entry by the selector's own + // rule; the page selectors take the full page. + limit: Some(match selector { + DocumentHistorySelector::Revision(_) => 1, + _ => 10, + }), + selector, + }; + + // A populated page, and a page whose lower bound is past the end of what + // survives. Both must authenticate the same erasing metadata. + for selector in [ + DocumentHistorySelector::StartAtTime(0), + DocumentHistorySelector::StartAtTime(u64::MAX / 2), + DocumentHistorySelector::Revision(1), + ] { + let query = query(selector); + let (page, proof) = drive + .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + .expect("expected to prove the page"); + let (_, verified) = Drive::verify_document_history_v1( + &query, + &proof, + document_type_of(&contract), + latest(), + ) + .expect("expected the proof to verify"); + assert_eq!(verified, page); + assert_eq!(verified.lifecycle.state, DocumentHistoryState::Erasing); + assert_eq!(verified.lifecycle.remaining_revisions, 2); + assert_eq!(verified.lifecycle.times.deleted_at_ms, 5_000); + assert_eq!(verified.lifecycle.times.erasing_started_at_ms, 6_000); + assert_eq!(verified.lifecycle.times.erasing_from_revision, chunk + 2); + + // A claim that differs from the proof in any one field is not the + // proof's claim, and the verifier rebuilds it rather than trusting it. + let mut tampered = verified.lifecycle.clone(); + for corrupt in [ + &mut tampered.times.deleted_at_ms, + &mut tampered.times.erasing_started_at_ms, + &mut tampered.times.erasing_from_time_ms, + &mut tampered.times.erasing_from_revision, + ] { + *corrupt += 1; + } + tampered.remaining_revisions += 1; + tampered.state = DocumentHistoryState::Deleted; + assert_ne!( + tampered, verified.lifecycle, + "the verifier's answer is derived, so a different claim cannot match it" + ); + } + + // And the terminal state after the erasure finishes. + erase(&drive, &contract, id, 7_000); + let query = query(DocumentHistorySelector::StartAtTime(0)); + let (page, proof) = drive + .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + .expect("expected to prove the absent id"); + let (_, verified) = + Drive::verify_document_history_v1(&query, &proof, document_type_of(&contract), latest()) + .expect("expected the absence proof to verify"); + assert_eq!(verified, page); + assert_eq!(verified.lifecycle.state, DocumentHistoryState::Absent); + assert_eq!(verified.lifecycle.remaining_revisions, 0); + assert_eq!(verified.lifecycle.times, Default::default()); + assert!( + proof.entries_proof.is_none(), + "there is no history tree left to page over" + ); +} + +/// After a partial erasure the surviving revisions are the oldest ones and +/// still number from one, so a by-revision read lands on the revision it asks +/// for rather than on whatever is now at that offset. +#[test] +fn should_read_revisions_by_position_after_a_partial_erasure() { + let chunk = chunk_size(); + let owner = [48u8; 32]; + let (drive, contract, id) = setup_history(chunk + 3, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + erase(&drive, &contract, id, 6_000); + + for revision in 1..=3u64 { + let query = DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + selector: DocumentHistorySelector::Revision(revision), + limit: Some(1), + }; + let page = drive + .fetch_document_history_v1(&query, document_type_of(&contract), None, latest()) + .expect("expected to read a surviving revision"); + assert_eq!( + page.entries.len(), + 1, + "revision {revision} survives a partial erasure" + ); + assert_eq!(page.entries[0].revision, revision); + } + + // The revisions the chunk removed are gone, not silently answered with a + // neighbour. + let query = DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + selector: DocumentHistorySelector::Revision(4), + limit: Some(1), + }; + let page = drive + .fetch_document_history_v1(&query, document_type_of(&contract), None, latest()) + .expect("expected the read to run"); + assert!( + page.entries.is_empty(), + "a removed revision must not be answered with the one that took its place" + ); +} diff --git a/packages/rs-drive/src/query/document_history_drive_query.rs b/packages/rs-drive/src/query/document_history_drive_query.rs index f1ea0f5f75a..584ce61a7a3 100644 --- a/packages/rs-drive/src/query/document_history_drive_query.rs +++ b/packages/rs-drive/src/query/document_history_drive_query.rs @@ -349,10 +349,8 @@ impl DocumentHistoryDriveQuery { (true, _) => DocumentHistoryState::Active, (false, Some(record)) if record.is_erasing() => DocumentHistoryState::Erasing, (false, Some(_)) => DocumentHistoryState::Deleted, - // A history with no record and no current pointer predates the - // lifecycle record; every lifecycle delete writes one, so this is - // retained defensively while old histories are migrated. - (false, None) if count.unwrap_or_default() > 0 => DocumentHistoryState::Deleted, + // Retained history with no record and no pointer was rejected as + // inconsistent above, so nothing is left here but an unused id. (false, None) => DocumentHistoryState::Absent, }; let times = record diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs index 0182f23bdec..96b7c9f3d82 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs @@ -32,8 +32,9 @@ impl DriveHighLevelBatchOperationConverter for DocumentEraseTransitionAction { let data_contract_id = base.data_contract_id(); let identity_contract_nonce = base.identity_contract_nonce(); - // No token operation: erase carries no token cost, and its - // structure validation refuses a transition that offers one. + // No token operation: erase carries no token cost, and a + // transition that offers to pay one is refused where the offer + // is still visible, when the transition becomes an action. Ok(vec![ IdentityOperation(IdentityOperationType::UpdateIdentityContractNonce { identity_id: owner_id.into_buffer(), From b273c0e2035a244a8863c97de7b8af62a9564164 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 08:32:04 +0700 Subject: [PATCH 21/48] test(platform-test-suite): read the network from the environment The suite drives writes through the legacy client, which exposes no getNetwork, so the browser runner threw a TypeError out of the before-all hook and every case in the spec failed before it ran. Read NETWORK from the environment instead, the way the other functional specs that reach for the Evo SDK already do. dotenv-safe requires the key and karma's EnvironmentPlugin copies it into the bundle, so the one expression serves both the node and the browser runner. The failure was a TypeError in the hook rather than a wrong value, so there is no behaviour to pin: the assertion that catches a regression here is the hook completing at all, which the suite run itself is. Verified with yarn workspace @dashevo/platform-test-suite lint, whose only complaint about this file is the unresolved "dash" import that every functional spec reports in a worktree where the legacy SDK is not built. The suite itself needs a running network and was not run. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/functional/platform/KeepHistoryDocument.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js index 0901c587db9..7eae196846e 100644 --- a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js +++ b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js @@ -73,7 +73,7 @@ describe('Platform', () => { // Additional wait time to mitigate testnet latency await waitForSTPropagated(); - ({ evo, sdk: evoSdk } = await getEvoSdkForNetwork(client.getNetwork())); + ({ evo, sdk: evoSdk } = await getEvoSdkForNetwork(process.env.NETWORK)); }); after(async () => { From fce51929309be75aae8b785c97bd19a50e818297 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 10:47:35 +0700 Subject: [PATCH 22/48] test(platform-test-suite): refetch the note before every keep-history replace The legacy SDK signs revision + 1 from the document object it is handed and never bumps that local copy, so a second replace of the same object resent revision 2 and Drive rejected it (both the node and the browser suite failed on the first KeepHistoryDocument case). Refetch the stored document before each edit, the way Document.spec.js already does. Co-Authored-By: Claude Fable 5.1 --- .../functional/platform/KeepHistoryDocument.spec.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js index 7eae196846e..d034df1d71f 100644 --- a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js +++ b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js @@ -129,8 +129,14 @@ describe('Platform', () => { await waitForSTPropagated(); for (const message of ['second', 'third']) { - note.set('message', message); - await client.platform.documents.broadcast({ replace: [note] }, identity); + // The SDK signs `revision + 1` from the document it is handed but never + // bumps that local copy, so every replace starts from a fresh fetch. + const [stored] = await client.platform.documents.get( + 'notes.note', + { where: [['$id', '==', note.getId()]] }, + ); + stored.set('message', message); + await client.platform.documents.broadcast({ replace: [stored] }, identity); // Additional wait time to mitigate testnet latency await waitForSTPropagated(); From 8db8a6be533a5ca2814550e28b12b242bceaf1a1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 12:08:15 +0700 Subject: [PATCH 23/48] test(platform-test-suite): sign the keep-history erase with the HIGH key The erase helper signed with identity key 0, the MASTER key, and Drive rejected it: document transitions require a HIGH or CRITICAL key. Use key 1, the HIGH authentication key the legacy SDK registers. Co-Authored-By: Claude Fable 5.1 --- .../test/functional/platform/KeepHistoryDocument.spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js index d034df1d71f..0b5ac059227 100644 --- a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js +++ b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js @@ -22,18 +22,22 @@ const { * @param {Object} identity * @returns {Promise<{ identityKey: Object, signer: Object }>} */ +// Key 1 is the identity's HIGH authentication key: document transitions +// require a HIGH or CRITICAL key, and key 0 is the MASTER key. +const DOCUMENT_SIGNING_KEY_ID = 1; + async function evoSignerFor(evo, evoSdk, client, identity) { const account = await client.getWalletAccount(); const { privateKey } = account.identities.getIdentityHDKeyById( identity.getId().toString(), - 0, + DOCUMENT_SIGNING_KEY_ID, ); const signer = new evo.IdentitySigner(); signer.addKeyFromWif(privateKey.toWIF()); const fetched = await evoSdk.identities.fetch(identity.getId().toString()); - const identityKey = fetched.getPublicKeyById(0); + const identityKey = fetched.getPublicKeyById(DOCUMENT_SIGNING_KEY_ID); return { identityKey, signer }; } From f58df76ca855784b7da04986f65a8d54cd86262f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 11 Sep 2026 13:07:08 +0700 Subject: [PATCH 24/48] test(platform-test-suite): wait for the refused delete to propagate before erasing The browser suite timed out on the erase: the Evo SDK fetched the identity contract nonce from a node that had not yet committed the block holding the refused delete, reused nonce 6, and that node's mempool dropped the erase as a duplicate sender while the broadcast still reported success. Every other broadcast in the spec is followed by waitForSTPropagated(); the refused delete was the one exception. Co-Authored-By: Claude Fable 5.1 --- .../test/functional/platform/KeepHistoryDocument.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js index 0b5ac059227..9f6029adc5c 100644 --- a/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js +++ b/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js @@ -187,6 +187,11 @@ describe('Platform', () => { // DocumentNotFoundError: a document nothing can read is not there to be // deleted again, and a delete never escalates into removing a revision. expect(broadcastError.code).to.equal(40101); + + // The refused delete still consumed a contract nonce in a block. Let every + // node commit it before the erase fetches that nonce, or a lagging node + // hands out the old value and its mempool drops the erase as a duplicate. + await waitForSTPropagated(); }); it('should erase the retained revisions of a deleted note', async () => { From 98e99e3360011b27b9b00a72577d22d46f8445e6 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 10:03:21 +0700 Subject: [PATCH 25/48] fix(wasm-sdk): let an erase name the identity that submits it The erase builder's owner is the identity that submits and pays, and the wrapper always filled it with the document's owner, so a continuation signed by another identity was built under the owner's identity and nonce and failed validation. DocumentEraseOptions gains an optional identityId for the submitter, defaulting to the document owner so the first erase keeps working unchanged. Co-Authored-By: Claude Fable 5.1 --- .../src/state_transitions/document.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/wasm-sdk/src/state_transitions/document.rs b/packages/wasm-sdk/src/state_transitions/document.rs index 52e90e9ac31..d601d579da2 100644 --- a/packages/wasm-sdk/src/state_transitions/document.rs +++ b/packages/wasm-sdk/src/state_transitions/document.rs @@ -531,6 +531,15 @@ export interface DocumentEraseOptions { documentTypeName: string; }; + /** + * The identity submitting and paying for this erase. It is the identity the + * key and signer below belong to, and its contract nonce is consumed. + * Defaults to the document's owner, which is who the first erase must come + * from; the erases after it may come from any identity, which then names + * itself here. + */ + identityId?: IdentifierLike; + /** * The identity public key to use for signing the transition. * The first erase must be signed by the document's owner; any identity may @@ -611,6 +620,15 @@ impl WasmSdk { ) }; + // The builder's owner is the identity that submits and pays; only the + // first erase has to be the document's owner, so a continuation names + // its own identity here and keeps the document owner as metadata. + let submitter_id: Identifier = + match try_from_options_optional::(&options, "identityId")? { + Some(identity_id) => identity_id.into(), + None => owner_id, + }; + let identity_key_wasm = IdentityPublicKeyWasm::try_from_options(&options, "identityKey")?; let identity_key: IdentityPublicKey = identity_key_wasm.into(); let signer = IdentitySignerWasm::try_from_options(&options, "signer")?; @@ -622,7 +640,7 @@ impl WasmSdk { Arc::new(data_contract), document_type_name, document_id, - owner_id, + submitter_id, ); let builder = if let Some(s) = settings { builder.with_settings(s) From fc9fb5d8823e84fd5d1464145c7a04587303dac4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 13:37:36 +0700 Subject: [PATCH 26/48] fix(dpp): keep loading legacy contested keep-history contracts at protocol 14 The generation-3 parser refused a keep-history type with a contested index in every mode, but that combination was legal before protocol 14 and every stored contract is loaded through the structural parse. A node activating protocol 14 would have failed to deserialize such a contract while inventorying contracts for the history migration, and the activation hook would have aborted before migrating anything. The refusal now applies only under full validation, where registration happens. A parser test loads the combination without validation, and a migration test registers such a contract at protocol 13 and migrates it at protocol 14. Co-Authored-By: Claude Fable 5.1 --- .../try_from_schema/v3/keep_history_tests.rs | 33 +++++++ .../class_methods/try_from_schema/v3/mod.rs | 7 +- .../src/drive/document/migration/tests.rs | 87 +++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs index a97cf7f28f2..65f664341ee 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs @@ -249,6 +249,39 @@ fn should_reject_a_keep_history_type_that_carries_a_contested_index() { assert_contract_structure_consensus_error(&error); } +/// The refusal is a registration rule. A contract that carries the combination +/// was registered under a protocol that allowed it, and every stored contract +/// is loaded through the structural parse, so that parse must keep reading it +/// or the activation that migrates its history could not even start. +#[test] +fn should_still_load_a_legacy_contested_keep_history_type_without_validation() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], + "additionalProperties": false, + "documentsMutable": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }); + let document_type = parse_at_version(schema, 14, false) + .expect("a stored contract with this combination must still load"); + assert!(document_type.documents_keep_history()); +} + /// The same index without history is unaffected: the refusal is about the /// combination, not about contested indexes. #[test] diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index c91180e4f47..b84d9937f43 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -317,7 +317,12 @@ fn try_from_schema_generation_3( // it sees the value resolved against the contract config default (`true` // when the key is omitted). common::apply_can_be_erased(&mut v2, can_be_erased, name)?; - common::reject_contested_keep_history(&v2, name)?; + // A registration-time rule only: a contract that already carries this + // combination was registered under an earlier protocol, and the structural + // parse that loads stored contracts must keep reading it. + if full_validation { + common::reject_contested_keep_history(&v2, name)?; + } Ok(v2) } diff --git a/packages/rs-drive/src/drive/document/migration/tests.rs b/packages/rs-drive/src/drive/document/migration/tests.rs index 3513d0075ce..8a23bda8061 100644 --- a/packages/rs-drive/src/drive/document/migration/tests.rs +++ b/packages/rs-drive/src/drive/document/migration/tests.rs @@ -638,3 +638,90 @@ fn should_halt_migration_when_an_inventoried_index_reference_was_not_rewritten() .unwrap(); assert_eq!(stats.rewritten_index_entries, references.len() as u64); } + +/// A keep-history type with a contested index could be registered before +/// protocol 14. Activation loads every stored contract through the structural +/// parse, which must keep reading that combination, or the migration would +/// abort on the first such contract before touching any history. +#[test] +fn should_migrate_a_legacy_contested_keep_history_contract() { + use dpp::data_contract::DataContractFactory; + use dpp::document::document_factory::DocumentFactory; + use dpp::platform_value::platform_value; + + let old = PlatformVersion::get(13).unwrap(); + let new = PlatformVersion::get(14).unwrap(); + let schema = platform_value!({ + "type": "object", + "documentsKeepHistory": true, + "canBeDeleted": false, + "documentsMutable": false, + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "required": ["label"], + "additionalProperties": false, + "indices": [{ + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }], + }); + let contract = DataContractFactory::new(13) + .unwrap() + .create_with_value_config( + [7; 32].into(), + 0, + platform_value!({"name": schema}), + None, + None, + ) + .expect("protocol 13 admits a contested keep-history type") + .data_contract_owned(); + let drive = setup_drive_with_initial_state_structure(None); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, old) + .unwrap(); + let document_type = contract.document_type_for_name("name").unwrap(); + let mut document = DocumentFactory::new(13) + .unwrap() + .create_document( + &contract, + [7; 32].into(), + "name".into(), + platform_value!({"label": "alice"}), + ) + .unwrap(); + // An immutable type carries no revision; its history is keyed by time. + document.set_id([9; 32].into()); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(1_000), + true, + None, + old, + None, + ) + .unwrap(); + + let transaction = drive.grove.start_transaction(); + let stats = drive + .migrate_document_history_storage(&transaction, new) + .expect("a stored contested keep-history contract must load and migrate"); + assert_eq!(stats.contracts, 1); + assert_eq!(stats.types, 1); + assert_eq!(stats.migrated_documents, 1); +} From 007044d1f4d27df601d9164cc05f18b8fe3da9a4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 13:37:37 +0700 Subject: [PATCH 27/48] fix(drive): keep refusing by-revision reads of a gapped history after deletion A by-revision read maps the revision onto a position, which only holds while the retained revisions number one through the latest without a gap. The check compared the current document's revision with the count, so it ran only while the document was current: once a delete removed the pointer, a history that protocol 13 left gapped answered a revision read with an empty page, and that page verified as an authenticated absence. The lifecycle record now keeps the revision the document was deleted with and how many revisions the history retained, read off the history tree's count during the delete. An erase removes the newest revisions first and cannot open a gap, so those two fields say for the rest of the document's life whether a by-revision read is meaningful, and the gap check consults them for deleted and erasing documents. Test would have caught this in CI: with the previous check the new regression returned an empty page for revision 3 of a deleted [1, 3] history; with this change it is refused. Co-Authored-By: Claude Fable 5.1 --- .../v1/mod.rs | 87 +++++++-- .../src/drive/document/lifecycle/mod.rs | 80 ++++++-- .../src/drive/document/lifecycle/tests.rs | 174 ++++++++++++++++++ .../src/query/document_history_drive_query.rs | 23 ++- 4 files changed, 324 insertions(+), 40 deletions(-) diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 7db9034b833..675dba0a638 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -5,10 +5,11 @@ use dpp::data_contract::document_type::DocumentTypeRef; use std::collections::HashMap; -use crate::drive::constants::DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE; +use crate::drive::constants::{DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE, STORAGE_FLAGS_SIZE}; use crate::drive::document::lifecycle::DocumentLifecycleRecord; use crate::drive::document::paths::{ - contract_documents_primary_key_path, document_lifecycle_path, DOCUMENT_LIFECYCLE_TREE_KEY, + contract_documents_primary_key_path, document_history_path, document_lifecycle_path, + DOCUMENT_LIFECYCLE_TREE_KEY, }; use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; use crate::util::object_size_info::DocumentInfo::{ @@ -18,11 +19,11 @@ use crate::util::storage_flags::StorageFlags; use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; -use dpp::document::Document; +use dpp::document::{Document, DocumentV0Getters}; use crate::drive::Drive; -use crate::util::grove_operations::QueryTarget::QueryTargetValue; -use crate::util::grove_operations::{BatchInsertTreeApplyType, QueryType}; +use crate::util::grove_operations::QueryTarget::{QueryTargetTree, QueryTargetValue}; +use crate::util::grove_operations::{BatchInsertTreeApplyType, DirectQueryType, QueryType}; use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; use crate::util::object_size_info::PathKeyInfo::PathKey; use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; @@ -157,21 +158,67 @@ impl Drive { &platform_version.drive, )?; - let document_info = if let QueryType::StatelessQuery { query_target, .. } = query_type { - DocumentEstimatedAverageSize(query_target.len()) - } else if let Some(document_element) = &document_element { - let Element::Item(data, element_flags) = document_element else { - return Err(Error::Drive(DriveError::CorruptedDocumentNotItem( - "the current pointer of a keep-history document did not resolve to an item", + let (document_info, latest_revision) = + if let QueryType::StatelessQuery { query_target, .. } = query_type { + (DocumentEstimatedAverageSize(query_target.len()), 0) + } else if let Some(document_element) = &document_element { + let Element::Item(data, element_flags) = document_element else { + return Err(Error::Drive(DriveError::CorruptedDocumentNotItem( + "the current pointer of a keep-history document did not resolve to an item", + ))); + }; + let document = + Document::from_bytes(data.as_slice(), document_type, platform_version)?; + let storage_flags = StorageFlags::map_cow_some_element_flags_ref(element_flags)?; + let latest_revision = document.revision().unwrap_or(1); + ( + DocumentOwnedInfo((document, storage_flags)), + latest_revision, + ) + } else { + return Err(Error::Drive(DriveError::DeletingDocumentThatDoesNotExist( + "document being deleted does not exist", ))); }; - let document = Document::from_bytes(data.as_slice(), document_type, platform_version)?; - let storage_flags = StorageFlags::map_cow_some_element_flags_ref(element_flags)?; - DocumentOwnedInfo((document, storage_flags)) + + // The record remembers how many revisions the history retained next to + // the revision being deleted, so a later by-revision read can tell a + // contiguous history from one a pre-protocol-14 overwrite left gapped. + // That count is the history tree's own aggregate, read off its element + // in the type's history tree. + let mut history_type_path = document_history_path( + contract.id_ref().as_bytes(), + document_type.name().as_str(), + document_id.as_slice(), + ); + history_type_path.pop(); + let history_query_type = if estimated_costs_only_with_layer_info.is_some() { + DirectQueryType::StatelessDirectQuery { + in_tree_type: TreeType::NormalTree, + query_target: QueryTargetTree(STORAGE_FLAGS_SIZE, TreeType::ProvableCountTree), + } } else { - return Err(Error::Drive(DriveError::DeletingDocumentThatDoesNotExist( - "document being deleted does not exist", - ))); + DirectQueryType::StatefulDirectQuery + }; + let history_element = self.grove_get_raw( + history_type_path.as_slice().into(), + document_id.as_slice(), + history_query_type, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + let revision_count = match ( + estimated_costs_only_with_layer_info.is_some(), + history_element, + ) { + (true, _) => 0, + (false, Some(Element::ProvableCountTree(_, count, _))) => count, + (false, _) => { + return Err(Error::Drive(DriveError::CorruptedDriveState( + "a keep-history document being deleted has no history tree".to_string(), + ))) + } }; // The pointer goes; the revisions it named stay in the history tree. @@ -213,7 +260,11 @@ impl Drive { document_id, contract, document_type, - DocumentLifecycleRecord::deleted_at(block_info.time_ms), + DocumentLifecycleRecord::deleted_at( + block_info.time_ms, + latest_revision, + revision_count, + ), record_flags.as_ref(), estimated_costs_only_with_layer_info, transaction, diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index e864de37ffd..80a8c8f8700 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -26,9 +26,9 @@ pub use fetch::DocumentLifecycleState; use crate::error::{drive::DriveError, Error}; -/// Byte length of an encoded lifecycle record: one version byte and four +/// Byte length of an encoded lifecycle record: one version byte and six /// big-endian `u64`s. -pub const DOCUMENT_LIFECYCLE_RECORD_SIZE: u32 = 33; +pub const DOCUMENT_LIFECYCLE_RECORD_SIZE: u32 = 49; /// Version byte of the only record layout that exists. const DOCUMENT_LIFECYCLE_RECORD_V0: u8 = 0; @@ -44,6 +44,15 @@ const DOCUMENT_LIFECYCLE_RECORD_V0: u8 = 0; pub struct DocumentLifecycleRecordV0 { /// Block time the document was deleted at. pub deleted_at_ms: u64, + /// Revision the document carried when it was deleted. + pub latest_revision: u64, + /// Number of revisions the history retained when the document was deleted. + /// + /// Together with `latest_revision` this says whether the retained history + /// was contiguous: an erase removes the newest revisions first, so what + /// survives afterwards is a prefix of what was retained here, and a + /// by-revision read stays meaningful exactly when the two are equal. + pub revision_count: u64, /// Block time an authorized erasure started at, or zero while none has. pub erasing_started_at_ms: u64, /// Timestamp component of the newest revision retained when the erasure @@ -62,10 +71,14 @@ pub enum DocumentLifecycleRecord { } impl DocumentLifecycleRecord { - /// Builds the record a delete writes: a deletion time and no erasure. - pub fn deleted_at(deleted_at_ms: u64) -> Self { + /// Builds the record a delete writes: the deletion time, what the deleted + /// document's revision was and how many revisions its history retained, + /// and no erasure. + pub fn deleted_at(deleted_at_ms: u64, latest_revision: u64, revision_count: u64) -> Self { DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { deleted_at_ms, + latest_revision, + revision_count, ..Default::default() }) } @@ -77,6 +90,31 @@ impl DocumentLifecycleRecord { } } + /// Revision the document carried when it was deleted. + pub fn latest_revision(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.latest_revision, + } + } + + /// Number of revisions the history retained when the document was deleted. + pub fn revision_count(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.revision_count, + } + } + + /// Whether the revisions retained at deletion numbered one through the + /// deleted revision without a gap, which is what lets a by-revision read + /// map a revision onto a position in the history. + /// + /// A gap can only come from a history written before protocol 14, where + /// two writes in one block overwrote each other's revision; an erase + /// removes the newest revisions first and so never opens one. + pub fn revisions_are_contiguous(&self) -> bool { + self.latest_revision() == self.revision_count() + } + /// Block time an authorized erasure started at, or zero while none has. pub fn erasing_started_at_ms(&self) -> u64 { match self { @@ -119,13 +157,15 @@ impl DocumentLifecycleRecord { ) -> Self { DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { deleted_at_ms: self.deleted_at_ms(), + latest_revision: self.latest_revision(), + revision_count: self.revision_count(), erasing_started_at_ms, erasing_from_time_ms, erasing_from_revision, }) } - /// Encodes the record as one version byte followed by four big-endian + /// Encodes the record as one version byte followed by six big-endian /// `u64`s. /// /// The layout is fixed width rather than the variable-length integer @@ -138,6 +178,8 @@ impl DocumentLifecycleRecord { DocumentLifecycleRecord::V0(v0) => { bytes.push(DOCUMENT_LIFECYCLE_RECORD_V0); bytes.extend(v0.deleted_at_ms.to_be_bytes()); + bytes.extend(v0.latest_revision.to_be_bytes()); + bytes.extend(v0.revision_count.to_be_bytes()); bytes.extend(v0.erasing_started_at_ms.to_be_bytes()); bytes.extend(v0.erasing_from_time_ms.to_be_bytes()); bytes.extend(v0.erasing_from_revision.to_be_bytes()); @@ -161,9 +203,9 @@ impl DocumentLifecycleRecord { if *version != DOCUMENT_LIFECYCLE_RECORD_V0 { return Err(corrupt("unknown document lifecycle record version")); } - if fields.len() != 32 { + if fields.len() != 48 { return Err(corrupt( - "document lifecycle record does not contain four timestamps", + "document lifecycle record does not contain six fields", )); } let field = |index: usize| -> u64 { @@ -173,9 +215,11 @@ impl DocumentLifecycleRecord { }; Ok(DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { deleted_at_ms: field(0), - erasing_started_at_ms: field(1), - erasing_from_time_ms: field(2), - erasing_from_revision: field(3), + latest_revision: field(1), + revision_count: field(2), + erasing_started_at_ms: field(3), + erasing_from_time_ms: field(4), + erasing_from_revision: field(5), })) } } @@ -186,7 +230,7 @@ mod record_tests { #[test] fn should_encode_deleted_and_erasing_records_to_the_same_length() { - let deleted = DocumentLifecycleRecord::deleted_at(1_700_000_000_000); + let deleted = DocumentLifecycleRecord::deleted_at(1_700_000_000_000, 42, 42); let erasing = deleted.starting_erase_at(1_700_000_500_000, 1_699_000_000_000, 42); assert_eq!( deleted.serialize().len(), @@ -201,16 +245,20 @@ mod record_tests { #[test] fn should_round_trip_every_field() { - let record = DocumentLifecycleRecord::deleted_at(7).starting_erase_at(8, 9, 10); + let record = DocumentLifecycleRecord::deleted_at(7, 12, 11).starting_erase_at(8, 9, 10); let recovered = DocumentLifecycleRecord::deserialize(&record.serialize()).expect("round trip"); assert_eq!(record, recovered); assert!(recovered.is_erasing()); + assert_eq!(recovered.latest_revision(), 12); + assert_eq!(recovered.revision_count(), 11); + assert!(!recovered.revisions_are_contiguous()); + assert!(DocumentLifecycleRecord::deleted_at(7, 11, 11).revisions_are_contiguous()); } #[test] fn should_report_a_record_without_erase_fields_as_deleted_not_erasing() { - let record = DocumentLifecycleRecord::deleted_at(7); + let record = DocumentLifecycleRecord::deleted_at(7, 1, 1); assert!(!record.is_erasing()); assert_eq!(record.erasing_from_revision(), 0); } @@ -219,16 +267,16 @@ mod record_tests { /// one that never started. #[test] fn should_report_an_erasure_started_in_a_block_at_time_zero_as_erasing() { - let record = DocumentLifecycleRecord::deleted_at(0).starting_erase_at(0, 0, 1); + let record = DocumentLifecycleRecord::deleted_at(0, 1, 1).starting_erase_at(0, 0, 1); assert!(record.is_erasing()); } #[test] fn should_reject_records_that_do_not_consume_their_whole_input() { - let mut bytes = DocumentLifecycleRecord::deleted_at(7).serialize(); + let mut bytes = DocumentLifecycleRecord::deleted_at(7, 1, 1).serialize(); bytes.push(0); assert!(DocumentLifecycleRecord::deserialize(&bytes).is_err()); assert!(DocumentLifecycleRecord::deserialize(&[]).is_err()); - assert!(DocumentLifecycleRecord::deserialize(&[9; 33]).is_err()); + assert!(DocumentLifecycleRecord::deserialize(&[9; 49]).is_err()); } } diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index abe90ce3313..209154bb6b6 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -1639,3 +1639,177 @@ fn should_read_revisions_by_position_after_a_partial_erasure() { "a removed revision must not be answered with the one that took its place" ); } + +/// A history that protocol 13 left gapped keeps refusing by-revision reads +/// after the document is deleted: a revision no longer maps onto a position, +/// and an empty page for a revision that exists would otherwise verify as an +/// authenticated absence. +#[test] +fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { + let old = PlatformVersion::get(13).unwrap(); + let new = PlatformVersion::get(14).unwrap(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract( + "tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json", + false, + old, + ) + .unwrap(); + drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, old) + .unwrap(); + let document_type = contract.document_type_for_name("profile").unwrap(); + let owner = [9u8; 32]; + let mut document = json_document_to_document( + "tests/supporting_files/contract/dashpay/profile0.json", + Some(owner.into()), + document_type, + old, + ) + .unwrap(); + // Two writes in one block under protocol 13 overwrite each other's + // revision, so revision 2 is lost and the retained history is [1, 3]. + for (revision, time) in [(1, 1_000), (2, 2_000), (3, 2_000)] { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &document, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(time), + true, + None, + old, + None, + ) + .unwrap(); + } + let transaction = drive.grove.start_transaction(); + drive + .migrate_document_history_storage(&transaction, new) + .expect("expected the migration to run"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected the migration to commit"); + let id = document.id(); + + let query = |selector| DocumentHistoryQueryV1 { + contract_id: contract.id().to_buffer(), + document_type_name: "profile".into(), + document_id: id.to_buffer(), + // A single-revision read is capped at one entry by the selector's + // own rule; the page selectors take the full page. + limit: Some(match selector { + DocumentHistorySelector::Revision(_) => 1, + _ => 10, + }), + selector, + }; + let by_revision = [ + DocumentHistorySelector::Revision(3), + DocumentHistorySelector::StartAtRevision(3), + DocumentHistorySelector::Revision(2), + ]; + let is_gap_refusal = |error: Error| { + let message = error.to_string(); + assert!( + message.contains("revision gap"), + "expected the gap refusal, got {message}" + ); + }; + + // While the document is current the gap already refuses both selectors. + for selector in by_revision.clone() { + is_gap_refusal( + drive + .fetch_document_history_v1(&query(selector), document_type, None, new) + .expect_err("a gapped history refuses by-revision reads"), + ); + } + + let apply = |batch: Vec| { + drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut vec![], &new.drive) + .expect("expected to apply the operations"); + }; + apply( + drive + .delete_document_for_contract_operations( + id, + &contract, + document_type, + &BlockInfo::default_with_time(5_000), + Some(Identifier::new(owner)), + None, + &mut None, + None, + new, + ) + .expect("expected to build the delete"), + ); + let page = drive + .fetch_document_history_v1( + &query(DocumentHistorySelector::StartAtTime(0)), + document_type, + None, + new, + ) + .expect("time pagination still reads a deleted gapped history"); + assert_eq!(page.lifecycle.state, DocumentHistoryState::Deleted); + assert_eq!(page.lifecycle.remaining_revisions, 2); + assert_eq!( + page.entries + .iter() + .map(|entry| entry.revision) + .collect::>(), + vec![1, 3] + ); + for selector in by_revision.clone() { + let query = query(selector.clone()); + is_gap_refusal( + drive + .fetch_document_history_v1(&query, document_type, None, new) + .expect_err("a deleted gapped history refuses by-revision reads"), + ); + is_gap_refusal( + drive + .prove_document_history_v1(&query, document_type, None, new) + .expect_err("a deleted gapped history cannot be proved by revision"), + ); + } + + // Two revisions fit in one erase chunk, so the erasure finishes at once + // and the id is unused again: a by-revision read of an unused id is an + // ordinary empty page, authenticated as an absence rather than refused. + apply( + drive + .erase_document_for_contract_operations( + id, + &contract, + document_type, + &BlockInfo::default_with_time(6_000), + &mut None, + None, + new, + ) + .expect("expected to build the erase"), + ); + for selector in by_revision { + let page = drive + .fetch_document_history_v1(&query(selector), document_type, None, new) + .expect("an unused id reads as an authenticated absence"); + assert_eq!(page.lifecycle.state, DocumentHistoryState::Absent); + assert!(page.entries.is_empty()); + } +} diff --git a/packages/rs-drive/src/query/document_history_drive_query.rs b/packages/rs-drive/src/query/document_history_drive_query.rs index 584ce61a7a3..beb1c971056 100644 --- a/packages/rs-drive/src/query/document_history_drive_query.rs +++ b/packages/rs-drive/src/query/document_history_drive_query.rs @@ -335,15 +335,26 @@ impl DocumentHistoryDriveQuery { if record.is_some() && count.unwrap_or_default() == 0 { return Err(corrupt("a lifecycle record survives its retained history")); } + // A by-revision read maps the revision onto a position, which only + // holds while the retained revisions number one through the latest + // without a gap. A current document says so through its revision; a + // deleted one through what its record kept from the moment of + // deletion, since an erase only ever removes the newest revisions and + // so cannot open a gap afterwards. if matches!( self.filter, DocumentHistoryFilter::Revision(_) | DocumentHistoryFilter::StartAtRevision(_) - ) && active - && latest_revision != count - { - return Err(invalid( - "retained history contains a revision gap; use time pagination", - )); + ) { + let gapped = match (active, &record) { + (true, _) => latest_revision != count, + (false, Some(record)) => !record.revisions_are_contiguous(), + (false, None) => false, + }; + if gapped { + return Err(invalid( + "retained history contains a revision gap; use time pagination", + )); + } } let state = match (active, &record) { (true, _) => DocumentHistoryState::Active, From f0edec26f26a01c319e920bc6adb8db966a3b897 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 13:37:37 +0700 Subject: [PATCH 28/48] fix(sdk): keep every lifecycle state and time in mocked document histories The mock codec kept only whether the lifecycle was active and how many revisions remained, so a mocked deleted or erasing history came back absent with default times. It now carries the state discriminant and the four lifecycle times, with a round-trip test over every state. Co-Authored-By: Claude Fable 5.1 --- packages/rs-drive-proof-verifier/src/types.rs | 3 +- packages/rs-sdk/src/mock/requests.rs | 106 ++++++++++++++++-- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index d8839998c15..f9633cf60e0 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -115,7 +115,8 @@ pub type RetrievedValues = IndexMap; /// Contains a map of data contract revisions to data contracts. pub type DataContractHistory = RetrievedValues; pub use drive::query::document_history_drive_query::{ - DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryState, + DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryLifecycleTimes, + DocumentHistoryState, }; /// Ordered document revisions with lifecycle metadata when the storage layout supports it. diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 902a9559792..806a875ad05 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -234,7 +234,13 @@ impl MockResponse for Document { } } -type MockDocumentHistory = (Vec<(u64, u64, Vec)>, Option<(bool, u64)>); +/// Entries as (time, revision, document bytes); the lifecycle as its state +/// discriminant, the remaining revision count and the four lifecycle times, +/// so a mocked deleted or erasing history round-trips with its metadata. +type MockDocumentHistory = ( + Vec<(u64, u64, Vec)>, + Option<(u8, u64, u64, u64, u64, u64)>, +); impl MockResponse for drive_proof_verifier::types::DocumentHistoryProofInfo { fn mock_serialize(&self, sdk: &MockDashPlatformSdk) -> Vec { @@ -277,9 +283,19 @@ impl MockResponse for drive_proof_verifier::types::DocumentHistory { }) .collect::>(); let lifecycle = self.lifecycle.as_ref().map(|lifecycle| { + let state = match lifecycle.state { + DocumentHistoryState::Active => 0u8, + DocumentHistoryState::Deleted => 1, + DocumentHistoryState::Erasing => 2, + DocumentHistoryState::Absent => 3, + }; ( - lifecycle.state == DocumentHistoryState::Active, + state, lifecycle.remaining_revisions, + lifecycle.times.deleted_at_ms, + lifecycle.times.erasing_started_at_ms, + lifecycle.times.erasing_from_time_ms, + lifecycle.times.erasing_from_revision, ) }); bincode::encode_to_vec((entries, lifecycle), BINCODE_CONFIG) @@ -288,7 +304,8 @@ impl MockResponse for drive_proof_verifier::types::DocumentHistory { fn mock_deserialize(sdk: &MockDashPlatformSdk, buf: &[u8]) -> Self { use drive_proof_verifier::types::{ - DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryState, + DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryLifecycleTimes, + DocumentHistoryState, }; let ((entries, lifecycle), _): (MockDocumentHistory, _) = bincode::decode_from_slice(buf, BINCODE_CONFIG).expect("decode document history"); @@ -301,19 +318,86 @@ impl MockResponse for drive_proof_verifier::types::DocumentHistory { document: Document::mock_deserialize(sdk, &bytes), }) .collect(), - lifecycle: lifecycle.map(|(active, remaining_revisions)| DocumentHistoryLifecycle { - state: if active { - DocumentHistoryState::Active - } else { - DocumentHistoryState::Absent + lifecycle: lifecycle.map( + |( + state, + remaining_revisions, + deleted_at_ms, + erasing_started_at_ms, + erasing_from_time_ms, + erasing_from_revision, + )| DocumentHistoryLifecycle { + state: match state { + 0 => DocumentHistoryState::Active, + 1 => DocumentHistoryState::Deleted, + 2 => DocumentHistoryState::Erasing, + _ => DocumentHistoryState::Absent, + }, + remaining_revisions, + times: DocumentHistoryLifecycleTimes { + deleted_at_ms, + erasing_started_at_ms, + erasing_from_time_ms, + erasing_from_revision, + }, }, - remaining_revisions, - times: Default::default(), - }), + ), } } } +#[cfg(test)] +mod document_history_mock_tests { + use super::*; + use drive_proof_verifier::types::{ + DocumentHistory, DocumentHistoryLifecycle, DocumentHistoryLifecycleTimes, + DocumentHistoryState, + }; + + /// Every lifecycle state and every lifecycle time survives the mock + /// round trip, so a mocked deleted or erasing history looks to the caller + /// exactly as the verified one would. + #[test] + fn should_round_trip_every_lifecycle_state_with_its_times() { + let mut sdk = crate::SdkBuilder::default() + .with_version(dpp::version::PlatformVersion::get(14).unwrap()) + .build() + .unwrap(); + let mock = sdk.mock(); + let sdk: &MockDashPlatformSdk = &mock; + for (state, remaining_revisions) in [ + (DocumentHistoryState::Active, 3), + (DocumentHistoryState::Deleted, 3), + (DocumentHistoryState::Erasing, 1), + (DocumentHistoryState::Absent, 0), + ] { + let history = DocumentHistory { + entries: vec![], + lifecycle: Some(DocumentHistoryLifecycle { + state, + remaining_revisions, + times: DocumentHistoryLifecycleTimes { + deleted_at_ms: 5_000, + erasing_started_at_ms: 6_000, + erasing_from_time_ms: 4_000, + erasing_from_revision: 3, + }, + }), + }; + let recovered = DocumentHistory::mock_deserialize(&sdk, &history.mock_serialize(&sdk)); + assert_eq!(recovered, history, "{state:?} did not round-trip"); + } + let without_lifecycle = DocumentHistory { + entries: vec![], + lifecycle: None, + }; + assert_eq!( + DocumentHistory::mock_deserialize(&sdk, &without_lifecycle.mock_serialize(&sdk)), + without_lifecycle + ); + } +} + impl MockResponse for Element { fn mock_serialize(&self, _sdk: &MockDashPlatformSdk) -> Vec { // Create a bincode configuration From e953db4541a43c6c4f2736b8a69fe62d540665a4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 13:37:37 +0700 Subject: [PATCH 29/48] test(drive-abci): reject tampered lifecycle claims for deleted and erasing histories The response-level verifier test tampered a current document's claim only. It now deletes the document and then starts its erasure, signs each new root, and checks that every lifecycle field altered on the wire is rejected by the proof. The delete and erase operation builders are public so the test can drive them. Co-Authored-By: Claude Fable 5.1 --- .../src/query/document_history/v0/tests.rs | 229 ++++++++++++++++++ .../mod.rs | 2 +- .../mod.rs | 2 +- 3 files changed, 231 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs index f723b97c80b..7c5175aeac9 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs @@ -577,3 +577,232 @@ fn should_reject_missing_selectors_before_reading_state() { .unwrap() .is_valid()); } + +/// A response that claims a lifecycle other than the one its proofs establish +/// is rejected field by field, for a deleted document and for one whose +/// erasure is under way, not only for a current one. +#[test] +fn should_reject_tampered_lifecycle_claims_for_deleted_and_erasing_histories() { + use dapi_grpc::platform::v0::get_document_history_request::get_document_history_request_v0::Selector; + use dapi_grpc::platform::v0::get_document_history_request::GetDocumentHistoryRequestV0; + use dapi_grpc::platform::v0::get_document_history_response::GetDocumentHistoryResponseV0; + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; + use drive_proof_verifier::types::DocumentHistoryState; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let mut state = state.as_ref().clone(); + let contract = json_document_to_contract( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json" + ), + false, + version, + ) + .unwrap(); + platform + .drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .unwrap(); + let document_type = contract.document_type_for_name("profile").unwrap(); + let owner = Identifier::from([8; 32]); + let mut document = json_document_to_document( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" + ), + Some(owner), + document_type, + version, + ) + .unwrap(); + // Enough revisions that one erase chunk leaves some behind. + let chunk = version + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the erase chunk") as u64; + for revision in 1..=chunk + 2 { + document.set_revision(Some(revision)); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .unwrap(); + } + let key = SecretKey::::from_hash(b"document-history-quorum"); + let provider = Provider { + contract: Arc::new(contract.clone()), + key: key.public_key().to_bytes().try_into().unwrap(), + }; + state.last_committed_block_info = Some( + dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { + basic_info: BlockInfo { + height: 42, + core_height: 12, + time_ms: 3000, + epoch: Default::default(), + }, + app_hash: [0; 32], + quorum_hash: [9; 32], + block_id_hash: [7; 32], + proposer_pro_tx_hash: [0; 32], + signature: [0; 96], + round: 0, + } + .into(), + ); + let apply = |batch| { + platform + .drive + .apply_batch_low_level_drive_operations(None, None, batch, &mut vec![], &version.drive) + .unwrap(); + }; + let wire_request = GetDocumentHistoryRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type_name: "profile".into(), + document_id: document.id().to_vec(), + limit: None, + prove: true, + selector: Some(Selector::StartAtMs(0)), + }; + let request: GetDocumentHistoryRequest = wire_request.clone().into(); + + // Delete, then start an erasure; after each, sign the new root and check + // that the honest response verifies while every altered claim does not. + let stages: [(&str, Box, DocumentHistoryState); 2] = [ + ( + "deleted", + Box::new(|| { + apply( + platform + .drive + .delete_document_for_contract_operations( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(5_000), + Some(owner), + None, + &mut None, + None, + version, + ) + .unwrap(), + ) + }), + DocumentHistoryState::Deleted, + ), + ( + "erasing", + Box::new(|| { + apply( + platform + .drive + .erase_document_for_contract_operations( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(6_000), + &mut None, + None, + version, + ) + .unwrap(), + ) + }), + DocumentHistoryState::Erasing, + ), + ]; + for (stage, advance, expected_state) in stages { + advance(); + let root = platform + .drive + .grove + .root_hash(None, &version.drive.grove_version) + .value + .unwrap(); + let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); + let signed = signed_proof( + vec![], + root, + &metadata, + &key, + platform.config.validator_set.quorum_type as u32, + ); + let committed = state.last_committed_block_info.as_mut().unwrap(); + committed.set_app_hash(root); + committed.set_signature(signed.signature.try_into().unwrap()); + + let response = platform + .query_document_history_v0(wire_request.clone(), &state, version) + .unwrap() + .into_data() + .unwrap(); + let verify = |response: GetDocumentHistoryResponseV0| { + DocumentHistory::maybe_from_proof( + request.clone(), + GetDocumentHistoryResponse::from(response), + Network::Testnet, + version, + &provider, + ) + }; + let honest = verify(response.clone()) + .unwrap_or_else(|error| panic!("{stage}: honest response must verify: {error}")) + .unwrap(); + let lifecycle = honest + .lifecycle + .expect("the history response always carries a lifecycle"); + assert_eq!(lifecycle.state, expected_state, "{stage}"); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000, "{stage}"); + + let tamperings: Vec<(&str, Box)> = vec![ + ( + "state", + Box::new(|r| r.lifecycle.as_mut().unwrap().state = 0), + ), + ( + "remaining_revisions", + Box::new(|r| r.lifecycle.as_mut().unwrap().remaining_revisions += 1), + ), + ( + "deleted_at_ms", + Box::new(|r| r.lifecycle.as_mut().unwrap().deleted_at_ms += 1), + ), + ( + "erasing_started_at_ms", + Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_started_at_ms += 1), + ), + ( + "erasing_from_time_ms", + Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_from_time_ms += 1), + ), + ( + "erasing_from_revision", + Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_from_revision += 1), + ), + ]; + for (field, tamper) in tamperings { + let mut tampered = response.clone(); + tamper(&mut tampered); + assert!( + verify(tampered).is_err(), + "{stage}: a response whose {field} differs from its proof must be rejected" + ); + } + } +} 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 536a288859d..4839e13afbd 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 @@ -34,7 +34,7 @@ impl Drive { /// * `Ok(Vec)` if the operation was successful. /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. #[allow(clippy::too_many_arguments)] - pub(crate) fn delete_document_for_contract_operations( + pub fn delete_document_for_contract_operations( &self, document_id: Identifier, contract: &DataContract, diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs index cdd88a782e5..6d41183175c 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs @@ -32,7 +32,7 @@ impl Drive { /// * `Ok(Vec)` if the operation was successful. /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. #[allow(clippy::too_many_arguments)] - pub(crate) fn erase_document_for_contract_operations( + pub fn erase_document_for_contract_operations( &self, document_id: Identifier, contract: &DataContract, From f08dee3d7cc892a43036ad1f07009f1bace729aa Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 15:26:43 +0700 Subject: [PATCH 30/48] fix(wasm-sdk): refuse JavaScript numbers past the safe range in history selectors The history query accepted a number for its exact u64 selectors, and JavaScript rounds any number above Number.MAX_SAFE_INTEGER before Rust sees it, so such a query would have been built and verified for a different timestamp or revision than the caller asked for. The five selector fields now deserialize through a type that accepts a bigint exactly and a number only while it is a non-negative safe integer, refusing fractions, negatives and anything already rounded; the TypeScript surface says so. Unit test over the accepted and refused inputs. Co-Authored-By: Claude Fable 5.1 --- packages/wasm-sdk/src/queries/document.rs | 115 +++++++++++++++++++--- 1 file changed, 104 insertions(+), 11 deletions(-) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 4fbbe155ad3..1b817755b86 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -176,7 +176,13 @@ export interface DocumentHistoryQuery { */ documentId: IdentifierLike - /** Inclusive lower time bound. Supply exactly one selector. */ + /** + * Inclusive lower time bound. Supply exactly one selector. + * + * Every selector is an exact u64: a `number` is accepted only up to + * `Number.MAX_SAFE_INTEGER`, and anything larger must be a `bigint`, since + * JavaScript would have rounded it before the query is built. + */ startAtMs?: bigint | number; /** Complete exclusive cursor returned by a previous page. */ startAfter?: { timeMs: bigint | number; revision: bigint | number }; @@ -254,18 +260,71 @@ struct DocumentHistoryQueryInput { data_contract_id: IdentifierWasm, document_type_name: String, document_id: IdentifierWasm, - start_at_ms: Option, + start_at_ms: Option, start_after: Option, - start_at_revision: Option, - revision: Option, + start_at_revision: Option, + revision: Option, limit: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct DocumentHistoryCursorInput { - time_ms: u64, - revision: u64, + time_ms: ExactU64, + revision: ExactU64, +} + +/// A `u64` selector that a JavaScript caller may pass as a `bigint` or as a +/// `number`, where a `number` is accepted only while it is a safe integer. +/// +/// JavaScript has already rounded any `number` past `Number.MAX_SAFE_INTEGER` +/// by the time it reaches Rust, so silently converting it would query and +/// verify a different timestamp or revision than the caller asked for. A +/// `bigint` carries the exact value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ExactU64(u64); + +impl<'de> serde::Deserialize<'de> for ExactU64 { + fn deserialize>(deserializer: D) -> Result { + struct Visitor; + + impl serde::de::Visitor<'_> for Visitor { + type Value = ExactU64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a non-negative integer: a bigint, or a number no larger than Number.MAX_SAFE_INTEGER") + } + + fn visit_u64(self, value: u64) -> Result { + Ok(ExactU64(value)) + } + + fn visit_i64(self, value: i64) -> Result { + u64::try_from(value) + .map(ExactU64) + .map_err(|_| E::custom("history selectors cannot be negative")) + } + + fn visit_f64(self, value: f64) -> Result { + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; + if value.fract() != 0.0 || !(0.0..=MAX_SAFE_INTEGER).contains(&value) { + return Err(E::custom( + "history selectors must be non-negative integers; pass a bigint for \ + values past Number.MAX_SAFE_INTEGER", + )); + } + Ok(ExactU64(value as u64)) + } + } + + deserializer.deserialize_any(Visitor) + } +} + +impl From for u64 { + fn from(value: ExactU64) -> Self { + value.0 + } } fn parse_document_history_query( @@ -285,16 +344,16 @@ fn parse_document_history_query( )); } let filter = if let Some(time) = input.start_at_ms { - DocumentHistoryFilter::StartAtTime(time) + DocumentHistoryFilter::StartAtTime(time.into()) } else if let Some(cursor) = input.start_after { DocumentHistoryFilter::StartAfter { - time_ms: cursor.time_ms, - revision: cursor.revision, + time_ms: cursor.time_ms.into(), + revision: cursor.revision.into(), } } else if let Some(revision) = input.start_at_revision { - DocumentHistoryFilter::StartAtRevision(revision) + DocumentHistoryFilter::StartAtRevision(revision.into()) } else { - DocumentHistoryFilter::Revision(input.revision.expect("one filter was checked")) + DocumentHistoryFilter::Revision(input.revision.expect("one filter was checked").into()) }; Ok(DocumentHistoryQuery { data_contract_id: input.data_contract_id.into(), @@ -1799,3 +1858,37 @@ mod history_wasm_tests { ); } } + +#[cfg(test)] +mod exact_u64_tests { + use super::ExactU64; + + /// A safe integer passes whether it arrives as an integer or as a whole + /// floating-point number; anything a JavaScript `number` has already + /// rounded, and anything fractional or negative, is refused. + #[test] + fn should_accept_only_exact_non_negative_integers() { + assert_eq!( + serde_json::from_str::("12").unwrap(), + ExactU64(12) + ); + assert_eq!( + serde_json::from_str::("9007199254740991").unwrap(), + ExactU64(9_007_199_254_740_991) + ); + assert_eq!( + serde_json::from_str::("18446744073709551615").unwrap(), + ExactU64(u64::MAX) + ); + assert_eq!( + serde_json::from_str::("12.0").unwrap(), + ExactU64(12) + ); + for rejected in ["12.5", "-1", "-1.0", "9007199254740992.0", "1e300"] { + assert!( + serde_json::from_str::(rejected).is_err(), + "{rejected} must be refused" + ); + } + } +} From a7660a742d7917fd92e16aeb097649387c190513 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 12 Sep 2026 15:26:43 +0700 Subject: [PATCH 31/48] test(sdk): drop needless borrows in the history mock round-trip test Clippy in CI rejected the double reference on the mock SDK handle. Co-Authored-By: Claude Fable 5.1 --- packages/rs-sdk/src/mock/requests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 806a875ad05..e8f8955bf0f 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -384,7 +384,7 @@ mod document_history_mock_tests { }, }), }; - let recovered = DocumentHistory::mock_deserialize(&sdk, &history.mock_serialize(&sdk)); + let recovered = DocumentHistory::mock_deserialize(sdk, &history.mock_serialize(sdk)); assert_eq!(recovered, history, "{state:?} did not round-trip"); } let without_lifecycle = DocumentHistory { @@ -392,7 +392,7 @@ mod document_history_mock_tests { lifecycle: None, }; assert_eq!( - DocumentHistory::mock_deserialize(&sdk, &without_lifecycle.mock_serialize(&sdk)), + DocumentHistory::mock_deserialize(sdk, &without_lifecycle.mock_serialize(sdk)), without_lifecycle ); } From ea6dcf8a6b7b4794eadf13693659a683b2fff258 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 13 Sep 2026 11:24:28 +0700 Subject: [PATCH 32/48] fix(sdk): sign an erase with its settings and refuse unsupported versions before the nonce The erase builder signed with a zero fee increase and default creation options unless a caller set them on the builder itself, so the wasm and FFI wrappers, which hand over put settings only, dropped a requested userFeeIncrease before signing, where broadcasting cannot repair it. The signing step now falls back to the values carried by the settings. It also reserved the identity's contract nonce before the transition constructor rejected a platform version without erase transitions, so each refused call under protocol 12 or 13 advanced the cached nonce for a transition that was never broadcast. The version gate now runs before the nonce fetch, together with a check of the requested batch feature version. Co-Authored-By: Claude Fable 5.1 --- .../platform/documents/transitions/erase.rs | 138 +++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index 4d09c549476..6dafdfa83ef 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -14,6 +14,7 @@ use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; use dpp::version::PlatformVersion; +use dpp::ProtocolError; use drive::drive::document::history::{ DocumentHistoryLifecycle, DocumentHistorySelector, DocumentHistoryState, }; @@ -102,6 +103,8 @@ impl DocumentEraseTransitionBuilder { .data_contract .document_type_for_name(&self.document_type_name) .map_err(|e| Error::Protocol(e.into()))?; + let (user_fee_increase, creation_options) = self.signing_parameters(); + Self::check_erase_is_constructible(creation_options.as_ref(), platform_version)?; // The transition carries only the base, so an id is all the builder // needs; the values of a document that is no longer visible are not @@ -138,15 +141,71 @@ impl DocumentEraseTransitionBuilder { document_type, identity_public_key, identity_contract_nonce, - self.user_fee_increase.unwrap_or_default(), + user_fee_increase, signer, platform_version, - self.state_transition_creation_options, + creation_options, ) .await?; Ok(state_transition) } + + /// The fee increase and creation options the transition is signed with. + /// + /// An explicitly set value wins; otherwise the ones carried by the put + /// settings apply, so a caller that only hands over settings (the wasm and + /// FFI wrappers do) still signs with what it asked for. + fn signing_parameters(&self) -> (UserFeeIncrease, Option) { + let settings = self.settings.as_ref(); + ( + self.user_fee_increase + .or(settings.and_then(|settings| settings.user_fee_increase)) + .unwrap_or_default(), + self.state_transition_creation_options + .or(settings.and_then(|settings| settings.state_transition_creation_options)), + ) + } + + /// Refuses, before any nonce is reserved, an erase the platform version + /// cannot construct: the transition kind joined the wire at protocol + /// version 14, and the batch may only be built at a version it knows. + /// + /// The same rejection happens inside the transition constructor, but by + /// then the SDK has already advanced its cached contract nonce for a + /// transition that is never broadcast. + fn check_erase_is_constructible( + creation_options: Option<&StateTransitionCreationOptions>, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let serialization = &platform_version.dpp.state_transition_serialization_versions; + let Some(erase_bounds) = serialization.document_erase_state_transition.as_ref() else { + return Err(Error::Protocol(ProtocolError::Generic( + "erase transitions do not exist at this platform version".to_string(), + ))); + }; + let batch_feature_version = creation_options + .and_then(|options| options.batch_feature_version) + .unwrap_or(serialization.batch_state_transition.default_current_version); + if !matches!(batch_feature_version, 0 | 1) { + return Err(Error::Protocol(ProtocolError::UnknownVersionMismatch { + method: "DocumentEraseTransitionBuilder::sign".to_string(), + known_versions: vec![0, 1], + received: batch_feature_version, + })); + } + let method_feature_version = creation_options + .and_then(|options| options.method_feature_version) + .unwrap_or(erase_bounds.bounds.default_current_version); + if method_feature_version != 0 { + return Err(Error::Protocol(ProtocolError::UnknownVersionMismatch { + method: "DocumentEraseTransitionBuilder::sign".to_string(), + known_versions: vec![0], + received: method_feature_version, + })); + } + Ok(()) + } } /// What one erase transition left behind. @@ -293,4 +352,79 @@ mod tests { erase_observation(StateTransitionProofResult::VerifiedTokenBalanceAbsence(id)) .expect_err("only a document result can describe an erase"); } + + #[cfg(feature = "mocks")] + fn builder_with(settings: Option) -> DocumentEraseTransitionBuilder { + let contract = dpp::tests::fixtures::get_dashpay_contract_fixture( + None, + 0, + PlatformVersion::latest().protocol_version, + ) + .data_contract_owned(); + let builder = DocumentEraseTransitionBuilder::new( + Arc::new(contract), + "profile".to_string(), + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + ); + match settings { + Some(settings) => builder.with_settings(settings), + None => builder, + } + } + + /// A fee increase or creation options handed over inside the put settings + /// reach the signing step, and an explicit builder value still wins. + #[cfg(feature = "mocks")] + #[test] + fn should_sign_with_the_settings_fee_increase_unless_set_explicitly() { + let options = StateTransitionCreationOptions { + batch_feature_version: Some(1), + ..Default::default() + }; + let settings = PutSettings { + user_fee_increase: Some(250), + state_transition_creation_options: Some(options), + ..Default::default() + }; + assert_eq!(builder_with(None).signing_parameters(), (0, None)); + assert_eq!( + builder_with(Some(settings)).signing_parameters(), + (250, Some(options)) + ); + assert_eq!( + builder_with(Some(settings)) + .with_user_fee_increase(7) + .with_state_transition_creation_options(Default::default()) + .signing_parameters(), + (7, Some(Default::default())) + ); + } + + /// The version gate runs before the nonce fetch, so a platform version + /// that cannot construct an erase is refused without reserving a nonce. + #[test] + fn should_refuse_an_erase_the_platform_version_cannot_construct() { + let too_old = PlatformVersion::get(13).unwrap(); + let error = DocumentEraseTransitionBuilder::check_erase_is_constructible(None, too_old) + .expect_err("protocol 13 has no erase transition"); + assert!(error + .to_string() + .contains("erase transitions do not exist at this platform version")); + let current = PlatformVersion::get(14).unwrap(); + DocumentEraseTransitionBuilder::check_erase_is_constructible(None, current) + .expect("protocol 14 constructs erases"); + let unknown_batch = StateTransitionCreationOptions { + batch_feature_version: Some(9), + ..Default::default() + }; + DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&unknown_batch), current) + .expect_err("an unknown batch version is refused before any nonce is reserved"); + let unknown_method = StateTransitionCreationOptions { + method_feature_version: Some(1), + ..Default::default() + }; + DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&unknown_method), current) + .expect_err("an unknown erase method version is refused before any nonce is reserved"); + } } From 3cb369c283465abc9497a00f9089729be96d04bb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 18 Sep 2026 20:10:19 +0700 Subject: [PATCH 33/48] refactor(platform)!: land the document lifecycle as protocol 14 generations The delete and erase lifecycle was written against an earlier shape of the keep-history storage branch and edited generations that released protocol versions select. Every behaviour it adds now lives in a generation that only protocol 14's tables reach, and every module a released protocol selects is byte-identical to the storage branch again. Wire and action shape: - Batch transition format 2 (`BatchTransitionV2`) carries a nested shell (`BatchedTransitionV1` / `DocumentTransitionV1`) that repeats the shipped document kinds and adds `Erase`. The shipped `DocumentTransition` and `BatchedTransition` enums are unchanged, so formats 0 and 1 cannot decode an erase: software that only knows them keeps rejecting one at decode time. Protocol 14 raises the batch bounds to format 2 and makes it the default. - Two accessor views: the shipped `DocumentsBatchTransitionAccessorsV0` reports a format 2 batch as empty, which fails closed at basic-structure validation; `DocumentsBatchTransitionAccessorsV1` sees every format and is what every unversioned consumer and every new generation uses (outer transition naming and key-purpose checks, constructor traits, estimated fees, SDK, wasm-sdk, wasm-dpp2, the wallet FFI parser). - Drive gains batch action format 1 (`BatchTransitionActionV1`, items wrap a format 0 action or an erase action) behind a new `StateTransitionAction::BatchActionV1` arm, so the format 0 action and the validators that consume its typed accessors stay frozen. Generations selected only by protocol 14: DPP basic structure v1 (owns its duplicate finder over the format-aware view), Drive delete wrappers v1, erase operations and estimation v0, action conversion delete v1 / erase v0 / batch converter v1 (emits operations in batch order), state transition prover v1 and verifier v1, ABCI transform v2 running transformer v1, advanced structure v1, state v1, identity contract nonce v1, is-allowed v1, replaced document fetch v2, document-delete state v1, erase structure and state v0, and history handler v1 (lifecycle states and times). Released tables gain dormant `None`/backfilled slots only; the new slots for the lifecycle read, erase operations, estimation and erase conversion are optional and `None` through protocol 13. Behaviour fixes found while adapting, each pinned red then green: - an authenticated history tree that is present but empty is corrupt, not an absent document (`should_authenticate_history_pages_metadata_and_absence`: FAIL before, PASS after); - an erase prices two structural refund recipients, the lifecycle record and the history subtree; the current pointer was already refunded by the delete (`should_price_the_refund_recipients_an_erase_can_credit`: FAIL before, PASS after); - the SDK erase builder refuses a protocol version or an explicit batch or erase generation it cannot construct before it fetches a nonce (`should_refuse_an_erase_the_platform_version_cannot_construct`: FAIL before, PASS after); - wasm-sdk documents the history lifecycle as optional on the legacy layout and the erase result as an affected-state observation. New regressions: a relabelled format 2 batch shows format 1 decodes every shipped kind and refuses an erase; format 2 round-trips through bytes, JSON and values; the format-aware view sees every format while the shipped view reports format 2 as empty; downcasting an erase to the shipped shells fails; basic structure v1 admits an erase in format 2 and bounds its nonce; batch converter v1 preserves batch order and refuses a protocol whose converter only knows format 0; wasm-dpp2 picks format 2 only when an item needs it. Tests that pinned the default batch format or matched the format 0 action now go through the format-aware view or accept both action formats. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/serialization/json/safe_fields.rs | 8 + packages/rs-dpp/src/state_transition/mod.rs | 69 +- .../batch_transition/accessors/mod.rs | 24 + .../document/batch_transition/accessors/v1.rs | 232 + .../from_document.rs | 4 +- .../document_erase_transition/mod.rs | 4 +- .../v0/from_document.rs | 2 +- .../document_erase_transition/v0/mod.rs | 4 +- .../batched_transition/document_transition.rs | 32 +- .../document_transition_action_type.rs | 27 +- .../batched_transition/mod.rs | 5 + .../batch_transition/batched_transition/v1.rs | 944 +++ .../batch_transition/identity_signed.rs | 4 + .../document/batch_transition/methods/mod.rs | 496 +- .../batch_transition/methods/v0/mod.rs | 48 +- .../batch_transition/methods/v1/mod.rs | 6 +- .../batch_transition/methods/v2/mod.rs | 35 + .../document/batch_transition/mod.rs | 4 + ...ate_transition_estimated_fee_validation.rs | 4 +- .../batch_transition/state_transition_like.rs | 10 + .../document/batch_transition/tests.rs | 166 +- .../batch_transition/v0/v0_methods.rs | 51 +- .../batch_transition/v1/v0_methods.rs | 51 +- .../batch_transition/v2/identity_signed.rs | 42 + .../document/batch_transition/v2/mod.rs | 38 + .../v2/state_transition_like.rs | 100 + .../document/batch_transition/v2/types.rs | 18 + .../batch_transition/v2/v0_methods.rs | 459 ++ .../batch_transition/v2/v1_methods.rs | 929 +++ .../document/batch_transition/v2/version.rs | 9 + .../validate_basic_structure/mod.rs | 4 +- .../validate_basic_structure/v0/mod.rs | 84 +- .../validate_basic_structure/v1/mod.rs | 578 ++ .../document/batch_transition/version.rs | 1 + .../execution/types/execution_event/mod.rs | 21 + .../state_v2/mod.rs | 5 +- .../state_v1/mod.rs | 2 +- .../document_erase_transition_action/mod.rs | 16 +- .../state_v0/mod.rs | 2 +- .../document_reference_validation/v0/mod.rs | 12 +- .../batch/advanced_structure/mod.rs | 1 + .../batch/advanced_structure/v0/mod.rs | 15 - .../batch/advanced_structure/v1/mod.rs | 284 + .../batch/identity_contract_nonce/mod.rs | 1 + .../batch/identity_contract_nonce/v1/mod.rs | 67 + .../state_transitions/batch/is_allowed/mod.rs | 4 +- .../batch/is_allowed/v1/mod.rs | 53 + .../state_transitions/batch/mod.rs | 68 +- .../mod.rs | 56 + .../v0/mod.rs | 38 + .../state_transitions/batch/state/mod.rs | 3 + .../batch/state/v0/fetch_documents.rs | 123 +- .../state_transitions/batch/state/v0/mod.rs | 10 - .../state_transitions/batch/state/v2/mod.rs | 47 + .../batch/state/validation_v1/mod.rs | 373 + .../batch/tests/document/creation.rs | 2 +- .../batch/tests/document/erase.rs | 78 +- .../distribution/perpetual/block_based.rs | 19 +- .../batch/tests/token/transfer/mod.rs | 8 +- .../batch/transformer/mod.rs | 1 + .../batch/transformer/v0/mod.rs | 11 - .../batch/transformer/v1/mod.rs | 1162 +++ .../src/query/document_history/mod.rs | 24 +- .../src/query/document_history/v0/mod.rs | 10 +- .../src/query/document_history/v0/tests.rs | 231 +- .../src/query/document_history/v1/mod.rs | 163 + .../src/query/document_history/v1/tests.rs | 683 ++ .../test_cases/upgrade_fork_tests.rs | 2 +- .../verify_state_transitions.rs | 53 +- .../v0/tests/batched_group_drain.rs | 1 - .../contract/update/update_keywords/v0/mod.rs | 2 - .../delete_document_for_contract/mod.rs | 13 +- .../delete_document_for_contract/v0/mod.rs | 4 - .../delete_document_for_contract/v1/mod.rs | 60 + .../mod.rs | 20 +- .../v0/mod.rs | 5 - .../v1/mod.rs | 63 + .../delete_document_for_contract_id/mod.rs | 13 +- .../delete_document_for_contract_id/v0/mod.rs | 4 - .../delete_document_for_contract_id/v1/mod.rs | 80 + .../mod.rs | 23 +- .../v0/mod.rs | 9 +- .../v1/mod.rs | 66 + .../mod.rs | 112 +- .../v0/mod.rs | 7 - .../v1/mod.rs | 4 +- .../mod.rs | 21 +- .../v0/mod.rs | 5 - .../v1/mod.rs | 53 + .../mod.rs | 8 +- .../rs-drive/src/drive/document/delete/mod.rs | 3 +- .../mod.rs | 8 +- .../src/drive/document/lifecycle/fetch/mod.rs | 8 +- .../document/lifecycle/refund_recipients.rs | 7 +- .../src/drive/document/lifecycle/tests.rs | 268 +- .../src/prove/prove_state_transition/mod.rs | 4 +- .../prove/prove_state_transition/v1/mod.rs | 555 ++ .../src/query/document_history_drive_query.rs | 10 +- packages/rs-drive/src/query/filter.rs | 4 - .../batch/batch_transition_v1.rs | 138 + .../document/document_delete_transition.rs | 52 +- .../document/document_erase_transition.rs | 9 +- .../batch/document/document_transition.rs | 7 - .../action_convert_to_operations/batch/mod.rs | 1 + .../action_convert_to_operations/mod.rs | 3 + .../transformer.rs | 4 +- .../v0/transformer.rs | 17 +- .../document_transition_action_type.rs | 1 - .../document_transition/mod.rs | 7 - .../src/state_transition_action/batch/mod.rs | 2 + .../state_transition_action/batch/v1/mod.rs | 272 + .../src/state_transition_action/mod.rs | 6 + .../transformer.rs | 50 +- .../src/util/batch/drive_op_batch/document.rs | 52 +- .../document/verify_document_history/tests.rs | 40 +- .../mod.rs | 10 +- .../v0/mod.rs | 187 +- .../v1/mod.rs | 6409 +++++++++++++++++ .../tests/drive_storage_ops_coverage.rs | 1 - .../v3.rs | 4 +- .../dpp_state_transition_versions/mod.rs | 1 + .../dpp_state_transition_versions/v4.rs | 21 + .../drive_abci_query_versions/mod.rs | 2 + .../drive_abci_query_versions/v0.rs | 1 + .../drive_abci_query_versions/v1.rs | 1 + .../drive_abci_query_versions/v3.rs | 1 + .../drive_abci_validation_versions/mod.rs | 15 +- .../drive_abci_validation_versions/v1.rs | 6 +- .../drive_abci_validation_versions/v10.rs | 22 +- .../drive_abci_validation_versions/v2.rs | 6 +- .../drive_abci_validation_versions/v3.rs | 6 +- .../drive_abci_validation_versions/v4.rs | 6 +- .../drive_abci_validation_versions/v5.rs | 6 +- .../drive_abci_validation_versions/v6.rs | 6 +- .../drive_abci_validation_versions/v7.rs | 6 +- .../drive_abci_validation_versions/v8.rs | 6 +- .../drive_abci_validation_versions/v9.rs | 6 +- .../drive_document_method_versions/mod.rs | 14 +- .../drive_document_method_versions/v1.rs | 6 +- .../drive_document_method_versions/v2.rs | 6 +- .../drive_document_method_versions/v3.rs | 6 +- .../drive_document_method_versions/v4.rs | 16 +- .../mod.rs | 7 +- .../v1.rs | 2 +- .../v2.rs | 2 +- .../v3.rs | 2 +- .../v4.rs | 8 +- .../drive_verify_method_versions/v3.rs | 8 +- .../src/version/drive_versions/v9.rs | 4 +- .../src/version/mocks/v2_test.rs | 1 + .../rs-platform-version/src/version/v14.rs | 4 +- .../tests/keep_history_delete_versions.rs | 168 +- .../src/parse_state_transition.rs | 10 +- .../platform/documents/transitions/erase.rs | 100 +- .../document_transition/mod.rs | 3 - .../base/state_transition.rs | 17 +- .../batch/batch_transition.rs | 66 +- .../batch/batched_transition.rs | 45 +- .../batch/document_transition.rs | 28 +- .../batch/document_transitions/create.rs | 4 +- .../batch/document_transitions/delete.rs | 4 +- .../batch/document_transitions/purchase.rs | 4 +- .../batch/document_transitions/replace.rs | 4 +- .../batch/document_transitions/transfer.rs | 4 +- .../document_transitions/update_price.rs | 4 +- packages/wasm-sdk/src/queries/document.rs | 13 +- .../src/state_transitions/broadcast.rs | 4 +- .../src/state_transitions/document.rs | 5 +- 168 files changed, 16166 insertions(+), 1373 deletions(-) create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/query/document_history/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/query/document_history/v1/tests.rs create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs create mode 100644 packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/v1/mod.rs create mode 100644 packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index 4b3c27a89f7..1816204b8d2 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -137,6 +137,14 @@ impl JsonSafeFields for crate::state_transition::batch_transition::batched_transition::BatchedTransition { } +impl JsonSafeFields + for crate::state_transition::batch_transition::batched_transition::DocumentTransitionV1 +{ +} +impl JsonSafeFields + for crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1 +{ +} impl JsonSafeFields for crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice {} impl JsonSafeFields for crate::group::action_event::GroupActionEvent {} // TokenEvent contains u64 aliases (TokenAmount, Credits) in tuple variants that diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index cff847f8ecf..f08f7f82c91 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1,7 +1,6 @@ use derive_more::From; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; -use state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; use std::collections::BTreeMap; use std::ops::RangeInclusive; @@ -85,8 +84,10 @@ use crate::state_transition::address_funding_from_asset_lock_transition::{ use crate::state_transition::address_funds_transfer_transition::{ AddressFundsTransferTransition, AddressFundsTransferTransitionSignable, }; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; -use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransitionRefV1, DocumentTransitionRefV1, +}; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionSignable}; @@ -896,6 +897,7 @@ impl StateTransition { StateTransition::Batch(batch_transition) => match batch_transition { BatchTransition::V0(_) => ALL_VERSIONS, BatchTransition::V1(_) => 9..=LATEST_VERSION, + BatchTransition::V2(_) => 14..=LATEST_VERSION, }, StateTransition::IdentityCreate(_) | StateTransition::IdentityTopUp(_) @@ -973,47 +975,55 @@ impl StateTransition { Self::DataContractUpdate(_) => "DataContractUpdate".to_string(), Self::Batch(batch_transition) => { let mut document_transition_types = vec![]; - for transition in batch_transition.transitions_iter() { + for transition in batch_transition.transitions_iter_v1() { let document_transition_name = match transition { - BatchedTransitionRef::Document(DocumentTransition::Create(_)) => "Create", - BatchedTransitionRef::Document(DocumentTransition::Replace(_)) => "Replace", - BatchedTransitionRef::Document(DocumentTransition::Delete(_)) => "Delete", - BatchedTransitionRef::Document(DocumentTransition::Transfer(_)) => { - "Transfer" + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Create(_)) => { + "Create" + } + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Replace(_)) => { + "Replace" } - BatchedTransitionRef::Document(DocumentTransition::UpdatePrice(_)) => { - "UpdatePrice" + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Delete(_)) => { + "Delete" + } + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Transfer(_)) => { + "Transfer" } - BatchedTransitionRef::Document(DocumentTransition::Purchase(_)) => { + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::UpdatePrice( + _, + )) => "UpdatePrice", + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Purchase(_)) => { "Purchase" } - BatchedTransitionRef::Document(DocumentTransition::IndexOnlyDelete(_)) => { - "IndexOnlyDelete" + BatchedTransitionRefV1::Document( + DocumentTransitionRefV1::IndexOnlyDelete(_), + ) => "IndexOnlyDelete", + BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Erase(_)) => { + "Erase" } - BatchedTransitionRef::Document(DocumentTransition::Erase(_)) => "Erase", - BatchedTransitionRef::Token(TokenTransition::Transfer(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::Transfer(_)) => { "TokenTransfer" } - BatchedTransitionRef::Token(TokenTransition::Mint(_)) => "TokenMint", - BatchedTransitionRef::Token(TokenTransition::Burn(_)) => "TokenBurn", - BatchedTransitionRef::Token(TokenTransition::Freeze(_)) => "TokenFreeze", - BatchedTransitionRef::Token(TokenTransition::Unfreeze(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::Mint(_)) => "TokenMint", + BatchedTransitionRefV1::Token(TokenTransition::Burn(_)) => "TokenBurn", + BatchedTransitionRefV1::Token(TokenTransition::Freeze(_)) => "TokenFreeze", + BatchedTransitionRefV1::Token(TokenTransition::Unfreeze(_)) => { "TokenUnfreeze" } - BatchedTransitionRef::Token(TokenTransition::DestroyFrozenFunds(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::DestroyFrozenFunds(_)) => { "TokenDestroyFrozenFunds" } - BatchedTransitionRef::Token(TokenTransition::EmergencyAction(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::EmergencyAction(_)) => { "TokenEmergencyAction" } - BatchedTransitionRef::Token(TokenTransition::ConfigUpdate(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::ConfigUpdate(_)) => { "TokenConfigUpdate" } - BatchedTransitionRef::Token(TokenTransition::Claim(_)) => "TokenClaim", - BatchedTransitionRef::Token(TokenTransition::DirectPurchase(_)) => { + BatchedTransitionRefV1::Token(TokenTransition::Claim(_)) => "TokenClaim", + BatchedTransitionRefV1::Token(TokenTransition::DirectPurchase(_)) => { "TokenDirectPurchase" } - BatchedTransitionRef::Token( + BatchedTransitionRefV1::Token( TokenTransition::SetPriceForDirectPurchase(_), ) => "SetPriceForDirectPurchase", }; @@ -1389,14 +1399,14 @@ impl StateTransition { st.verify_public_key_is_enabled(identity_public_key)?; } StateTransition::Batch(st) => { - let allow_token_transfer_keys = st.transitions_len() == 1 + let allow_token_transfer_keys = st.transitions_len_v1() == 1 && (st - .first_transition() + .first_transition_v1() .expect("expected first transition with len 1") .as_transition_token_claim() .is_some() || st - .first_transition() + .first_transition_v1() .expect("expected first transition with len 1") .as_transition_token_transfer() .is_some()); @@ -2055,6 +2065,7 @@ impl StateTransitionStructureValidation for StateTransition { #[cfg(test)] mod tests { use super::*; + use crate::state_transition::batch_transition::batched_transition::DocumentTransition; // ----------------------------------------------------------------------- // StateTransitionSigningOptions tests diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs index f18d109bb24..a2ac1110b4e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs @@ -1,15 +1,21 @@ mod v0; +mod v1; +use std::iter::Empty; use std::slice::Iter; use crate::state_transition::batch_transition::batched_transition::{BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef}; use crate::state_transition::batch_transition::BatchTransition; pub use v0::*; +pub use v1::*; use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; /// Iterator enum for `BatchTransition` that can handle both V0 and V1. pub enum DocumentBatchIterator<'a> { V0(Iter<'a, DocumentTransition>), V1(DocumentBatchV1Iterator<'a>), + /// Batch format 2 has no view through the format 0 and 1 accessors; see + /// the `DocumentsBatchTransitionAccessorsV0` implementation below. + V2(Empty>), } /// Iterator for version 1, yielding `BatchedTransitionRef<'a>` items. @@ -37,10 +43,21 @@ impl<'a> Iterator for DocumentBatchIterator<'a> { match self { DocumentBatchIterator::V0(iter) => iter.next().map(BatchedTransitionRef::Document), DocumentBatchIterator::V1(iter) => iter.next(), + DocumentBatchIterator::V2(iter) => iter.next(), } } } +/// The view of a batch through the transition shells of batch formats 0 and 1. +/// +/// Batch format 2 carries a document transition shell with an erase kind that +/// [`BatchedTransitionRef`] cannot represent, so a format 2 batch has no view +/// here: every accessor reports an empty batch for it. Code that must see every +/// format uses [`DocumentsBatchTransitionAccessorsV1`]. The generations that +/// still read this view are the ones selected for protocol versions that reject +/// a format 2 batch by its wire version before any of them runs, and the first +/// of them rejects an empty batch, so the empty view fails closed rather than +/// silently dropping transitions. impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { type IterType<'a> = DocumentBatchIterator<'a> @@ -54,6 +71,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { BatchTransition::V1(v1) => DocumentBatchIterator::V1(DocumentBatchV1Iterator { inner: v1.transitions.iter(), }), + BatchTransition::V2(_) => DocumentBatchIterator::V2(std::iter::empty()), } } @@ -61,6 +79,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.transitions.len(), BatchTransition::V1(v1) => v1.transitions.len(), + BatchTransition::V2(_) => 0, } } @@ -68,6 +87,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.transitions.is_empty(), BatchTransition::V1(v1) => v1.transitions.is_empty(), + BatchTransition::V2(_) => true, } } @@ -78,6 +98,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .first() .map(|batch_transition| batch_transition.borrow_as_ref()), + BatchTransition::V2(_) => None, } } @@ -91,6 +112,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .first_mut() .map(|batch_transition| batch_transition.borrow_as_mut()), + BatchTransition::V2(_) => None, } } @@ -101,6 +123,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .iter() .any(|transition| matches!(transition, BatchedTransition::Document(_))), + BatchTransition::V2(_) => false, } } @@ -111,6 +134,7 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .iter() .any(|transition| matches!(transition, BatchedTransition::Token(_))), + BatchTransition::V2(_) => false, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs new file mode 100644 index 00000000000..55f2af5d0a9 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs @@ -0,0 +1,232 @@ +use std::slice::Iter; + +use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionMutRefV1, BatchedTransitionRefV1, BatchedTransitionV1, + DocumentTransition, +}; +use crate::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, +}; + +/// The view of a batch that sees every batch format. +/// +/// Items are borrowed through the transition shells introduced with batch +/// format 2, which know every kind the older formats carry plus the erase +/// kind. Formats 0 and 1 are viewed through a lossless conversion of their own +/// shells. +pub trait DocumentsBatchTransitionAccessorsV1 { + /// Associated type for the iterator. + type IterType<'a>: Iterator> + where + Self: 'a; + + /// Returns an iterator over the `BatchedTransitionRefV1` items. + fn transitions_iter_v1(&self) -> Self::IterType<'_>; + + fn transitions_len_v1(&self) -> usize; + fn transitions_are_empty_v1(&self) -> bool; + + fn first_transition_v1(&self) -> Option>; + + fn first_transition_mut_v1(&mut self) -> Option>; + fn contains_document_transition_v1(&self) -> bool; + fn contains_token_transition_v1(&self) -> bool; +} + +/// Iterator over the items of any batch format, yielding +/// `BatchedTransitionRefV1<'a>` items. +pub enum DocumentBatchIteratorV1<'a> { + V0(Iter<'a, DocumentTransition>), + V1(Iter<'a, BatchedTransition>), + V2(Iter<'a, BatchedTransitionV1>), +} + +impl<'a> Iterator for DocumentBatchIteratorV1<'a> { + type Item = BatchedTransitionRefV1<'a>; + + fn next(&mut self) -> Option { + match self { + Self::V0(iter) => iter + .next() + .map(|transition| BatchedTransitionRefV1::Document(transition.into())), + Self::V1(iter) => iter.next().map(Into::into), + Self::V2(iter) => iter.next().map(BatchedTransitionV1::borrow_as_ref), + } + } +} + +impl DocumentsBatchTransitionAccessorsV1 for BatchTransition { + type IterType<'a> + = DocumentBatchIteratorV1<'a> + where + Self: 'a; + + fn transitions_iter_v1(&self) -> Self::IterType<'_> { + match self { + Self::V0(transition) => DocumentBatchIteratorV1::V0(transition.transitions.iter()), + Self::V1(transition) => DocumentBatchIteratorV1::V1(transition.transitions.iter()), + Self::V2(transition) => DocumentBatchIteratorV1::V2(transition.transitions.iter()), + } + } + + fn transitions_len_v1(&self) -> usize { + match self { + Self::V0(transition) => transition.transitions.len(), + Self::V1(transition) => transition.transitions.len(), + Self::V2(transition) => transition.transitions.len(), + } + } + + fn transitions_are_empty_v1(&self) -> bool { + self.transitions_len_v1() == 0 + } + + fn first_transition_v1(&self) -> Option> { + self.transitions_iter_v1().next() + } + + fn first_transition_mut_v1(&mut self) -> Option> { + match self { + Self::V0(transition) => transition + .transitions + .first_mut() + .map(|transition| BatchedTransitionMutRefV1::Document(transition.into())), + Self::V1(transition) => transition.transitions.first_mut().map(Into::into), + Self::V2(transition) => transition + .transitions + .first_mut() + .map(BatchedTransitionV1::borrow_as_mut), + } + } + + fn contains_document_transition_v1(&self) -> bool { + self.transitions_iter_v1() + .any(|transition| matches!(transition, BatchedTransitionRefV1::Document(_))) + } + + fn contains_token_transition_v1(&self) -> bool { + self.transitions_iter_v1() + .any(|transition| matches!(transition, BatchedTransitionRefV1::Token(_))) + } +} + +impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV2 { + type IterType<'a> + = DocumentBatchIteratorV1<'a> + where + Self: 'a; + + fn transitions_iter_v1(&self) -> Self::IterType<'_> { + DocumentBatchIteratorV1::V2(self.transitions.iter()) + } + + fn transitions_len_v1(&self) -> usize { + self.transitions.len() + } + + fn transitions_are_empty_v1(&self) -> bool { + self.transitions.is_empty() + } + + fn first_transition_v1(&self) -> Option> { + self.transitions + .first() + .map(BatchedTransitionV1::borrow_as_ref) + } + + fn first_transition_mut_v1(&mut self) -> Option> { + self.transitions + .first_mut() + .map(BatchedTransitionV1::borrow_as_mut) + } + + fn contains_document_transition_v1(&self) -> bool { + self.transitions + .iter() + .any(|transition| matches!(transition, BatchedTransitionV1::Document(_))) + } + + fn contains_token_transition_v1(&self) -> bool { + self.transitions + .iter() + .any(|transition| matches!(transition, BatchedTransitionV1::Token(_))) + } +} + +impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV0 { + type IterType<'a> + = DocumentBatchIteratorV1<'a> + where + Self: 'a; + + fn transitions_iter_v1(&self) -> Self::IterType<'_> { + DocumentBatchIteratorV1::V0(self.transitions.iter()) + } + + fn transitions_len_v1(&self) -> usize { + self.transitions.len() + } + + fn transitions_are_empty_v1(&self) -> bool { + self.transitions.is_empty() + } + + fn first_transition_v1(&self) -> Option> { + self.transitions + .first() + .map(|transition| BatchedTransitionRefV1::Document(transition.into())) + } + + fn first_transition_mut_v1(&mut self) -> Option> { + self.transitions + .first_mut() + .map(|transition| BatchedTransitionMutRefV1::Document(transition.into())) + } + + fn contains_document_transition_v1(&self) -> bool { + !self.transitions.is_empty() + } + + fn contains_token_transition_v1(&self) -> bool { + false + } +} + +impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV1 { + type IterType<'a> + = DocumentBatchIteratorV1<'a> + where + Self: 'a; + + fn transitions_iter_v1(&self) -> Self::IterType<'_> { + DocumentBatchIteratorV1::V1(self.transitions.iter()) + } + + fn transitions_len_v1(&self) -> usize { + self.transitions.len() + } + + fn transitions_are_empty_v1(&self) -> bool { + self.transitions.is_empty() + } + + fn first_transition_v1(&self) -> Option> { + self.transitions.first().map(Into::into) + } + + fn first_transition_mut_v1(&mut self) -> Option> { + self.transitions.first_mut().map(Into::into) + } + + fn contains_document_transition_v1(&self) -> bool { + self.transitions + .iter() + .any(|transition| matches!(transition, BatchedTransition::Document(_))) + } + + fn contains_token_transition_v1(&self) -> bool { + self.transitions + .iter() + .any(|transition| matches!(transition, BatchedTransition::Token(_))) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs index 6f2926d9211..3b515a9a18f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs @@ -15,9 +15,9 @@ impl DocumentEraseTransition { document_type: DocumentTypeRef, token_payment_info: Option, identity_contract_nonce: IdentityNonce, - platform_version: &PlatformVersion, feature_version: Option, base_feature_version: Option, + platform_version: &PlatformVersion, ) -> Result { // `None` bounds mean the kind does not exist at this platform version // (it joined the wire at protocol version 14) — constructing one there @@ -38,8 +38,8 @@ impl DocumentEraseTransition { document_type, token_payment_info, identity_contract_nonce, - platform_version, base_feature_version, + platform_version, )? .into()), version => Err(ProtocolError::UnknownVersionMismatch { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs index e1b0626b41e..ae751b487cf 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/mod.rs @@ -2,13 +2,13 @@ mod from_document; pub mod v0; pub mod v0_methods; -use bincode::{Decode, Encode}; +use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::{Display, From}; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; pub use v0::*; -#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From)] +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs index b581b61fa97..b423ad0397c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/from_document.rs @@ -13,8 +13,8 @@ impl DocumentEraseTransitionV0 { document_type: DocumentTypeRef, token_payment_info: Option, identity_contract_nonce: IdentityNonce, - platform_version: &PlatformVersion, base_feature_version: Option, + platform_version: &PlatformVersion, ) -> Result { Ok(DocumentEraseTransitionV0 { base: DocumentBaseTransition::from_document( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs index b65e0986c97..c86632c159e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/v0/mod.rs @@ -3,7 +3,7 @@ pub mod v0_methods; use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; -use bincode::{Decode, Encode}; +use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::Display; #[cfg(feature = "json-conversion")] @@ -19,7 +19,7 @@ pub use super::super::document_base_transition::IDENTIFIER_FIELDS; /// whether it starts or continues an erasure, are decided from the document's /// committed lifecycle rather than from anything the submitter signs. #[cfg_attr(feature = "json-conversion", json_safe_fields)] -#[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display)] +#[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Display, DecodeUntrusted)] #[cfg_attr( feature = "serde-conversion", derive(Serialize, Deserialize), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index 410ae076dbd..1b9c13ddea9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use bincode::{Encode, Decode, DecodeUntrusted}; use crate::prelude::{IdentityNonce, Revision}; use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; -use crate::state_transition::batch_transition::batched_transition::{DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; +use crate::state_transition::batch_transition::batched_transition::{DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; @@ -28,7 +28,7 @@ use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionRes // `document_type_name` as `$type` in JSON (the long-standing DPP // document-type field). The variant names (`create`, `replace`, // `delete`, `transfer`, `updatePrice`, `purchase`, - // `indexOnlyDelete`, `erase`) read naturally as + // `indexOnlyDelete`) read naturally as // actions, matching the existing `PROPERTY_ACTION = "$action"` // constant on the parent batch transition. serde(tag = "$action", rename_all = "camelCase") @@ -57,13 +57,6 @@ pub enum DocumentTransition { /// PV14+ (see the wire gate in `validate_base_structure_v0`). #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] IndexOnlyDelete(DocumentIndexOnlyDeleteTransition), - - /// The erase kind, which purges the retained revisions of a deleted - /// keep-history document — appended at the end so every existing variant - /// keeps its bincode discriminant. Only exists at protocol version 14 and - /// later (see the wire gate in `validate_base_structure_v0`). - #[display("EraseDocumentTransition({})", "_0")] - Erase(DocumentEraseTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -81,7 +74,7 @@ impl crate::serialization::ValueConvertible for DocumentTransition {} pub(crate) mod json_convertible_tests { use super::*; use crate::state_transition::batch_transition::batched_transition::{ - document_create_transition, document_delete_transition, document_erase_transition, + document_create_transition, document_delete_transition, document_index_only_delete_transition, document_purchase_transition, document_replace_transition, document_transfer_transition, document_update_price_transition, @@ -190,14 +183,6 @@ pub(crate) mod json_convertible_tests { "indexOnlyDelete", ); } - - #[test] - fn umbrella_erase() { - assert_umbrella_round_trip( - DocumentTransition::Erase(document_erase_transition::json_convertible_tests::fixture()), - "erase", - ); - } } impl BatchTransitionResolversV0 for DocumentTransition { @@ -340,7 +325,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base(), DocumentTransition::Purchase(t) => t.base(), DocumentTransition::IndexOnlyDelete(t) => t.base(), - DocumentTransition::Erase(t) => t.base(), } } @@ -353,7 +337,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => t.data().get(path), - DocumentTransition::Erase(_) => None, } } @@ -374,7 +357,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, - DocumentTransition::Erase(_) => None, } } @@ -391,7 +373,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data()), - DocumentTransition::Erase(_) => None, } } @@ -410,7 +391,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => Some(t.revision()), DocumentTransition::Purchase(t) => Some(t.revision()), DocumentTransition::IndexOnlyDelete(_) => None, - DocumentTransition::Erase(_) => None, } } @@ -423,7 +403,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base().identity_contract_nonce(), DocumentTransition::Purchase(t) => t.base().identity_contract_nonce(), DocumentTransition::IndexOnlyDelete(t) => t.base().identity_contract_nonce(), - DocumentTransition::Erase(t) => t.base().identity_contract_nonce(), } } @@ -447,7 +426,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.data_mut().insert(property_name, value); } - DocumentTransition::Erase(_) => {} } } @@ -464,7 +442,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base_mut(), DocumentTransition::Purchase(t) => t.base_mut(), DocumentTransition::IndexOnlyDelete(t) => t.base_mut(), - DocumentTransition::Erase(t) => t.base_mut(), } } @@ -477,7 +454,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data_mut()), - DocumentTransition::Erase(_) => None, } } @@ -490,7 +466,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(ref mut t) => t.set_revision(revision), DocumentTransition::Purchase(ref mut t) => t.set_revision(revision), DocumentTransition::IndexOnlyDelete(_) => {} - DocumentTransition::Erase(_) => {} } } @@ -505,7 +480,6 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.base_mut().set_identity_contract_nonce(nonce) } - DocumentTransition::Erase(t) => t.base_mut().set_identity_contract_nonce(nonce), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs index b2a946706ee..74691b73421 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs @@ -1,3 +1,6 @@ +use crate::state_transition::batch_transition::batched_transition::{ + DocumentTransitionRefV1, DocumentTransitionV1, +}; use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; use crate::ProtocolError; @@ -29,7 +32,29 @@ impl DocumentTransitionActionTypeGetter for DocumentTransition { DocumentTransition::UpdatePrice(_) => DocumentTransitionActionType::UpdatePrice, DocumentTransition::Purchase(_) => DocumentTransitionActionType::Purchase, DocumentTransition::IndexOnlyDelete(_) => DocumentTransitionActionType::IndexOnlyDelete, - DocumentTransition::Erase(_) => DocumentTransitionActionType::Erase, + } + } +} + +impl DocumentTransitionActionTypeGetter for DocumentTransitionV1 { + fn action_type(&self) -> DocumentTransitionActionType { + self.borrow_as_ref().action_type() + } +} + +impl DocumentTransitionActionTypeGetter for DocumentTransitionRefV1<'_> { + fn action_type(&self) -> DocumentTransitionActionType { + match self { + DocumentTransitionRefV1::Create(_) => DocumentTransitionActionType::Create, + DocumentTransitionRefV1::Delete(_) => DocumentTransitionActionType::Delete, + DocumentTransitionRefV1::Replace(_) => DocumentTransitionActionType::Replace, + DocumentTransitionRefV1::Transfer(_) => DocumentTransitionActionType::Transfer, + DocumentTransitionRefV1::UpdatePrice(_) => DocumentTransitionActionType::UpdatePrice, + DocumentTransitionRefV1::Purchase(_) => DocumentTransitionActionType::Purchase, + DocumentTransitionRefV1::IndexOnlyDelete(_) => { + DocumentTransitionActionType::IndexOnlyDelete + } + DocumentTransitionRefV1::Erase(_) => DocumentTransitionActionType::Erase, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index d2a0fa480d1..1dc2eb21c33 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -30,6 +30,7 @@ pub mod token_transfer_transition; pub mod token_transition; pub mod token_transition_action_type; pub mod token_unfreeze_transition; +pub mod v1; use crate::prelude::IdentityNonce; use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; @@ -46,6 +47,10 @@ pub use document_transition::DocumentTransition; pub use document_update_price_transition::DocumentUpdatePriceTransition; use platform_value::Identifier; pub use token_transition::TokenTransition; +pub use v1::{ + BatchedTransitionMutRefV1, BatchedTransitionRefV1, BatchedTransitionV1, + DocumentTransitionMutRefV1, DocumentTransitionRefV1, DocumentTransitionV1, +}; pub const PROPERTY_ACTION: &str = "$action"; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs new file mode 100644 index 00000000000..215bbcc34fe --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs @@ -0,0 +1,944 @@ +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +use platform_value::{Identifier, Value}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use crate::prelude::{IdentityNonce, Revision}; +use crate::ProtocolError; +use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransition, DocumentCreateTransition, DocumentDeleteTransition, + DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, + DocumentReplaceTransition, DocumentTransferTransition, DocumentTransition, + DocumentUpdatePriceTransition, TokenTransition, +}; +use crate::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; +use crate::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use crate::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; +use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; +use crate::state_transition::batch_transition::{ + TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, + TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, + TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, + TokenSetPriceForDirectPurchaseTransition, TokenTransferTransition, TokenUnfreezeTransition, +}; + +/// The document transition shell carried by batch transition format 2. +/// +/// Batch formats 0 and 1 carry [`DocumentTransition`], whose wire discriminants +/// end at the indexOnly delete kind. Format 2 owns this shell so that the erase +/// kind can exist without changing what a format 0 or 1 batch can decode: +/// software that only knows the older formats keeps rejecting an erase at +/// decode time, exactly as software that predates the kind does. The concrete +/// per-kind payloads are shared with [`DocumentTransition`]; only the two shell +/// enums differ. +#[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + // The same `$action` discriminator and variant names as + // `DocumentTransition`, so a format 2 batch has the same external shape + // for every kind the older formats already carried. + serde(tag = "$action", rename_all = "camelCase") +)] +pub enum DocumentTransitionV1 { + #[display("CreateDocumentTransition({})", "_0")] + Create(DocumentCreateTransition), + + #[display("ReplaceDocumentTransition({})", "_0")] + Replace(DocumentReplaceTransition), + + #[display("DeleteDocumentTransition({})", "_0")] + Delete(DocumentDeleteTransition), + + #[display("TransferDocumentTransition({})", "_0")] + Transfer(DocumentTransferTransition), + + #[display("UpdatePriceDocumentTransition({})", "_0")] + UpdatePrice(DocumentUpdatePriceTransition), + + #[display("PurchaseDocumentTransition({})", "_0")] + Purchase(DocumentPurchaseTransition), + + #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] + IndexOnlyDelete(DocumentIndexOnlyDeleteTransition), + + /// The erase kind, which purges the retained revisions of a deleted + /// keep-history document. It only exists in this shell. + #[display("EraseDocumentTransition({})", "_0")] + Erase(DocumentEraseTransition), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for DocumentTransitionV1 {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for DocumentTransitionV1 {} + +impl From for DocumentTransitionV1 { + fn from(value: DocumentTransition) -> Self { + match value { + DocumentTransition::Create(transition) => Self::Create(transition), + DocumentTransition::Replace(transition) => Self::Replace(transition), + DocumentTransition::Delete(transition) => Self::Delete(transition), + DocumentTransition::Transfer(transition) => Self::Transfer(transition), + DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), + DocumentTransition::Purchase(transition) => Self::Purchase(transition), + DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), + } + } +} + +impl<'a> From<&'a DocumentTransition> for DocumentTransitionRefV1<'a> { + fn from(value: &'a DocumentTransition) -> Self { + match value { + DocumentTransition::Create(transition) => Self::Create(transition), + DocumentTransition::Replace(transition) => Self::Replace(transition), + DocumentTransition::Delete(transition) => Self::Delete(transition), + DocumentTransition::Transfer(transition) => Self::Transfer(transition), + DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), + DocumentTransition::Purchase(transition) => Self::Purchase(transition), + DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), + } + } +} + +impl<'a> From<&'a mut DocumentTransition> for DocumentTransitionMutRefV1<'a> { + fn from(value: &'a mut DocumentTransition) -> Self { + match value { + DocumentTransition::Create(transition) => Self::Create(transition), + DocumentTransition::Replace(transition) => Self::Replace(transition), + DocumentTransition::Delete(transition) => Self::Delete(transition), + DocumentTransition::Transfer(transition) => Self::Transfer(transition), + DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), + DocumentTransition::Purchase(transition) => Self::Purchase(transition), + DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), + } + } +} + +impl TryFrom for DocumentTransition { + type Error = ProtocolError; + + /// Fails for an erase: no batch format before 2 can carry one. + fn try_from(value: DocumentTransitionV1) -> Result { + Ok(match value { + DocumentTransitionV1::Create(transition) => Self::Create(transition), + DocumentTransitionV1::Replace(transition) => Self::Replace(transition), + DocumentTransitionV1::Delete(transition) => Self::Delete(transition), + DocumentTransitionV1::Transfer(transition) => Self::Transfer(transition), + DocumentTransitionV1::UpdatePrice(transition) => Self::UpdatePrice(transition), + DocumentTransitionV1::Purchase(transition) => Self::Purchase(transition), + DocumentTransitionV1::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), + DocumentTransitionV1::Erase(_) => { + return Err(ProtocolError::Generic( + "an erase transition only exists in batch transition format 2".to_string(), + )) + } + }) + } +} + +/// The batched transition shell carried by batch transition format 2. +/// +/// It differs from [`BatchedTransition`] only in the document shell it wraps, +/// which is the one that knows the erase kind. +#[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + // The same `$transition` discriminator as `BatchedTransition`, so the + // external shape of a format 2 batch matches the older formats. + serde(tag = "$transition", rename_all = "camelCase") +)] +pub enum BatchedTransitionV1 { + #[display("DocumentTransition({})", "_0")] + Document(DocumentTransitionV1), + #[display("TokenTransition({})", "_0")] + Token(TokenTransition), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for BatchedTransitionV1 {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for BatchedTransitionV1 {} + +impl From for BatchedTransitionV1 { + fn from(value: BatchedTransition) -> Self { + match value { + BatchedTransition::Document(transition) => Self::Document(transition.into()), + BatchedTransition::Token(transition) => Self::Token(transition), + } + } +} + +impl<'a> From<&'a BatchedTransition> for BatchedTransitionRefV1<'a> { + fn from(value: &'a BatchedTransition) -> Self { + match value { + BatchedTransition::Document(transition) => Self::Document(transition.into()), + BatchedTransition::Token(transition) => Self::Token(transition), + } + } +} + +impl<'a> From<&'a mut BatchedTransition> for BatchedTransitionMutRefV1<'a> { + fn from(value: &'a mut BatchedTransition) -> Self { + match value { + BatchedTransition::Document(transition) => Self::Document(transition.into()), + BatchedTransition::Token(transition) => Self::Token(transition), + } + } +} + +impl TryFrom for BatchedTransition { + type Error = ProtocolError; + + /// Fails for an erase: no batch format before 2 can carry one. + fn try_from(value: BatchedTransitionV1) -> Result { + Ok(match value { + BatchedTransitionV1::Document(transition) => Self::Document(transition.try_into()?), + BatchedTransitionV1::Token(transition) => Self::Token(transition), + }) + } +} + +/// A borrowed view of one document transition of any batch format. +/// +/// Every batch format can be viewed through it: formats 0 and 1 convert their +/// [`DocumentTransition`] items, format 2 borrows its own shell. +#[derive(Debug, Clone, Copy, PartialEq, Display)] +pub enum DocumentTransitionRefV1<'a> { + #[display("CreateDocumentTransition({})", "_0")] + Create(&'a DocumentCreateTransition), + #[display("ReplaceDocumentTransition({})", "_0")] + Replace(&'a DocumentReplaceTransition), + #[display("DeleteDocumentTransition({})", "_0")] + Delete(&'a DocumentDeleteTransition), + #[display("TransferDocumentTransition({})", "_0")] + Transfer(&'a DocumentTransferTransition), + #[display("UpdatePriceDocumentTransition({})", "_0")] + UpdatePrice(&'a DocumentUpdatePriceTransition), + #[display("PurchaseDocumentTransition({})", "_0")] + Purchase(&'a DocumentPurchaseTransition), + #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] + IndexOnlyDelete(&'a DocumentIndexOnlyDeleteTransition), + #[display("EraseDocumentTransition({})", "_0")] + Erase(&'a DocumentEraseTransition), +} + +/// A mutable view of one document transition of any batch format. +#[derive(Debug, PartialEq, Display)] +pub enum DocumentTransitionMutRefV1<'a> { + #[display("CreateDocumentTransition({})", "_0")] + Create(&'a mut DocumentCreateTransition), + #[display("ReplaceDocumentTransition({})", "_0")] + Replace(&'a mut DocumentReplaceTransition), + #[display("DeleteDocumentTransition({})", "_0")] + Delete(&'a mut DocumentDeleteTransition), + #[display("TransferDocumentTransition({})", "_0")] + Transfer(&'a mut DocumentTransferTransition), + #[display("UpdatePriceDocumentTransition({})", "_0")] + UpdatePrice(&'a mut DocumentUpdatePriceTransition), + #[display("PurchaseDocumentTransition({})", "_0")] + Purchase(&'a mut DocumentPurchaseTransition), + #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] + IndexOnlyDelete(&'a mut DocumentIndexOnlyDeleteTransition), + #[display("EraseDocumentTransition({})", "_0")] + Erase(&'a mut DocumentEraseTransition), +} + +impl DocumentTransitionV1 { + pub fn borrow_as_ref(&self) -> DocumentTransitionRefV1<'_> { + match self { + Self::Create(transition) => DocumentTransitionRefV1::Create(transition), + Self::Replace(transition) => DocumentTransitionRefV1::Replace(transition), + Self::Delete(transition) => DocumentTransitionRefV1::Delete(transition), + Self::Transfer(transition) => DocumentTransitionRefV1::Transfer(transition), + Self::UpdatePrice(transition) => DocumentTransitionRefV1::UpdatePrice(transition), + Self::Purchase(transition) => DocumentTransitionRefV1::Purchase(transition), + Self::IndexOnlyDelete(transition) => { + DocumentTransitionRefV1::IndexOnlyDelete(transition) + } + Self::Erase(transition) => DocumentTransitionRefV1::Erase(transition), + } + } + + pub fn borrow_as_mut(&mut self) -> DocumentTransitionMutRefV1<'_> { + match self { + Self::Create(transition) => DocumentTransitionMutRefV1::Create(transition), + Self::Replace(transition) => DocumentTransitionMutRefV1::Replace(transition), + Self::Delete(transition) => DocumentTransitionMutRefV1::Delete(transition), + Self::Transfer(transition) => DocumentTransitionMutRefV1::Transfer(transition), + Self::UpdatePrice(transition) => DocumentTransitionMutRefV1::UpdatePrice(transition), + Self::Purchase(transition) => DocumentTransitionMutRefV1::Purchase(transition), + Self::IndexOnlyDelete(transition) => { + DocumentTransitionMutRefV1::IndexOnlyDelete(transition) + } + Self::Erase(transition) => DocumentTransitionMutRefV1::Erase(transition), + } + } + + /// The erase transition, when this is one. + pub fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { + match self { + Self::Erase(transition) => Some(transition), + _ => None, + } + } +} + +/// A borrowed view of one batched transition of any batch format. +#[derive(Debug, Clone, Copy, PartialEq, Display)] +pub enum BatchedTransitionRefV1<'a> { + #[display("DocumentTransition({})", "_0")] + Document(DocumentTransitionRefV1<'a>), + #[display("TokenTransition({})", "_0")] + Token(&'a TokenTransition), +} + +/// A mutable view of one batched transition of any batch format. +#[derive(Debug, PartialEq, Display)] +pub enum BatchedTransitionMutRefV1<'a> { + #[display("DocumentTransition({})", "_0")] + Document(DocumentTransitionMutRefV1<'a>), + #[display("TokenTransition({})", "_0")] + Token(&'a mut TokenTransition), +} + +impl BatchedTransitionV1 { + pub fn borrow_as_ref(&self) -> BatchedTransitionRefV1<'_> { + match self { + Self::Document(transition) => { + BatchedTransitionRefV1::Document(transition.borrow_as_ref()) + } + Self::Token(transition) => BatchedTransitionRefV1::Token(transition), + } + } + + pub fn borrow_as_mut(&mut self) -> BatchedTransitionMutRefV1<'_> { + match self { + Self::Document(transition) => { + BatchedTransitionMutRefV1::Document(transition.borrow_as_mut()) + } + Self::Token(transition) => BatchedTransitionMutRefV1::Token(transition), + } + } + + pub fn set_identity_contract_nonce(&mut self, identity_contract_nonce: IdentityNonce) { + match self { + Self::Document(transition) => { + transition.set_identity_contract_nonce(identity_contract_nonce) + } + Self::Token(transition) => { + transition.set_identity_contract_nonce(identity_contract_nonce) + } + } + } +} + +impl<'a> BatchedTransitionRefV1<'a> { + pub fn to_owned_transition(&self) -> BatchedTransitionV1 { + match self { + Self::Document(transition) => { + BatchedTransitionV1::Document(transition.to_owned_transition()) + } + Self::Token(transition) => BatchedTransitionV1::Token((*transition).clone()), + } + } + + pub fn identity_contract_nonce(&self) -> IdentityNonce { + match self { + Self::Document(transition) => transition.identity_contract_nonce(), + Self::Token(transition) => transition.identity_contract_nonce(), + } + } + + pub fn data_contract_id(&self) -> Identifier { + match self { + Self::Document(transition) => transition.data_contract_id(), + Self::Token(transition) => transition.data_contract_id(), + } + } + + /// The erase transition, when this is one. + pub fn as_transition_erase(&self) -> Option<&'a DocumentEraseTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Erase(transition)) => Some(transition), + _ => None, + } + } +} + +impl<'a> DocumentTransitionRefV1<'a> { + pub fn to_owned_transition(&self) -> DocumentTransitionV1 { + match self { + Self::Create(transition) => DocumentTransitionV1::Create((*transition).clone()), + Self::Replace(transition) => DocumentTransitionV1::Replace((*transition).clone()), + Self::Delete(transition) => DocumentTransitionV1::Delete((*transition).clone()), + Self::Transfer(transition) => DocumentTransitionV1::Transfer((*transition).clone()), + Self::UpdatePrice(transition) => { + DocumentTransitionV1::UpdatePrice((*transition).clone()) + } + Self::Purchase(transition) => DocumentTransitionV1::Purchase((*transition).clone()), + Self::IndexOnlyDelete(transition) => { + DocumentTransitionV1::IndexOnlyDelete((*transition).clone()) + } + Self::Erase(transition) => DocumentTransitionV1::Erase((*transition).clone()), + } + } + + pub fn base(&self) -> &'a DocumentBaseTransition { + match self { + Self::Create(transition) => transition.base(), + Self::Replace(transition) => transition.base(), + Self::Delete(transition) => transition.base(), + Self::Transfer(transition) => transition.base(), + Self::UpdatePrice(transition) => transition.base(), + Self::Purchase(transition) => transition.base(), + Self::IndexOnlyDelete(transition) => transition.base(), + Self::Erase(transition) => transition.base(), + } + } + + pub fn get_id(&self) -> Identifier { + self.base().id() + } + + pub fn document_type_name(&self) -> &'a String { + self.base().document_type_name() + } + + pub fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + pub fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } + + pub fn entropy(&self) -> Option> { + match self { + Self::Create(transition) => Some(Vec::from(transition.entropy())), + _ => None, + } + } + + /// The document data the transition carries, when its kind carries any. + pub fn data(&self) -> Option<&'a BTreeMap> { + match self { + Self::Create(transition) => Some(transition.data()), + Self::Replace(transition) => Some(transition.data()), + Self::IndexOnlyDelete(transition) => Some(transition.data()), + Self::Delete(_) + | Self::Transfer(_) + | Self::UpdatePrice(_) + | Self::Purchase(_) + | Self::Erase(_) => None, + } + } + + pub fn get_dynamic_property(&self, path: &str) -> Option<&'a Value> { + self.data()?.get(path) + } + + pub fn first_data_depth_exceeding(&self, max_depth: usize) -> Option { + self.data()? + .values() + .find_map(|value| value.first_depth_exceeding(max_depth)) + } + + pub fn revision(&self) -> Option { + match self { + Self::Create(_) => Some(1), + Self::Replace(transition) => Some(transition.revision()), + Self::Transfer(transition) => Some(transition.revision()), + Self::UpdatePrice(transition) => Some(transition.revision()), + Self::Purchase(transition) => Some(transition.revision()), + Self::Delete(_) | Self::IndexOnlyDelete(_) | Self::Erase(_) => None, + } + } + + pub fn as_transition_create(&self) -> Option<&'a DocumentCreateTransition> { + match self { + Self::Create(transition) => Some(transition), + _ => None, + } + } + + pub fn as_transition_replace(&self) -> Option<&'a DocumentReplaceTransition> { + match self { + Self::Replace(transition) => Some(transition), + _ => None, + } + } + + pub fn as_transition_delete(&self) -> Option<&'a DocumentDeleteTransition> { + match self { + Self::Delete(transition) => Some(transition), + _ => None, + } + } + + pub fn as_transition_transfer(&self) -> Option<&'a DocumentTransferTransition> { + match self { + Self::Transfer(transition) => Some(transition), + _ => None, + } + } + + pub fn as_transition_purchase(&self) -> Option<&'a DocumentPurchaseTransition> { + match self { + Self::Purchase(transition) => Some(transition), + _ => None, + } + } + + pub fn as_transition_erase(&self) -> Option<&'a DocumentEraseTransition> { + match self { + Self::Erase(transition) => Some(transition), + _ => None, + } + } +} + +impl DocumentTransitionV0Methods for DocumentTransitionV1 { + fn base(&self) -> &DocumentBaseTransition { + match self { + Self::Create(transition) => transition.base(), + Self::Replace(transition) => transition.base(), + Self::Delete(transition) => transition.base(), + Self::Transfer(transition) => transition.base(), + Self::UpdatePrice(transition) => transition.base(), + Self::Purchase(transition) => transition.base(), + Self::IndexOnlyDelete(transition) => transition.base(), + Self::Erase(transition) => transition.base(), + } + } + + fn get_dynamic_property(&self, path: &str) -> Option<&Value> { + self.data()?.get(path) + } + + fn get_id(&self) -> Identifier { + self.base().id() + } + + fn entropy(&self) -> Option> { + match self { + Self::Create(transition) => Some(Vec::from(transition.entropy())), + _ => None, + } + } + + fn document_type_name(&self) -> &String { + self.base().document_type_name() + } + + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + fn data(&self) -> Option<&BTreeMap> { + match self { + Self::Create(transition) => Some(transition.data()), + Self::Replace(transition) => Some(transition.data()), + Self::IndexOnlyDelete(transition) => Some(transition.data()), + Self::Delete(_) + | Self::Transfer(_) + | Self::UpdatePrice(_) + | Self::Purchase(_) + | Self::Erase(_) => None, + } + } + + fn first_data_depth_exceeding(&self, max_depth: usize) -> Option { + self.borrow_as_ref().first_data_depth_exceeding(max_depth) + } + + fn revision(&self) -> Option { + self.borrow_as_ref().revision() + } + + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } + + #[cfg(test)] + fn insert_dynamic_property(&mut self, property_name: String, value: Value) { + if let Some(data) = self.data_mut() { + data.insert(property_name, value); + } + } + + fn set_data_contract_id(&mut self, id: Identifier) { + self.base_mut().set_data_contract_id(id) + } + + fn base_mut(&mut self) -> &mut DocumentBaseTransition { + match self { + Self::Create(transition) => transition.base_mut(), + Self::Replace(transition) => transition.base_mut(), + Self::Delete(transition) => transition.base_mut(), + Self::Transfer(transition) => transition.base_mut(), + Self::UpdatePrice(transition) => transition.base_mut(), + Self::Purchase(transition) => transition.base_mut(), + Self::IndexOnlyDelete(transition) => transition.base_mut(), + Self::Erase(transition) => transition.base_mut(), + } + } + + fn data_mut(&mut self) -> Option<&mut BTreeMap> { + match self { + Self::Create(transition) => Some(transition.data_mut()), + Self::Replace(transition) => Some(transition.data_mut()), + Self::IndexOnlyDelete(transition) => Some(transition.data_mut()), + Self::Delete(_) + | Self::Transfer(_) + | Self::UpdatePrice(_) + | Self::Purchase(_) + | Self::Erase(_) => None, + } + } + + fn set_revision(&mut self, revision: Revision) { + match self { + Self::Replace(transition) => transition.set_revision(revision), + Self::Transfer(transition) => transition.set_revision(revision), + Self::UpdatePrice(transition) => transition.set_revision(revision), + Self::Purchase(transition) => transition.set_revision(revision), + Self::Create(_) | Self::Delete(_) | Self::IndexOnlyDelete(_) | Self::Erase(_) => {} + } + } + + fn set_identity_contract_nonce(&mut self, nonce: IdentityNonce) { + self.base_mut().set_identity_contract_nonce(nonce) + } +} + +impl BatchTransitionResolversV0 for BatchedTransitionV1 { + fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { + match self { + Self::Document(DocumentTransitionV1::Create(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_replace(&self) -> Option<&DocumentReplaceTransition> { + match self { + Self::Document(DocumentTransitionV1::Replace(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_delete(&self) -> Option<&DocumentDeleteTransition> { + match self { + Self::Document(DocumentTransitionV1::Delete(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_transfer(&self) -> Option<&DocumentTransferTransition> { + match self { + Self::Document(DocumentTransitionV1::Transfer(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_purchase(&self) -> Option<&DocumentPurchaseTransition> { + match self { + Self::Document(DocumentTransitionV1::Purchase(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_burn(), + } + } + + fn as_transition_token_mint(&self) -> Option<&TokenMintTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_mint(), + } + } + + fn as_transition_token_transfer(&self) -> Option<&TokenTransferTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_transfer(), + } + } + + fn as_transition_token_freeze(&self) -> Option<&TokenFreezeTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_freeze(), + } + } + + fn as_transition_token_unfreeze(&self) -> Option<&TokenUnfreezeTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_unfreeze(), + } + } + + fn as_transition_token_destroy_frozen_funds( + &self, + ) -> Option<&TokenDestroyFrozenFundsTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_destroy_frozen_funds(), + } + } + + fn as_transition_token_claim(&self) -> Option<&TokenClaimTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_claim(), + } + } + + fn as_transition_token_emergency_action(&self) -> Option<&TokenEmergencyActionTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_emergency_action(), + } + } + + fn as_transition_token_config_update(&self) -> Option<&TokenConfigUpdateTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_config_update(), + } + } + + fn as_transition_token_direct_purchase(&self) -> Option<&TokenDirectPurchaseTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_direct_purchase(), + } + } + + fn as_transition_token_set_price_for_direct_purchase( + &self, + ) -> Option<&TokenSetPriceForDirectPurchaseTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_set_price_for_direct_purchase(), + } + } +} + +impl BatchTransitionResolversV0 for BatchedTransitionRefV1<'_> { + fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Create(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_replace(&self) -> Option<&DocumentReplaceTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Replace(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_delete(&self) -> Option<&DocumentDeleteTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Delete(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_transfer(&self) -> Option<&DocumentTransferTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Transfer(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_purchase(&self) -> Option<&DocumentPurchaseTransition> { + match self { + Self::Document(DocumentTransitionRefV1::Purchase(transition)) => Some(transition), + _ => None, + } + } + + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_burn(), + } + } + + fn as_transition_token_mint(&self) -> Option<&TokenMintTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_mint(), + } + } + + fn as_transition_token_transfer(&self) -> Option<&TokenTransferTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_transfer(), + } + } + + fn as_transition_token_freeze(&self) -> Option<&TokenFreezeTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_freeze(), + } + } + + fn as_transition_token_unfreeze(&self) -> Option<&TokenUnfreezeTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_unfreeze(), + } + } + + fn as_transition_token_destroy_frozen_funds( + &self, + ) -> Option<&TokenDestroyFrozenFundsTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_destroy_frozen_funds(), + } + } + + fn as_transition_token_claim(&self) -> Option<&TokenClaimTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_claim(), + } + } + + fn as_transition_token_emergency_action(&self) -> Option<&TokenEmergencyActionTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_emergency_action(), + } + } + + fn as_transition_token_config_update(&self) -> Option<&TokenConfigUpdateTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_config_update(), + } + } + + fn as_transition_token_direct_purchase(&self) -> Option<&TokenDirectPurchaseTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_direct_purchase(), + } + } + + fn as_transition_token_set_price_for_direct_purchase( + &self, + ) -> Option<&TokenSetPriceForDirectPurchaseTransition> { + match self { + Self::Document(_) => None, + Self::Token(token) => token.as_transition_token_set_price_for_direct_purchase(), + } + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod json_convertible_tests { + use super::*; + use crate::serialization::{JsonConvertible, ValueConvertible}; + use crate::state_transition::batch_transition::batched_transition::{ + document_create_transition, document_erase_transition, + }; + + fn assert_document_round_trip(transition: DocumentTransitionV1, expected_action: &str) { + let json = transition.to_json().expect("to_json"); + let json_obj = json.as_object().expect("json object"); + assert_eq!( + json_obj.get("$action").and_then(|v| v.as_str()), + Some(expected_action), + "json `$action` discriminator mismatch" + ); + let recovered_json = DocumentTransitionV1::from_json(json).expect("from_json"); + assert_eq!(transition, recovered_json); + + let value = transition.to_object().expect("to_object"); + let recovered_value = DocumentTransitionV1::from_object(value).expect("from_object"); + assert_eq!(transition, recovered_value); + } + + #[test] + fn should_keep_the_external_shape_of_a_legacy_kind_in_the_format_2_shell() { + let create = document_create_transition::json_convertible_tests::fixture(); + let legacy = DocumentTransition::Create(create.clone()); + let format_2 = DocumentTransitionV1::Create(create); + + assert_eq!( + legacy.to_json().expect("legacy to_json"), + format_2.to_json().expect("format 2 to_json"), + "a kind shared by every batch format must serialize identically" + ); + assert_document_round_trip(format_2, "create"); + } + + #[test] + fn should_round_trip_an_erase_through_json_and_value() { + let erase = document_erase_transition::json_convertible_tests::fixture(); + assert_document_round_trip(DocumentTransitionV1::Erase(erase), "erase"); + } + + #[test] + fn should_tag_the_batched_shell_with_the_transition_kind() { + let erase = document_erase_transition::json_convertible_tests::fixture(); + let batched = BatchedTransitionV1::Document(DocumentTransitionV1::Erase(erase)); + + let json = batched.to_json().expect("to_json"); + let json_obj = json.as_object().expect("json object"); + assert_eq!( + json_obj.get("$transition").and_then(|v| v.as_str()), + Some("document") + ); + assert_eq!( + json_obj.get("$action").and_then(|v| v.as_str()), + Some("erase") + ); + let recovered = BatchedTransitionV1::from_json(json).expect("from_json"); + assert_eq!(batched, recovered); + } + + #[test] + fn should_refuse_an_erase_in_the_shell_of_the_older_batch_formats() { + let erase = document_erase_transition::json_convertible_tests::fixture(); + let json = DocumentTransitionV1::Erase(erase) + .to_json() + .expect("to_json"); + + assert!( + DocumentTransition::from_json(json).is_err(), + "the shell used by batch formats 0 and 1 has no erase kind" + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs index 549bc5c955c..0061ce2f174 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs @@ -7,6 +7,7 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.signature_public_key_id(), BatchTransition::V1(transition) => transition.signature_public_key_id(), + BatchTransition::V2(transition) => transition.signature_public_key_id(), } } @@ -14,6 +15,7 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature_public_key_id(key_id), BatchTransition::V1(transition) => transition.set_signature_public_key_id(key_id), + BatchTransition::V2(transition) => transition.set_signature_public_key_id(key_id), } } @@ -21,6 +23,7 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.security_level_requirement(purpose), BatchTransition::V1(transition) => transition.security_level_requirement(purpose), + BatchTransition::V2(transition) => transition.security_level_requirement(purpose), } } @@ -28,6 +31,7 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.purpose_requirement(), BatchTransition::V1(transition) => transition.purpose_requirement(), + BatchTransition::V2(transition) => transition.purpose_requirement(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index 5e9acff9b05..01e2b9872d9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -23,9 +23,12 @@ use crate::prelude::UserFeeIncrease; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; use crate::state_transition::batch_transition::BatchTransition; #[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{BatchTransitionV0, BatchTransitionV1}; +use crate::state_transition::batch_transition::{ + BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, +}; #[cfg(feature = "state-transition-signing")] use crate::state_transition::StateTransition; use crate::state_transition::StateTransitionSigningOptions; @@ -46,6 +49,7 @@ use platform_version::version::PlatformVersion; pub mod v0; pub mod v1; +pub mod v2; #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] pub struct StateTransitionCreationOptions { @@ -61,6 +65,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.all_document_purchases_amount(), BatchTransition::V1(v1) => v1.all_document_purchases_amount(), + BatchTransition::V2(v2) => v2.all_document_purchases_amount(), } } @@ -70,6 +75,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.all_conflicting_index_collateral_voting_funds(), BatchTransition::V1(v1) => v1.all_conflicting_index_collateral_voting_funds(), + BatchTransition::V2(v2) => v2.all_conflicting_index_collateral_voting_funds(), } } @@ -77,6 +83,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.set_transitions(transitions), BatchTransition::V1(v1) => v1.set_transitions(transitions), + BatchTransition::V2(v2) => v2.set_transitions(transitions), } } @@ -84,6 +91,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.set_identity_contract_nonce(identity_contract_nonce), BatchTransition::V1(v1) => v1.set_identity_contract_nonce(identity_contract_nonce), + BatchTransition::V2(v2) => v2.set_identity_contract_nonce(identity_contract_nonce), } } @@ -123,6 +131,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_creation_transition_from_document( document, @@ -138,9 +147,24 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 2 => Ok( + BatchTransitionV2::new_document_creation_transition_from_document( + document, + document_type, + entropy, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + token_payment_info, + signer, + platform_version, + options, + ) + .await?, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_created_from_document".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } @@ -180,6 +204,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_replacement_transition_from_document( document, @@ -194,56 +219,14 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - version => Err(ProtocolError::UnknownVersionMismatch { - method: - "DocumentsBatchTransition::new_document_replacement_transition_from_document" - .to_string(), - known_versions: vec![0, 1], - received: version, - }), - } - } - - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let resolved_options = options.unwrap_or_default(); - match resolved_options.batch_feature_version.unwrap_or( - platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .default_current_version, - ) { - 0 => Ok( - BatchTransitionV0::new_document_erase_transition_from_document( - document, - document_type, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await?, - ), - 1 => Ok( - BatchTransitionV1::new_document_erase_transition_from_document( + 2 => Ok( + BatchTransitionV2::new_document_replacement_transition_from_document( document, document_type, identity_public_key, identity_contract_nonce, user_fee_increase, + token_payment_info, signer, platform_version, options, @@ -251,9 +234,10 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { .await?, ), version => Err(ProtocolError::UnknownVersionMismatch { - method: "DocumentsBatchTransition::new_document_erase_transition_from_document" - .to_string(), - known_versions: vec![0, 1], + method: + "DocumentsBatchTransition::new_document_replacement_transition_from_document" + .to_string(), + known_versions: vec![0, 1, 2], received: version, }), } @@ -296,6 +280,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_transfer_transition_from_document( document, @@ -311,11 +296,26 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 2 => Ok( + BatchTransitionV2::new_document_transfer_transition_from_document( + document, + document_type, + recipient_owner_id, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + token_payment_info, + signer, + platform_version, + options, + ) + .await?, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_replacement_transition_from_document" .to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } @@ -355,6 +355,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_deletion_transition_from_document( document, @@ -369,10 +370,24 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 2 => Ok( + BatchTransitionV2::new_document_deletion_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + token_payment_info, + signer, + platform_version, + options, + ) + .await?, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_deletion_transition_from_document" .to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } @@ -414,6 +429,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_update_price_transition_from_document( document, @@ -429,11 +445,26 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 2 => Ok( + BatchTransitionV2::new_document_update_price_transition_from_document( + document, + document_type, + price, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + token_payment_info, + signer, + platform_version, + options, + ) + .await?, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_update_price_transition_from_document" .to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } @@ -477,6 +508,7 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 1 => Ok( BatchTransitionV1::new_document_purchase_transition_from_document( document, @@ -493,10 +525,70 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), + 2 => Ok( + BatchTransitionV2::new_document_purchase_transition_from_document( + document, + document_type, + new_owner_id, + price, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + token_payment_info, + signer, + platform_version, + options, + ) + .await?, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_purchase_transition_from_document" .to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], + received: version, + }), + } + } +} + +impl DocumentsBatchTransitionMethodsV2 for BatchTransition { + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + options: Option, + platform_version: &PlatformVersion, + ) -> Result { + let batch_feature_version = options.unwrap_or_default().batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ); + match batch_feature_version { + 2 => { + BatchTransitionV2::new_document_erase_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + options, + platform_version, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_document_erase_transition_from_document" + .to_string(), + known_versions: vec![2], received: version, }), } @@ -529,6 +621,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + BatchTransitionV2::new_token_mint_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + issued_to_identity_id, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -557,7 +674,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_mint_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -587,6 +704,30 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + BatchTransitionV2::new_token_burn_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -614,7 +755,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_burn_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -646,6 +787,33 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the transfer transition for batch version 1 + BatchTransitionV2::new_token_transfer_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + recipient_id, + public_note, + shared_encrypted_note, + private_encrypted_note, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -676,7 +844,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_transfer_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -706,6 +874,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the freeze transition for batch version 1 + BatchTransitionV2::new_token_freeze_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + freeze_identity_id, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -734,7 +927,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_freeze_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -764,6 +957,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the freeze transition for batch version 1 + BatchTransitionV2::new_token_unfreeze_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + unfreeze_identity_id, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -792,7 +1010,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_unfreeze_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -822,6 +1040,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the destroy frozen funds transition for batch version 1 + BatchTransitionV2::new_token_destroy_frozen_funds_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + frozen_identity_id, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -851,7 +1094,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_destroy_frozen_funds_transition" .to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -881,6 +1124,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the emergency action transition for batch version 1 + BatchTransitionV2::new_token_emergency_action_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + emergency_action, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -910,7 +1178,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_emergency_action_transition" .to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -940,6 +1208,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the emergency action transition for batch version 1 + BatchTransitionV2::new_token_config_update_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + update_token_configuration_item, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -968,7 +1261,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_config_update_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -997,6 +1290,30 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the emergency action transition for batch version 1 + BatchTransitionV2::new_token_claim_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + distribution_type, + public_note, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -1024,7 +1341,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_claim_transition".to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -1055,6 +1372,31 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the emergency action transition for batch version 1 + BatchTransitionV2::new_token_change_direct_purchase_price_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + token_pricing_schedule, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -1085,7 +1427,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { method: "DocumentsBatchTransition::new_token_change_direct_purchase_price_transition" .to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } @@ -1114,6 +1456,30 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { + 2 if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 2 => + { + // Create the emergency action transition for batch version 1 + BatchTransitionV2::new_token_direct_purchase_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + total_agreed_price, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } 1 | 0 if platform_version .dpp @@ -1142,7 +1508,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_direct_purchase_transition" .to_string(), - known_versions: vec![1], + known_versions: vec![1, 2], received: version, }), } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs index 60ad2b2a32c..76bf918daa4 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs @@ -11,23 +11,24 @@ use crate::identity::SecurityLevel; use crate::prelude::IdentityNonce; #[cfg(feature = "state-transition-signing")] use crate::prelude::UserFeeIncrease; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionRefV1, +}; use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; +#[cfg(feature = "state-transition-signing")] use crate::state_transition::StateTransition; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::token_payment_info::TokenPaymentInfo; use crate::ProtocolError; use platform_value::Identifier; #[cfg(feature = "state-transition-signing")] use platform_version::version::PlatformVersion; use std::convert::TryFrom; -use crate::state_transition::batch_transition::batched_transition::{BatchedTransition, BatchedTransitionRef}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; -use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::token_payment_info::TokenPaymentInfo; -pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 { +pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 { #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_document_creation_transition_from_document>( @@ -71,25 +72,6 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 options: Option, ) -> Result; - /// Builds a signed erase of one already deleted keep-history document. - /// - /// The transition carries no token payment: the deletion cost was charged - /// when the document was deleted. Whether this erase starts or continues an - /// erasure, and how many revisions it removes, are read from committed - /// state rather than signed here. - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result; - #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( @@ -150,14 +132,14 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 let mut highest_security_level = SecurityLevel::lowest_level(); if self - .transitions_iter() - .any(|transition| matches!(transition, BatchedTransitionRef::Token(_))) + .transitions_iter_v1() + .any(|transition| matches!(transition, BatchedTransitionRefV1::Token(_))) { // If we ever have a token transition it will be security level critical and so will the whole state transition highest_security_level = SecurityLevel::CRITICAL; } else if self - .transitions_iter() - .any(|transition| matches!(transition, BatchedTransitionRef::Document(_))) + .transitions_iter_v1() + .any(|transition| matches!(transition, BatchedTransitionRefV1::Document(_))) { // We know we don't have token transitions at this point let get_data_contract_security_level_requirement = @@ -167,8 +149,8 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 .to_string(), ), )?; - for transition in self.transitions_iter() { - if let BatchedTransitionRef::Document(document_transition) = transition { + for transition in self.transitions_iter_v1() { + if let BatchedTransitionRefV1::Document(document_transition) = transition { let document_type_name = document_transition.base().document_type_name(); let data_contract_id = document_transition.base().data_contract_id(); let document_security_level = get_data_contract_security_level_requirement( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 0e3e0340ffd..4297b298ee9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -14,7 +14,7 @@ use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; #[cfg(feature = "state-transition-signing")] use crate::prelude::{IdentityNonce, UserFeeIncrease}; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; #[cfg(feature = "state-transition-signing")] @@ -34,7 +34,7 @@ use platform_version::version::PlatformVersion; /// A trait defining methods to create various token-related state transitions as part of a document batch. /// -/// This trait builds on `DocumentsBatchTransitionAccessorsV0` and provides a unified interface +/// This trait builds on `DocumentsBatchTransitionAccessorsV1` and provides a unified interface /// for constructing signed `StateTransition`s for token operations such as minting, burning, /// transferring, freezing, claiming, and direct purchases. /// @@ -43,7 +43,7 @@ use platform_version::version::PlatformVersion; /// version selection, group action metadata, and fee calculations. /// /// All methods in this trait require the `state-transition-signing` feature to be enabled. -pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 { +pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV1 { /// Creates a `StateTransition` to mint new tokens. /// /// # Parameters diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs new file mode 100644 index 00000000000..4c4a8bea4ef --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs @@ -0,0 +1,35 @@ +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::document_type::DocumentTypeRef; +#[cfg(feature = "state-transition-signing")] +use crate::document::Document; +#[cfg(feature = "state-transition-signing")] +use crate::identity::signer::Signer; +#[cfg(feature = "state-transition-signing")] +use crate::identity::IdentityPublicKey; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::{IdentityNonce, UserFeeIncrease}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransition; +#[cfg(feature = "state-transition-signing")] +use crate::version::PlatformVersion; +#[cfg(feature = "state-transition-signing")] +use crate::ProtocolError; + +/// Constructors introduced with the V2 batch transition wire format. +pub trait DocumentsBatchTransitionMethodsV2 { + /// Builds a signed erase of one already deleted keep-history document. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + options: Option, + platform_version: &PlatformVersion, + ) -> Result; +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index d44ee39e535..4fffbea81eb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -49,6 +49,7 @@ mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; mod v1; +mod v2; #[cfg(feature = "validation")] mod validation; mod version; @@ -62,6 +63,7 @@ use crate::state_transition::batch_transition::fields::property_names; use crate::identity::state_transition::OptionallyAssetLockProved; pub use v0::*; pub use v1::*; +pub use v2::*; #[derive( Debug, @@ -91,6 +93,8 @@ pub enum BatchTransition { V0(BatchTransitionV0), #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))] V1(BatchTransitionV1), + #[cfg_attr(feature = "serde-conversion", serde(rename = "2"))] + V2(BatchTransitionV2), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs index 70b5ded5676..b47fc40e67f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs @@ -3,7 +3,7 @@ use crate::consensus::basic::BasicError; use crate::consensus::state::identity::IdentityInsufficientBalanceError; use crate::consensus::ConsensusError; use crate::fee::Credits; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::{ @@ -23,7 +23,7 @@ impl StateTransitionEstimatedFeeValidation for BatchTransition { .fee_version .state_transition_min_fees .document_batch_sub_transition - .saturating_mul(self.transitions_len() as u64)) + .saturating_mul(self.transitions_len_v1() as u64)) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs index 6e086e05299..17fdf860ca4 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs @@ -13,6 +13,7 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.modified_data_ids(), BatchTransition::V1(transition) => transition.modified_data_ids(), + BatchTransition::V2(transition) => transition.modified_data_ids(), } } @@ -20,6 +21,7 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(_) => 0, BatchTransition::V1(_) => 1, + BatchTransition::V2(_) => 2, } } /// returns the type of State Transition @@ -27,6 +29,7 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.state_transition_type(), BatchTransition::V1(transition) => transition.state_transition_type(), + BatchTransition::V2(transition) => transition.state_transition_type(), } } @@ -34,6 +37,7 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.unique_identifiers(), BatchTransition::V1(transition) => transition.unique_identifiers(), + BatchTransition::V2(transition) => transition.unique_identifiers(), } } } @@ -43,6 +47,7 @@ impl StateTransitionHasUserFeeIncrease for BatchTransition { match self { BatchTransition::V0(transition) => transition.user_fee_increase(), BatchTransition::V1(transition) => transition.user_fee_increase(), + BatchTransition::V2(transition) => transition.user_fee_increase(), } } @@ -50,6 +55,7 @@ impl StateTransitionHasUserFeeIncrease for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_user_fee_increase(user_fee_increase), BatchTransition::V1(transition) => transition.set_user_fee_increase(user_fee_increase), + BatchTransition::V2(transition) => transition.set_user_fee_increase(user_fee_increase), } } } @@ -60,6 +66,7 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.signature(), BatchTransition::V1(transition) => transition.signature(), + BatchTransition::V2(transition) => transition.signature(), } } /// set a new signature @@ -67,6 +74,7 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature(signature), BatchTransition::V1(transition) => transition.set_signature(signature), + BatchTransition::V2(transition) => transition.set_signature(signature), } } @@ -74,6 +82,7 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature_bytes(signature), BatchTransition::V1(transition) => transition.set_signature_bytes(signature), + BatchTransition::V2(transition) => transition.set_signature_bytes(signature), } } } @@ -83,6 +92,7 @@ impl StateTransitionOwned for BatchTransition { match self { BatchTransition::V0(transition) => transition.owner_id(), BatchTransition::V1(transition) => transition.owner_id(), + BatchTransition::V2(transition) => transition.owner_id(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs index c8011b7ec59..3d70d83a6f5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs @@ -30,13 +30,14 @@ mod batch_transition_tests { TokenTransitionActionType, TokenTransitionActionTypeGetter, }; use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef, + BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef, BatchedTransitionV1, + DocumentTransitionV1, }; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{ - BatchTransitionV0, BatchTransitionV1, + BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, }; use crate::state_transition::StateTransitionLike; use crate::state_transition::StateTransition; @@ -1186,4 +1187,165 @@ mod batch_transition_tests { assert!(matches!(second, BatchedTransitionRef::Token(_))); assert!(iter.next().is_none()); } + + // ----------------------------------------------------------------------- + // Batch format 2: the only format whose document shell carries an erase + // ----------------------------------------------------------------------- + + fn make_erase_transition(nonce: u64) -> DocumentTransitionV1 { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; + + DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: make_base_transition(nonce), + })) + } + + fn make_batch_v2(transitions: Vec) -> BatchTransitionV2 { + BatchTransitionV2 { + owner_id: Identifier::new([0x03; 32]), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + } + } + + #[test] + fn should_see_every_format_through_the_format_aware_view_and_only_the_old_ones_through_the_shipped_view( + ) { + use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; + + let v0 = BatchTransition::V0(make_batch_v0(vec![make_delete_transition(1)])); + let v1 = BatchTransition::V1(make_batch_v1(vec![ + BatchedTransition::Document(make_delete_transition(1)), + BatchedTransition::Token(make_token_burn_transition(2, 5)), + ])); + let v2 = BatchTransition::V2(make_batch_v2(vec![ + BatchedTransitionV1::Document(make_erase_transition(1)), + BatchedTransitionV1::Token(make_token_burn_transition(2, 5)), + ])); + + for (batch, expected_len) in [(&v0, 1), (&v1, 2), (&v2, 2)] { + assert_eq!(batch.transitions_len_v1(), expected_len); + assert_eq!(batch.transitions_iter_v1().count(), expected_len); + assert!(batch.first_transition_v1().is_some()); + assert!(batch.contains_document_transition_v1()); + } + assert!(v2.contains_token_transition_v1()); + assert!(v2 + .first_transition_v1() + .expect("first") + .as_transition_erase() + .is_some()); + + // The shipped view cannot represent an erase, so it reports a format 2 + // batch as empty: a shipped generation that ever received one would + // refuse it as a batch without transitions instead of dropping items. + assert_eq!(v0.transitions_len(), 1); + assert_eq!(v1.transitions_len(), 2); + assert_eq!(v2.transitions_len(), 0); + assert!(v2.transitions_are_empty()); + assert!(v2.transitions_iter().next().is_none()); + assert!(v2.first_transition().is_none()); + assert!(!v2.contains_document_transition()); + assert!(!v2.contains_token_transition()); + } + + #[test] + fn should_round_trip_a_format_2_batch_with_an_erase_through_bytes() { + use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; + + let batch = BatchTransition::V2(make_batch_v2(vec![ + BatchedTransitionV1::Document(make_erase_transition(1)), + BatchedTransitionV1::Document(DocumentTransitionV1::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: make_base_transition(2), + }), + )), + ])); + + let bytes = batch.serialize_to_bytes().expect("serialize"); + let recovered = + BatchTransition::deserialize_from_bytes_untrusted(&bytes).expect("deserialize"); + + assert_eq!(recovered, batch); + } + + /// Formats 1 and 2 lay out their items identically except for the kinds + /// their document shells know, so relabeling a format 2 batch as format 1 + /// shows exactly what software that only knows format 1 would do with the + /// same items: it decodes every shipped kind and refuses an erase. + #[test] + fn should_refuse_an_erase_in_the_bytes_of_a_format_1_batch() { + use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; + + let relabel_as_format_1 = |batch: BatchTransition| { + let mut bytes = batch.serialize_to_bytes().expect("serialize"); + assert_eq!(bytes[0], 2, "the first byte is the batch format"); + bytes[0] = 1; + BatchTransition::deserialize_from_bytes_untrusted(&bytes) + }; + + let delete_only = relabel_as_format_1(BatchTransition::V2(make_batch_v2(vec![ + BatchedTransitionV1::Document(DocumentTransitionV1::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: make_base_transition(2), + }), + )), + ]))) + .expect("a delete decodes in format 1"); + assert!(matches!(delete_only, BatchTransition::V1(_))); + assert_eq!(delete_only.transitions_len(), 1); + + let with_erase = relabel_as_format_1(BatchTransition::V2(make_batch_v2(vec![ + BatchedTransitionV1::Document(make_erase_transition(1)), + ]))); + assert!( + with_erase.is_err(), + "format 1 has no erase kind, so the bytes must not decode" + ); + } + + #[test] + fn should_refuse_to_downcast_an_erase_to_the_shipped_shells() { + let erase = BatchedTransitionV1::Document(make_erase_transition(1)); + let delete = BatchedTransitionV1::Document(DocumentTransitionV1::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: make_base_transition(2), + }), + )); + + assert!(BatchedTransition::try_from(erase).is_err()); + assert!(matches!( + BatchedTransition::try_from(delete), + Ok(BatchedTransition::Document(DocumentTransition::Delete(_))) + )); + } + + #[test] + fn should_upcast_and_set_shipped_transitions_on_a_format_2_batch() { + let mut batch = BatchTransition::V2(make_batch_v2(vec![BatchedTransitionV1::Document( + make_erase_transition(1), + )])); + + batch.set_transitions(vec![BatchedTransition::Document(make_delete_transition(7))]); + batch.set_identity_contract_nonce(9); + + let BatchTransition::V2(batch) = batch else { + panic!("the format does not change"); + }; + assert!(matches!( + batch.transitions.as_slice(), + [BatchedTransitionV1::Document(DocumentTransitionV1::Delete( + _ + ))] + )); + assert_eq!( + batch.transitions[0] + .borrow_as_ref() + .identity_contract_nonce(), + 9 + ); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs index becdc5a843c..73f5b7bb738 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs @@ -23,8 +23,7 @@ use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransi use crate::state_transition::batch_transition::BatchTransitionV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, - DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -183,54 +182,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV0 { } #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - // Erase never carries a token payment: it acts on a document whose - // deletion cost was already charged. - let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( - document, - document_type, - None, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )? - .into(); - let documents_batch_transition: BatchTransition = BatchTransitionV0 { - owner_id, - transitions: vec![erase_transition], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs index 73f716117ff..0c7247ce76c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs @@ -28,8 +28,7 @@ use std::slice::Iter; use crate::state_transition::batch_transition::BatchTransitionV1; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, - DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -258,54 +257,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV1 { } #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - // Erase never carries a token payment: it acts on a document whose - // deletion cost was already charged. - let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( - document, - document_type, - None, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )? - .into(); - let documents_batch_transition: BatchTransition = BatchTransitionV1 { - owner_id, - transitions: vec![BatchedTransition::Document(erase_transition)], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs new file mode 100644 index 00000000000..e85d03b799f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs @@ -0,0 +1,42 @@ +use crate::identity::SecurityLevel::{CRITICAL, HIGH, MEDIUM}; +use crate::identity::{KeyID, Purpose, SecurityLevel}; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; +use crate::state_transition::batch_transition::BatchTransitionV2; +use crate::state_transition::StateTransitionIdentitySigned; + +impl StateTransitionIdentitySigned for BatchTransitionV2 { + fn signature_public_key_id(&self) -> KeyID { + self.signature_public_key_id + } + + fn set_signature_public_key_id(&mut self, key_id: KeyID) { + self.signature_public_key_id = key_id + } + + fn security_level_requirement(&self, purpose: Purpose) -> Vec { + if purpose == Purpose::TRANSFER { + vec![CRITICAL] + } else { + // These are the available key levels that must sign the state transition + // However the fact that it is signed by one of these does not guarantee that it + // meets the security level requirement, as that is dictated from within the data + // contract + vec![CRITICAL, HIGH, MEDIUM] + } + } + + fn purpose_requirement(&self) -> Vec { + if self.transitions_len_v1() == 1 { + if let Some(first) = self.first_transition_v1() { + if first.as_transition_token_claim().is_some() + || first.as_transition_token_transfer().is_some() + { + return vec![Purpose::AUTHENTICATION, Purpose::TRANSFER]; + } + } + } + + vec![Purpose::AUTHENTICATION] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs new file mode 100644 index 00000000000..5194e165b84 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs @@ -0,0 +1,38 @@ +mod identity_signed; +mod state_transition_like; +mod types; +mod v0_methods; +mod v1_methods; +mod version; + +use crate::identity::KeyID; + +use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::PlatformSignable; + +use crate::prelude::UserFeeIncrease; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; +use platform_value::{BinaryData, Identifier}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "json-conversion", json_safe_fields)] +#[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[derive(Default)] +pub struct BatchTransitionV2 { + pub owner_id: Identifier, + pub transitions: Vec, + pub user_fee_increase: UserFeeIncrease, + #[platform_signable(exclude_from_sig_hash)] + pub signature_public_key_id: KeyID, + #[platform_signable(exclude_from_sig_hash)] + pub signature: BinaryData, +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs new file mode 100644 index 00000000000..dc2b25e5662 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs @@ -0,0 +1,100 @@ +use crate::prelude::UserFeeIncrease; +use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionV2}; +use crate::state_transition::StateTransitionHasUserFeeIncrease; +use crate::state_transition::StateTransitionType::Batch; +use crate::state_transition::{StateTransition, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, StateTransitionType}; +use crate::version::FeatureVersion; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use platform_value::{BinaryData, Identifier}; +use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; +use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; +use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; + +impl From for StateTransition { + fn from(value: BatchTransitionV2) -> Self { + let document_batch_transition: BatchTransition = value.into(); + document_batch_transition.into() + } +} + +impl StateTransitionLike for BatchTransitionV2 { + /// Returns ID of the created contract + fn modified_data_ids(&self) -> Vec { + self.transitions + .iter() + .filter_map(|t| match t { + BatchedTransitionV1::Document(document_transition) => { + Some(document_transition.base().id()) + } + BatchedTransitionV1::Token(_) => None, + }) + .collect() + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + 2 + } + /// returns the type of State Transition + fn state_transition_type(&self) -> StateTransitionType { + Batch + } + + /// We create a list of unique identifiers for the batch + fn unique_identifiers(&self) -> Vec { + self.transitions + .iter() + .map(|transition| match transition { + BatchedTransitionV1::Document(document_transition) => { + format!( + "{}-{}-{:x}", + BASE64_STANDARD.encode(self.owner_id), + BASE64_STANDARD.encode(document_transition.data_contract_id()), + document_transition.identity_contract_nonce() + ) + } + BatchedTransitionV1::Token(token_transition) => { + format!( + "{}-{}-{:x}", + BASE64_STANDARD.encode(self.owner_id), + BASE64_STANDARD.encode(token_transition.data_contract_id()), + token_transition.identity_contract_nonce() + ) + } + }) + .collect() + } +} + +impl StateTransitionHasUserFeeIncrease for BatchTransitionV2 { + fn user_fee_increase(&self) -> UserFeeIncrease { + self.user_fee_increase + } + + fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) { + self.user_fee_increase = user_fee_increase + } +} + +impl StateTransitionSingleSigned for BatchTransitionV2 { + /// returns the signature as a byte-array + fn signature(&self) -> &BinaryData { + &self.signature + } + /// set a new signature + fn set_signature(&mut self, signature: BinaryData) { + self.signature = signature + } + + fn set_signature_bytes(&mut self, signature: Vec) { + self.signature = BinaryData::new(signature) + } +} + +impl StateTransitionOwned for BatchTransitionV2 { + /// Get owner ID + fn owner_id(&self) -> Identifier { + self.owner_id + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs new file mode 100644 index 00000000000..d019af085d5 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs @@ -0,0 +1,18 @@ +use crate::state_transition::batch_transition::fields::property_names::*; +use crate::state_transition::batch_transition::fields::*; +use crate::state_transition::batch_transition::BatchTransitionV2; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for BatchTransitionV2 { + fn binary_property_paths() -> Vec<&'static str> { + vec![SIGNATURE] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![OWNER_ID] + } + + fn signature_property_paths() -> Vec<&'static str> { + vec![SIGNATURE, SIGNATURE_PUBLIC_KEY_ID] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs new file mode 100644 index 00000000000..3db2232f463 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs @@ -0,0 +1,459 @@ +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::document_type::DocumentTypeRef; +#[cfg(feature = "state-transition-signing")] +use crate::document::{Document, DocumentV0Getters}; +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::identity::signer::Signer; +use crate::prelude::IdentityNonce; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::IdentityPublicKey; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::UserFeeIncrease; +use crate::state_transition::batch_transition::batched_transition::BatchedTransition; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::batched_transition::{ + DocumentPurchaseTransition, DocumentReplaceTransition, DocumentTransferTransition, + DocumentUpdatePriceTransition, +}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; +use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use crate::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; + +use crate::state_transition::batch_transition::BatchTransitionV2; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::{ + BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, + DocumentIndexOnlyDeleteTransition, +}; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransition; +use crate::ProtocolError; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; +use crate::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; +use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::token_payment_info::TokenPaymentInfo; + +impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV2 { + #[cfg(feature = "state-transition-signing")] + async fn new_document_creation_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + entropy: [u8; 32], + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + let create_transition = DocumentCreateTransition::from_document( + document, + document_type, + entropy, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )?; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(create_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_document_replacement_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + let replace_transition = DocumentReplaceTransition::from_document( + document, + document_type, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )?; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(replace_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_document_deletion_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + // Deleting an indexOnly document is a different OPERATION from + // deleting a stored one (delete-by-values vs delete-by-id), so the + // factory picks the transition kind from the doctype's storage + // mode — every construction path, SDKs included, produces the kind + // the ABCI structure gates require, with no per-client knowledge + // of the storage layout. + let delete_transition: DocumentTransitionV1 = if document_type.index_only() { + DocumentIndexOnlyDeleteTransition::from_document( + document, + document_type, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )? + .into() + } else { + DocumentDeleteTransition::from_document( + document, + document_type, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )? + .into() + }; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(delete_transition)], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_transfer_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + recipient_owner_id: Identifier, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + let transfer_transition = DocumentTransferTransition::from_document( + document, + document_type, + token_payment_info, + identity_contract_nonce, + recipient_owner_id, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )?; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(transfer_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_document_update_price_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + price: Credits, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + let transfer_transition = DocumentUpdatePriceTransition::from_document( + document, + document_type, + price, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )?; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(transfer_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_document_purchase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + new_owner_id: Identifier, + price: Credits, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + token_payment_info: Option, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + let purchase_transition = DocumentPurchaseTransition::from_document( + document, + document_type, + price, + token_payment_info, + identity_contract_nonce, + platform_version, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + )?; + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id: new_owner_id, + transitions: vec![BatchedTransitionV1::Document(purchase_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + fn set_transitions(&mut self, transitions: Vec) { + self.transitions = transitions.into_iter().map(Into::into).collect(); + } + + fn set_identity_contract_nonce(&mut self, identity_contract_nonce: IdentityNonce) { + self.transitions + .iter_mut() + .for_each(|transition| transition.set_identity_contract_nonce(identity_contract_nonce)); + } + + fn all_conflicting_index_collateral_voting_funds( + &self, + ) -> Result, ProtocolError> { + let (total, any_voting_funds): (Option, bool) = self + .transitions + .iter() + .filter_map(|transition| { + transition + .as_transition_create() + .and_then(|document_create_transition| { + // Safely sum up values to avoid overflow. + document_create_transition + .prefunded_voting_balance() + .as_ref() + .map(|(_, credits)| *credits) + }) + }) + .fold((None, false), |(acc, _), price| match acc { + Some(acc_val) => acc_val + .checked_add(price) + .map_or((None, true), |sum| (Some(sum), true)), + None => (Some(price), true), + }); + + match (total, any_voting_funds) { + (Some(total), _) => Ok(Some(total)), + (None, true) => Err(ProtocolError::Overflow( + "overflow in all voting funds amount", + )), // Overflow occurred + _ => Ok(None), + } + } + + fn all_document_purchases_amount(&self) -> Result, ProtocolError> { + let (total, any_purchases): (Option, bool) = self + .transitions + .iter() + .filter_map(|transition| { + transition + .as_transition_purchase() + .map(|purchase| purchase.price()) + }) + .fold((None, false), |(acc, _), price| match acc { + Some(acc_val) => acc_val + .checked_add(price) + .map_or((None, true), |sum| (Some(sum), true)), + None => (Some(price), true), + }); + + match (total, any_purchases) { + (Some(total), _) => Ok(Some(total)), + (None, true) => Err(ProtocolError::Overflow("overflow in all purchases amount")), // Overflow occurred + _ => Ok(None), // No purchases were found + } + } +} + +impl DocumentsBatchTransitionMethodsV2 for BatchTransitionV2 { + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + options: Option, + platform_version: &PlatformVersion, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + let erase_transition: DocumentTransitionV1 = DocumentEraseTransition::from_document( + document, + document_type, + None, + identity_contract_nonce, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + platform_version, + )? + .into(); + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Document(erase_transition)], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs new file mode 100644 index 00000000000..7e8cc5e3bf6 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs @@ -0,0 +1,929 @@ +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::identity::signer::Signer; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::IdentityNonce; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::IdentityPublicKey; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::UserFeeIncrease; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; +use crate::state_transition::batch_transition::BatchTransitionV2; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::{TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::{TokenClaimTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenTransferTransition, TokenUnfreezeTransition}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::BatchTransition; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::StateTransition; +#[cfg(feature = "state-transition-signing")] +use crate::ProtocolError; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +#[cfg(feature = "state-transition-signing")] +use crate::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_burn_transition::TokenBurnTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_claim_transition::TokenClaimTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_config_update_transition::TokenConfigUpdateTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_direct_purchase_transition::TokenDirectPurchaseTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_emergency_action_transition::TokenEmergencyActionTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_freeze_transition::TokenFreezeTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_mint_transition::TokenMintTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_transfer_transition::TokenTransferTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_unfreeze_transition::TokenUnfreezeTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::GetDataContractSecurityLevelRequirementFn; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::emergency_action::TokenEmergencyAction; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::{PrivateEncryptedNote, SharedEncryptedNote}; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::token_pricing_schedule::TokenPricingSchedule; + +impl DocumentsBatchTransitionMethodsV1 for BatchTransitionV2 { + #[cfg(feature = "state-transition-signing")] + async fn new_token_mint_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + issued_to_identity_id: Option, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut mint_transition = TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + issued_to_identity_id, + amount, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = + mint_transition.calculate_action_id(owner_id, platform_version)?; + mint_transition.base_mut().set_using_group_info(Some( + GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + }, + )) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + mint_transition.base_mut().set_using_group_info(Some(info)) + } + } + } + + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(mint_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_burn_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut burn_transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + burn_amount: amount, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = + burn_transition.calculate_action_id(owner_id, platform_version)?; + burn_transition.base_mut().set_using_group_info(Some( + GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + }, + )) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + burn_transition.base_mut().set_using_group_info(Some(info)) + } + } + } + + // Wrap in a batch transition + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(burn_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + // Create the state transition + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + #[cfg(feature = "state-transition-signing")] + async fn new_token_transfer_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + recipient_id: Identifier, + public_note: Option, + shared_encrypted_note: Option, + private_encrypted_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + // Create the transfer transition for batch version 1 + let transfer_transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + recipient_id, + amount, + public_note, + shared_encrypted_note, + private_encrypted_note, + }); + + // Wrap in a batch transition + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(transfer_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + // Create the state transition + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_freeze_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + frozen_identity_id: Identifier, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut freeze_transition = TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + identity_to_freeze_id: frozen_identity_id, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = + freeze_transition.calculate_action_id(owner_id, platform_version)?; + freeze_transition.base_mut().set_using_group_info(Some( + GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + }, + )) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + freeze_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(freeze_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_unfreeze_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + frozen_identity_id: Identifier, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut unfreeze_transition = TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + frozen_identity_id, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = + unfreeze_transition.calculate_action_id(owner_id, platform_version)?; + unfreeze_transition.base_mut().set_using_group_info(Some( + GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + }, + )) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + unfreeze_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let documents_batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(unfreeze_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_destroy_frozen_funds_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + frozen_identity_id: Identifier, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut destroy_frozen_funds_transition = + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + frozen_identity_id, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = destroy_frozen_funds_transition + .calculate_action_id(owner_id, platform_version)?; + destroy_frozen_funds_transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + destroy_frozen_funds_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token( + destroy_frozen_funds_transition.into(), + )], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_emergency_action_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + emergency_action: TokenEmergencyAction, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut emergency_action_transition = + TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + emergency_action, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = emergency_action_transition + .calculate_action_id(owner_id, platform_version)?; + emergency_action_transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + emergency_action_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token( + emergency_action_transition.into(), + )], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_config_update_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + update_token_configuration_item: TokenConfigurationChangeItem, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut config_update_transition = + TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + update_token_configuration_item, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = + config_update_transition.calculate_action_id(owner_id, platform_version)?; + config_update_transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + config_update_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(config_update_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_claim_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + distribution_type: TokenDistributionType, + public_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let claim_transition = TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + distribution_type, + public_note, + }); + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token(claim_transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_change_direct_purchase_price_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + token_pricing_schedule: Option, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let mut change_direct_purchase_price_transition = + TokenSetPriceForDirectPurchaseTransition::V0( + TokenSetPriceForDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + price: token_pricing_schedule, + public_note, + }, + ); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = change_direct_purchase_price_transition + .calculate_action_id(owner_id, platform_version)?; + change_direct_purchase_price_transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + change_direct_purchase_price_transition + .base_mut() + .set_using_group_info(Some(info)) + } + } + } + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token( + change_direct_purchase_price_transition.into(), + )], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_direct_purchase_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + total_agreed_price: Credits, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let direct_purchase_transition = + TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + token_count: amount, + total_agreed_price, + }); + + let batch_transition: BatchTransition = BatchTransitionV2 { + owner_id, + transitions: vec![BatchedTransitionV1::Token( + direct_purchase_transition.into(), + )], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + Ok(state_transition) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs new file mode 100644 index 00000000000..b52ddd83a0a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::batch_transition::BatchTransitionV2; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for BatchTransitionV2 { + fn feature_version(&self) -> FeatureVersion { + 2 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs index b2f89c158d7..0fe196a2db3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs @@ -4,6 +4,7 @@ use crate::ProtocolError; use platform_version::version::PlatformVersion; mod v0; +mod v1; impl BatchTransition { pub fn validate_base_structure( @@ -19,9 +20,10 @@ impl BatchTransition { .validate_base_structure { 0 => self.validate_base_structure_v0(platform_version), + 1 => self.validate_base_structure_v1(platform_version), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::validate_base_structure".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 098d86e0142..83887155a60 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -4,9 +4,7 @@ use crate::consensus::basic::document::{ }; use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; use crate::consensus::basic::BasicError; -use crate::state_transition::batch_transition::batched_transition::{ - DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, -}; +use crate::state_transition::batch_transition::batched_transition::DocumentIndexOnlyDeleteTransition; use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; @@ -143,41 +141,6 @@ impl BatchTransition { Some(_) => {} } } - - // The erase kind joined the wire at protocol version 14. Old - // software cannot decode it at all, so no historical block can - // contain one — this check exists so that new software agrees - // with old software while a pre-14 protocol version is still - // active. - if let DocumentTransition::Erase(erase) = transition { - let feature_version = match erase { - DocumentEraseTransition::V0(_) => 0, - }; - match &platform_version - .dpp - .state_transition_serialization_versions - .document_erase_state_transition - { - None => { - // The kind does not exist at this protocol - // version; the empty supported range (min 1, max 0) - // states exactly that. - result.add_error(BasicError::UnsupportedVersionError( - UnsupportedVersionError::new(feature_version, 1, 0), - )); - } - Some(bounds) if !bounds.bounds.check_version(feature_version) => { - result.add_error(BasicError::UnsupportedVersionError( - UnsupportedVersionError::new( - feature_version, - bounds.bounds.min_version, - bounds.bounds.max_version, - ), - )); - } - Some(_) => {} - } - } } // Make sure we don't have duplicate transitions @@ -376,51 +339,6 @@ mod tests { /// (`document_index_only_delete_state_transition` is `Some` in /// STATE_TRANSITION_SERIALIZATION_VERSIONS_V3), rejected below (where /// the kind's table entry is `None`). - /// Software that predates the erase kind cannot decode one at all, so no - /// historical block can contain one. The gate exists so that new software - /// agrees with old software while a pre-14 protocol version is still - /// active. - #[test] - fn validate_base_structure_v0_gates_erase_by_protocol_version() { - use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; - use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; - - let erase = - DocumentTransition::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { - base: make_base(1, "note"), - })); - - let batch = make_batch_v0(vec![erase]); - - for protocol in [12, 13] { - let version = PlatformVersion::get(protocol).expect("the protocol version exists"); - let result = batch - .validate_base_structure_v0(version) - .expect("no protocol err"); - assert!( - result.errors.iter().any(|error| matches!( - error, - ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) - )), - "protocol {protocol} must reject an erase as an unsupported version, got {:?}", - result.errors - ); - } - - let pv14 = PlatformVersion::get(14).expect("PV14 exists"); - let result = batch - .validate_base_structure_v0(pv14) - .expect("no protocol err"); - assert!( - !result.errors.iter().any(|error| matches!( - error, - ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) - )), - "PV14 must admit an erase, got {:?}", - result.errors - ); - } - #[test] fn validate_base_structure_v0_gates_index_only_delete_by_protocol_version() { use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::DocumentIndexOnlyDeleteTransitionV0; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs new file mode 100644 index 00000000000..fad5f49c777 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs @@ -0,0 +1,578 @@ +use crate::consensus::basic::document::{ + DocumentTransitionsAreAbsentError, DuplicateDocumentTransitionsWithIdsError, + MaxDocumentsTransitionsExceededError, NonceOutOfBoundsError, +}; +use crate::consensus::basic::BasicError; + +use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use crate::state_transition::batch_transition::BatchTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; +use crate::consensus::basic::group::GroupActionNotAllowedOnTransitionError; +use crate::consensus::basic::token::{InvalidActionIdError, InvalidTokenIdError}; +use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransitionRefV1, DocumentTransitionRefV1, +}; +use crate::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionTypeGetter; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_config_update_transition::validate_structure::TokenConfigUpdateTransitionStructureValidation; +use crate::state_transition::batch_transition::token_destroy_frozen_funds_transition::validate_structure::TokenDestroyFrozenFundsTransitionStructureValidation; +use crate::state_transition::batch_transition::token_emergency_action_transition::validate_structure::TokenEmergencyActionTransitionStructureValidation; +use crate::state_transition::batch_transition::token_freeze_transition::validate_structure::TokenFreezeTransitionStructureValidation; +use crate::state_transition::batch_transition::token_mint_transition::validate_structure::TokenMintTransitionStructureValidation; +use crate::state_transition::batch_transition::token_claim_transition::validate_structure::TokenClaimTransitionStructureValidation; +use crate::state_transition::batch_transition::token_direct_purchase_transition::validate_structure::TokenDirectPurchaseTransitionStructureValidation; +use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::validate_structure::TokenSetPriceForDirectPurchaseTransitionStructureValidation; +use crate::state_transition::batch_transition::token_transfer_transition::validate_structure::TokenTransferTransitionStructureValidation; +use crate::state_transition::batch_transition::token_unfreeze_transition::validate_structure::TokenUnfreezeTransitionStructureValidation; +use crate::state_transition::state_transitions::document::batch_transition::batched_transition::token_burn_transition::validate_structure::TokenBurnTransitionStructureValidation; +use crate::state_transition::StateTransitionOwned; + +impl BatchTransition { + #[inline(always)] + pub(super) fn validate_base_structure_v1( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.transitions_are_empty_v1() { + return Ok(SimpleConsensusValidationResult::new_with_error( + DocumentTransitionsAreAbsentError::new().into(), + )); + } + + let transitions_len = self.transitions_len_v1(); + + if transitions_len > u16::MAX as usize + || transitions_len as u16 + > platform_version + .system_limits + .max_transitions_in_documents_batch + { + return Ok(SimpleConsensusValidationResult::new_with_error( + MaxDocumentsTransitionsExceededError::new( + platform_version + .system_limits + .max_transitions_in_documents_batch, + ) + .into(), + )); + } + + // Group transitions by contract ID + let mut document_transitions_by_contracts: BTreeMap< + Identifier, + Vec, + > = BTreeMap::new(); + + // Group transitions by contract ID + let mut token_transitions: Vec<&TokenTransition> = vec![]; + + self.transitions_iter_v1() + .for_each(|batch_transition| match batch_transition { + BatchedTransitionRefV1::Document(document_transition) => { + let contract_identifier = document_transition.data_contract_id(); + + match document_transitions_by_contracts.entry(contract_identifier) { + Entry::Vacant(vacant) => { + vacant.insert(vec![document_transition]); + } + Entry::Occupied(mut identifiers) => { + identifiers.get_mut().push(document_transition); + } + }; + } + BatchedTransitionRefV1::Token(token_transition) => { + token_transitions.push(token_transition) + } + }); + + let mut result = SimpleConsensusValidationResult::default(); + + for transitions in document_transitions_by_contracts.values() { + for transition in transitions { + // We need to make sure that the identity contract nonce is within the allowed bounds + // This means that it is stored on 40 bits + if transition.identity_contract_nonce() & MISSING_IDENTITY_REVISIONS_FILTER > 0 { + result.add_error(BasicError::NonceOutOfBoundsError( + NonceOutOfBoundsError::new(transition.identity_contract_nonce()), + )); + } + } + + // Make sure we don't have duplicate transitions + let duplicate_transitions = find_duplicates_by_id(transitions); + + if !duplicate_transitions.is_empty() { + let references: Vec<(String, [u8; 32])> = duplicate_transitions + .into_iter() + .map(|transition| { + Ok(( + transition.base().document_type_name().clone(), + transition.base().id().to_buffer(), + )) + }) + .collect::, anyhow::Error>>()?; + + result.add_error(BasicError::DuplicateDocumentTransitionsWithIdsError( + DuplicateDocumentTransitionsWithIdsError::new(references), + )); + } + } + + for transition in token_transitions { + // We need to make sure that the identity contract nonce is within the allowed bounds + // This means that it is stored on 40 bits + if transition.identity_contract_nonce() & MISSING_IDENTITY_REVISIONS_FILTER > 0 { + result.add_error(BasicError::NonceOutOfBoundsError( + NonceOutOfBoundsError::new(transition.identity_contract_nonce()), + )); + } + + let transition_token_id = transition.base().token_id(); + let calculated_token_id = transition.base().calculate_token_id(); + + // We need to verify that the token id is correct + if transition_token_id != calculated_token_id { + result.add_error(BasicError::InvalidTokenIdError(InvalidTokenIdError::new( + calculated_token_id, + transition_token_id, + ))); + } + + let consensus_result = match transition { + TokenTransition::Burn(burn_transition) => { + burn_transition.validate_structure(platform_version)? + } + TokenTransition::Mint(mint_transition) => { + mint_transition.validate_structure(platform_version)? + } + TokenTransition::Transfer(transfer_transition) => { + transfer_transition.validate_structure(self.owner_id(), platform_version)? + } + TokenTransition::Freeze(freeze_transition) => { + freeze_transition.validate_structure(platform_version)? + } + TokenTransition::Unfreeze(unfreeze_transition) => { + unfreeze_transition.validate_structure(platform_version)? + } + TokenTransition::DestroyFrozenFunds(destroy_frozen_funds_transition) => { + destroy_frozen_funds_transition.validate_structure(platform_version)? + } + TokenTransition::EmergencyAction(emergency_action_transition) => { + emergency_action_transition.validate_structure(platform_version)? + } + TokenTransition::ConfigUpdate(config_update_transition) => { + config_update_transition.validate_structure(platform_version)? + } + TokenTransition::Claim(release_transition) => { + release_transition.validate_structure(platform_version)? + } + TokenTransition::DirectPurchase(direct_purchase_transition) => { + direct_purchase_transition.validate_structure(platform_version)? + } + TokenTransition::SetPriceForDirectPurchase( + set_price_for_direct_purchase_transition, + ) => { + set_price_for_direct_purchase_transition.validate_structure(platform_version)? + } + }; + + if !consensus_result.is_valid() { + return Ok(consensus_result); + } + + // We need to verify that the action id given matches the expected action id + // But only if we are the proposer + if let Some(group_state_transition_info) = transition.base().using_group_info() { + if group_state_transition_info.action_is_proposer { + if let Some(calculated_action_id) = transition + .calculate_action_id(self.owner_id(), platform_version) + .transpose()? + { + if group_state_transition_info.action_id != calculated_action_id { + result.add_error(BasicError::InvalidActionIdError( + InvalidActionIdError::new( + calculated_action_id, + group_state_transition_info.action_id, + ), + )); + } + } else { + result.add_error(BasicError::GroupActionNotAllowedOnTransitionError( + GroupActionNotAllowedOnTransitionError::new( + transition.action_type().to_string(), + ), + )); + } + } else if !transition.can_calculate_action_id() { + result.add_error(BasicError::GroupActionNotAllowedOnTransitionError( + GroupActionNotAllowedOnTransitionError::new( + transition.action_type().to_string(), + ), + )); + } + } + } + + Ok(result) + } +} + +#[derive(Hash, Eq, PartialEq, Ord, PartialOrd)] +struct TransitionFingerprint<'a> { + document_type: &'a str, + id: Identifier, +} + +impl<'a> From> for TransitionFingerprint<'a> { + fn from(transition: DocumentTransitionRefV1<'a>) -> Self { + let base = transition.base(); + + Self { + document_type: base.document_type_name(), + id: base.id(), + } + } +} + +/// Finds the duplicates among the document transitions of one contract. Two +/// transitions are duplicates when they name the same document type and +/// document id, whatever their kinds. Every duplicated transition is reported, +/// the first one once per later duplicate. +fn find_duplicates_by_id<'a>( + document_transitions: &[DocumentTransitionRefV1<'a>], +) -> Vec> { + let mut fingerprints: BTreeMap = + BTreeMap::new(); + let mut duplicates: Vec = vec![]; + + for transition in document_transitions { + let fingerprint = (*transition).into(); + + match fingerprints.entry(fingerprint) { + Entry::Occupied(val) => { + duplicates.push(*val.get()); + duplicates.push(*transition); + } + Entry::Vacant(v) => { + v.insert(*transition); + } + } + } + + duplicates +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::v0::DocumentCreateTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransition; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::v0::DocumentDeleteTransitionV0; + use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransition; + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionV1, DocumentEraseTransition, DocumentTransition, + DocumentTransitionV1, + }; + use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use crate::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, + }; + use platform_value::BinaryData; + use std::collections::BTreeMap; + + fn make_base(nonce: u64, type_name: &str) -> DocumentBaseTransition { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([1u8; 32]), + identity_contract_nonce: nonce, + document_type_name: type_name.to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }) + } + + fn make_create(nonce: u64) -> DocumentTransition { + DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base: make_base(nonce, "test_doc"), + entropy: [0u8; 32], + data: BTreeMap::new(), + prefunded_voting_balance: None, + })) + } + + fn make_delete(nonce: u64, id_byte: u8) -> DocumentTransition { + DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([id_byte; 32]), + identity_contract_nonce: nonce, + document_type_name: "test_doc".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + })) + } + + fn make_batch_v0(transitions: Vec) -> BatchTransition { + BatchTransition::V0(BatchTransitionV0 { + owner_id: Identifier::new([0x01; 32]), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }) + } + + fn make_batch_v1_empty() -> BatchTransition { + BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::new([0x02; 32]), + transitions: vec![], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }) + } + + // ----------------------------------------------------------------------- + // batch format 2 — the only format whose shell carries the erase kind + // ----------------------------------------------------------------------- + + fn make_erase(id_byte: u8, nonce: u64) -> DocumentTransitionV1 { + DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([id_byte; 32]), + identity_contract_nonce: nonce, + document_type_name: "note".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + })) + } + + fn make_batch_v2(transitions: Vec) -> BatchTransition { + BatchTransition::V2(BatchTransitionV2 { + owner_id: Identifier::new([0x03; 32]), + transitions: transitions + .into_iter() + .map(BatchedTransitionV1::Document) + .collect(), + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }) + } + + #[test] + fn should_admit_an_erase_in_a_format_2_batch() { + let result = make_batch_v2(vec![make_erase(1, 1)]) + .validate_base_structure_v1(PlatformVersion::latest()) + .expect("no protocol err"); + + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } + + #[test] + fn should_treat_an_erase_of_a_document_named_by_another_transition_as_a_duplicate() { + let first = make_erase(7, 1); + let second = DocumentTransitionV1::Create(DocumentCreateTransition::V0( + DocumentCreateTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([7; 32]), + identity_contract_nonce: 2, + document_type_name: "note".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + entropy: [0u8; 32], + data: BTreeMap::new(), + prefunded_voting_balance: None, + }, + )); + let other_type = make_create(3); + + let duplicates = find_duplicates_by_id(&[ + first.borrow_as_ref(), + second.borrow_as_ref(), + DocumentTransitionRefV1::from(&other_type), + ]); + + assert_eq!( + duplicates.len(), + 2, + "a batch may name one document at most once, whatever the kinds" + ); + assert!(find_duplicates_by_id(&[]).is_empty()); + } + + #[test] + fn should_reject_an_empty_format_2_batch() { + let result = make_batch_v2(vec![]) + .validate_base_structure_v1(PlatformVersion::latest()) + .expect("no protocol err"); + + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::DocumentTransitionsAreAbsentError(_) + )) + )); + } + + #[test] + fn should_bound_the_nonce_of_an_erase_in_a_format_2_batch() { + let result = make_batch_v2(vec![make_erase(1, u64::MAX)]) + .validate_base_structure_v1(PlatformVersion::latest()) + .expect("no protocol err"); + + assert!( + result.errors.iter().any(|error| matches!( + error, + ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) + )), + "expected NonceOutOfBoundsError, got {:?}", + result.errors + ); + } + + // ----------------------------------------------------------------------- + // empty batch — DocumentTransitionsAreAbsentError + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v1_errors_when_v0_empty() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v0(vec![]); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + let errors = result.errors; + assert_eq!(errors.len(), 1); + match &errors[0] { + ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} + other => panic!( + "expected DocumentTransitionsAreAbsentError, got {:?}", + other + ), + } + } + + #[test] + fn validate_base_structure_v1_errors_when_v1_empty() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v1_empty(); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + match &result.errors[0] { + ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} + other => panic!( + "expected DocumentTransitionsAreAbsentError, got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // valid single transition with no group / nonce / duplicate issues + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v1_passes_with_single_valid_transition() { + let pv = PlatformVersion::latest(); + let batch = make_batch_v0(vec![make_create(1)]); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } + + // ----------------------------------------------------------------------- + // nonce out of bounds — high bits set above 40-bit cap + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v1_errors_on_nonce_with_high_bits_set() { + let pv = PlatformVersion::latest(); + // MISSING_IDENTITY_REVISIONS_FILTER masks the top bits used to mark + // a missing revision. Setting a value above 40 bits triggers the + // NonceOutOfBoundsError path. + let bad_nonce: u64 = u64::MAX; + let batch = make_batch_v0(vec![make_delete(bad_nonce, 7)]); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + let has_nonce_err = result.errors.iter().any(|e| { + matches!( + e, + ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) + ) + }); + assert!( + has_nonce_err, + "expected NonceOutOfBoundsError, got {:?}", + result.errors + ); + } + + // ----------------------------------------------------------------------- + // max-transitions-exceeded — early-return before any other checks + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v1_errors_when_transitions_exceed_max() { + // The latest platform version caps the batch at a small number of + // transitions. Going over should produce a single + // MaxDocumentsTransitionsExceededError and NO other errors (the + // function early-returns). + let pv = PlatformVersion::latest(); + let max = pv.system_limits.max_transitions_in_documents_batch as usize; + let mut transitions = Vec::with_capacity(max + 1); + // Use distinct ids to avoid duplicate noise — but we never reach the + // duplicate-detection path anyway because we early-return. + for i in 0..(max + 1) { + transitions.push(make_delete(i as u64 + 1, i as u8)); + } + let batch = make_batch_v0(transitions); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(!result.is_valid()); + assert_eq!(result.errors.len(), 1, "should early-return with one error"); + match &result.errors[0] { + ConsensusError::BasicError(BasicError::MaxDocumentsTransitionsExceededError(_)) => {} + other => panic!( + "expected MaxDocumentsTransitionsExceededError, got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // V1 batch with only document transitions — same logic, takes the + // BatchedTransitionRef::Document arm of the iterator instead. + // ----------------------------------------------------------------------- + + #[test] + fn validate_base_structure_v1_passes_for_v1_with_documents_only() { + let pv = PlatformVersion::latest(); + let batch = BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::new([0x02; 32]), + transitions: vec![BatchedTransition::Document(make_create(1))], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + }); + let result = batch + .validate_base_structure_v1(pv) + .expect("no protocol err"); + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs index be7d07021e1..ed85796ecb7 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs @@ -7,6 +7,7 @@ impl FeatureVersioned for BatchTransition { match self { BatchTransition::V0(v0) => v0.feature_version(), BatchTransition::V1(v1) => v1.feature_version(), + BatchTransition::V2(v2) => v2.feature_version(), } } } diff --git a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs index 86e0015c68f..721af904313 100644 --- a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs @@ -290,6 +290,27 @@ impl ExecutionEvent<'_> { ))) } } + StateTransitionAction::BatchActionV1(batch_action) => { + let user_fee_increase = action.user_fee_increase(); + let removed_balance = batch_action.all_used_balances()?; + let operations = + action.into_high_level_drive_operations(epoch, platform_version)?; + if let Some(identity) = identity { + Ok(ExecutionEvent::Paid { + identity, + removed_balance, + added_to_balance_outputs: None, + operations, + execution_operations: execution_context.operations_consume(), + additional_fixed_fee_cost: None, + user_fee_increase, + }) + } else { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "partial identity should be present for other state transitions", + ))) + } + } StateTransitionAction::MasternodeVoteAction(_) => { let operations = action.into_high_level_drive_operations(epoch, platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs index 782dd41052f..1da1751ecca 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs @@ -22,9 +22,8 @@ use crate::execution::types::state_transition_execution_context::{ }; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::DocumentReferenceValidation; -use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::{ - fetch_keep_history_document_lifecycle, has_contested_document_with_document_id, -}; +use crate::execution::validation::state_transition::batch::state::fetch_keep_history_document_lifecycle::fetch_keep_history_document_lifecycle; +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::has_contested_document_with_document_id; use crate::platform_types::platform::PlatformStateRef; pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStateValidationV2 diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs index 79fff775a3d..f3de337bb2d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v1/mod.rs @@ -2,7 +2,7 @@ use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::state_v0::DocumentDeleteTransitionActionStateValidationV0; -use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_keep_history_document_lifecycle; +use crate::execution::validation::state_transition::batch::state::fetch_keep_history_document_lifecycle::fetch_keep_history_document_lifecycle; use crate::platform_types::platform::PlatformStateRef; use dpp::block::block_info::BlockInfo; use dpp::consensus::basic::document::InvalidDocumentTypeError; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs index 2fac85c999b..a98788c0d28 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/mod.rs @@ -43,12 +43,16 @@ impl DocumentEraseTransitionActionValidation for DocumentEraseTransitionAction { .batch_state_transition .document_erase_transition_structure_validation { - 0 => self.validate_structure_v0(), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(0) => self.validate_structure_v0(), + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentEraseTransitionAction::validate_structure".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "DocumentEraseTransitionAction::validate_structure".to_string(), + known_versions: vec![0], + })), } } @@ -68,7 +72,7 @@ impl DocumentEraseTransitionActionValidation for DocumentEraseTransitionAction { .batch_state_transition .document_erase_transition_state_validation { - 0 => self.validate_state_v0( + Some(0) => self.validate_state_v0( platform, owner_id, block_info, @@ -76,11 +80,15 @@ impl DocumentEraseTransitionActionValidation for DocumentEraseTransitionAction { transaction, platform_version, ), - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentEraseTransitionAction::validate_state".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "DocumentEraseTransitionAction::validate_state".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs index a719aec67a8..6285f88b77c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/state_v0/mod.rs @@ -4,7 +4,7 @@ use crate::execution::types::state_transition_execution_context::{ StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, }; use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::DocumentBaseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_keep_history_document_lifecycle; +use crate::execution::validation::state_transition::batch::state::fetch_keep_history_document_lifecycle::fetch_keep_history_document_lifecycle; use crate::platform_types::platform::PlatformStateRef; use dpp::block::block_info::BlockInfo; use dpp::consensus::basic::document::{ 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 f5c818b6b3e..9ca6a929874 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 @@ -252,14 +252,10 @@ fn validate_document_type_references_v0( )); }; - // Only document types whose documents can never be deleted may - // be referenced. A contract update may withdraw deletion but - // never grant it, and document types can not be removed, so a - // type that is referenceable when a reference is validated - // stays referenceable and the reference can never dangle. - // Erasure is out of the question for the same reason: it - // applies to deleted documents only, and a type that allows it - // must allow deletion. + // Only document types whose documents can never be deleted may be + // referenced: `canBeDeleted` is immutable on contract updates and + // document types can not be removed, so a reference validated here + // can never dangle if referenced_document_type.documents_can_be_deleted() { return Ok(SimpleConsensusValidationResult::new_with_error( ReferencedDocumentTypeDeletableError::new( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs index 0fc65514003..0eafc05f797 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs @@ -20,13 +20,11 @@ use dpp::version::PlatformVersion; use drive::state_transition_action::batch::BatchTransitionAction; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionActionAccessorsV0; @@ -219,19 +217,6 @@ impl DocumentsBatchStateTransitionStructureValidationV0 for BatchTransition { )); } } - DocumentTransitionAction::EraseAction(erase_action) => { - let result = erase_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(erase_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { let result = index_only_delete_action.validate_structure(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs new file mode 100644 index 00000000000..600f8ed8730 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs @@ -0,0 +1,284 @@ +use crate::error::Error; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::InvalidDocumentTransitionIdError; +use dpp::consensus::signature::{InvalidSignaturePublicKeySecurityLevelError, SignatureError}; +use dpp::dashcore::Network; +use dpp::document::Document; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::PartialIdentity; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionIdentitySigned, StateTransitionOwned}; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRefV1, DocumentTransitionRefV1}; +use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use dpp::validation::ConsensusValidationResult; + +use dpp::version::PlatformVersion; + +use drive::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; +use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::DocumentTransferTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::DocumentUpdatePriceTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use drive::state_transition_action::StateTransitionAction; +use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use crate::error::execution::ExecutionError; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; +use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_transfer_transition_action::DocumentTransferTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_update_price_transition_action::DocumentUpdatePriceTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; + +/// Generation 1 validates the structure of batch action format 1, which is +/// what a batch of any wire format becomes for protocol version 14 and later, +/// including the erase of a keep-history document. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStructureValidationV1 +{ + fn validate_advanced_structure_from_state_v1( + &self, + block_info: &BlockInfo, + network: Network, + action: &BatchTransitionActionV1, + identity: &PartialIdentity, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl DocumentsBatchStateTransitionStructureValidationV1 for BatchTransition { + fn validate_advanced_structure_from_state_v1( + &self, + block_info: &BlockInfo, + network: Network, + action: &BatchTransitionActionV1, + identity: &PartialIdentity, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let security_levels = action.combined_security_level_requirement()?; + + let signing_key = identity.loaded_public_keys.get(&self.signature_public_key_id()).ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution("the key must exist for advanced structure validation as we already fetched it during signature validation")))?; + + if !security_levels.contains(&signing_key.security_level()) { + // We only need to bump the first identity data contract nonce as that will make a replay + // attack not possible + + let first_transition = self.first_transition_v1().ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution("There must be at least one state transition as this is already verified in basic validation")))?; + + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_batched_transition_ref_v1( + first_transition, + self.owner_id(), + self.user_fee_increase(), + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![SignatureError::InvalidSignaturePublicKeySecurityLevelError( + InvalidSignaturePublicKeySecurityLevelError::new( + signing_key.security_level(), + security_levels, + ), + ) + .into()], + )); + } + + // We should validate that all newly created documents have valid ids + for transition in self.transitions_iter_v1() { + if let BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Create( + create_transition, + )) = transition + { + // Validate the ID + let generated_document_id = Document::generate_document_id_v0( + create_transition.base().data_contract_id_ref(), + &self.owner_id(), + create_transition.base().document_type_name(), + &create_transition.entropy(), + ); + + // This hash will take 2 blocks (128 bytes) + execution_context.add_operation(ValidationOperation::DoubleSha256(2)); + + let id = create_transition.base().id(); + if generated_document_id != id { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( + create_transition.base(), + self.owner_id(), + self.user_fee_increase(), + ), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + vec![ + InvalidDocumentTransitionIdError::new(generated_document_id, id).into(), + ], + )); + } + } + } + + // Next we need to validate the structure of all actions (this means with the data contract) + for transition in action.transitions() { + match transition { + BatchedTransitionActionV1::DocumentErase(erase_action) => { + let result = erase_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(erase_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( + document_action, + )) => match document_action { + DocumentTransitionAction::CreateAction(create_action) => { + let result = create_action.validate_structure( + identity.id, + block_info, + network, + platform_version, + )?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(document_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::ReplaceAction(replace_action) => { + let result = replace_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(replace_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::DeleteAction(delete_action) => { + let result = delete_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(delete_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::TransferAction(transfer_action) => { + let result = transfer_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(transfer_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::UpdatePriceAction(update_price_action) => { + let result = update_price_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(update_price_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::PurchaseAction(purchase_action) => { + let result = purchase_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(purchase_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { + let result = + index_only_delete_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(index_only_delete_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + }, + BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( + token_transition_action, + )) => { + // token actions only need to do advanced structure validation on the base action + let result = token_transition_action + .base() + .validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition_action(token_transition_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(_), + ) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "we should not have a bump identity contract nonce at this stage", + ))); + } + } + } + Ok(ConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs new file mode 100644 index 00000000000..679be90040e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs @@ -0,0 +1,67 @@ +use crate::error::Error; +use dpp::block::block_info::BlockInfo; +use dpp::identity::identity_nonce::{validate_identity_nonce_update, validate_new_identity_nonce}; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; + +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::StateTransitionOwned; +use dpp::validation::SimpleConsensusValidationResult; + +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::platform_types::platform::PlatformStateRef; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; + +/// Generation 1 reads the identity contract nonces of a batch of any wire +/// format, including format 2, whose document shell carries the erase kind. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionIdentityContractNonceV1 +{ + fn validate_identity_contract_nonces_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + tx: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentsBatchStateTransitionIdentityContractNonceV1 for BatchTransition { + fn validate_identity_contract_nonces_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + tx: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result { + for transition in self.transitions_iter_v1() { + let revision_nonce = transition.identity_contract_nonce(); + let identity_id = self.owner_id(); + let (existing_nonce, fee) = platform.drive.fetch_identity_contract_nonce_with_fees( + identity_id.to_buffer(), + transition.data_contract_id().to_buffer(), + block_info, + true, + tx, + platform_version, + )?; + + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + + let result = if let Some(existing_nonce) = existing_nonce { + validate_identity_nonce_update(existing_nonce, revision_nonce, identity_id) + } else { + validate_new_identity_nonce(revision_nonce, identity_id) + }; + if !result.is_valid() { + return Ok(result); + } + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs index 9b55207150b..68637971e6e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs @@ -7,6 +7,7 @@ use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; mod v0; +mod v1; impl StateTransitionIsAllowedValidationV0 for BatchTransition { fn has_is_allowed_validation(&self) -> Result { @@ -27,9 +28,10 @@ impl StateTransitionIsAllowedValidationV0 for BatchTransition { .is_allowed { 0 => Ok(v0::validate_is_allowed_v0(self, platform)), + 1 => Ok(v1::validate_is_allowed_v1(self, platform)), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "StateTransition::validate_is_allowed".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs new file mode 100644 index 00000000000..12a1ef487b2 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs @@ -0,0 +1,53 @@ +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use dpp::consensus::basic::document::ContestedDocumentsTemporarilyNotAllowedError; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use dpp::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::validation::ConsensusValidationResult; + +use crate::execution::validation::state_transition::batch::is_allowed::v0::TARGET_EPOCH_INDEX; + +/// Generation 1 reads a batch of any wire format, including format 2, whose +/// document shell carries the erase kind. The rule is unchanged: contested +/// document creation is not allowed before the target epoch. +#[inline(always)] +pub fn validate_is_allowed_v1( + state_transition: &BatchTransition, + platform: &PlatformRef, +) -> ConsensusValidationResult<()> { + #[cfg(feature = "testing-config")] + if platform + .config + .testing_configs + .disable_contested_documents_is_allowed_validation + { + return ConsensusValidationResult::new(); + } + + let block_info = platform.state.last_block_info(); + + if block_info.epoch.index >= TARGET_EPOCH_INDEX { + return ConsensusValidationResult::new(); + } + + let is_contested = state_transition.transitions_iter_v1().any(|transition| { + transition + .as_transition_create() + .and_then(|create| create.prefunded_voting_balance().as_ref()) + .is_some() + }); + + if is_contested { + return ConsensusValidationResult::new_with_errors(vec![ + ContestedDocumentsTemporarilyNotAllowedError::new( + block_info.epoch.index, + TARGET_EPOCH_INDEX, + ) + .into(), + ]); + } + + ConsensusValidationResult::new() +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index c1fef559bd1..ac4f6bd3341 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -31,9 +31,13 @@ use crate::platform_types::platform::{PlatformRef, PlatformStateRef}; use crate::rpc::core::CoreRPCLike; use crate::execution::validation::state_transition::batch::advanced_structure::v0::DocumentsBatchStateTransitionStructureValidationV0; +use crate::execution::validation::state_transition::batch::advanced_structure::v1::DocumentsBatchStateTransitionStructureValidationV1; use crate::execution::validation::state_transition::batch::identity_contract_nonce::v0::DocumentsBatchStateTransitionIdentityContractNonceV0; +use crate::execution::validation::state_transition::batch::identity_contract_nonce::v1::DocumentsBatchStateTransitionIdentityContractNonceV1; use crate::execution::validation::state_transition::batch::state::v0::DocumentsBatchStateTransitionStateValidationV0; use crate::execution::validation::state_transition::batch::state::v1::DocumentsBatchStateTransitionStateValidationV1; +use crate::execution::validation::state_transition::batch::state::v2::DocumentsBatchStateTransitionActionTransformV2; +use crate::execution::validation::state_transition::batch::state::validation_v1::DocumentsBatchStateTransitionValidationV1; use crate::execution::validation::state_transition::processor::advanced_structure_with_state::StateTransitionStructureKnownInStateValidationV0; use crate::execution::validation::state_transition::processor::basic_structure::StateTransitionBasicStructureValidationV0; use crate::execution::validation::state_transition::processor::identity_nonces::StateTransitionIdentityNonceValidationV0; @@ -90,9 +94,16 @@ impl StateTransitionActionTransformer for BatchTransition { execution_context, tx, ), + 2 => self.transform_into_action_v2( + &platform.into(), + block_info, + validation_mode, + execution_context, + tx, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: transform_into_action".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } @@ -149,9 +160,16 @@ impl StateTransitionIdentityNonceValidationV0 for BatchTransition { execution_context, platform_version, ), + 1 => self.validate_identity_contract_nonces_v1( + platform, + block_info, + tx, + execution_context, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: revision".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -195,9 +213,30 @@ impl StateTransitionStructureKnownInStateValidationV0 for BatchTransition { platform_version, ) } + 1 => { + let identity = + identity.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "The identity must be known on advanced structure validation", + )))?; + let StateTransitionAction::BatchActionV1(documents_batch_transition_action) = + action + else { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "action must be a documents batch transition action in format 1", + ))); + }; + self.validate_advanced_structure_from_state_v1( + block_info, + network, + documents_batch_transition_action, + identity, + execution_context, + platform_version, + ) + } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: advanced structure from state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -251,9 +290,30 @@ impl StateTransitionStateValidation for BatchTransition { platform_version, ) } + 1 => { + let action = + action.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "documents batch structure validation should have an action", + )))?; + let StateTransitionAction::BatchActionV1(documents_batch_transition_action) = + action + else { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "action must be a documents batch transition action in format 1", + ))); + }; + self.validate_state_v1( + documents_batch_transition_action, + &platform.into(), + block_info, + execution_context, + tx, + platform_version, + ) + } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/mod.rs new file mode 100644 index 00000000000..610e51097fc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/mod.rs @@ -0,0 +1,56 @@ +mod v0; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use dpp::block::epoch::Epoch; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; + +use self::v0::fetch_keep_history_document_lifecycle_v0; + +/// Classifies a keep-history document and bills the reads it performs. +#[allow(clippy::too_many_arguments)] +pub(crate) fn fetch_keep_history_document_lifecycle( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_keep_history_document_lifecycle + { + Some(0) => fetch_keep_history_document_lifecycle_v0( + drive, + contract, + document_type, + id, + epoch, + execution_context, + transaction, + platform_version, + ), + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "fetch_keep_history_document_lifecycle".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "fetch_keep_history_document_lifecycle".to_string(), + known_versions: vec![0], + })), + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/v0/mod.rs new file mode 100644 index 00000000000..5a22011a383 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/fetch_keep_history_document_lifecycle/v0/mod.rs @@ -0,0 +1,38 @@ +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use dpp::block::epoch::Epoch; +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; + +#[allow(clippy::too_many_arguments)] +pub(super) fn fetch_keep_history_document_lifecycle_v0( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + let (state, fee_result) = drive + .fetch_document_lifecycle( + contract, + document_type, + id, + Some(epoch), + transaction, + platform_version, + ) + .map_err(Error::Drive)?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(state) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs index 008be12cc67..71c5e28a336 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs @@ -1,2 +1,5 @@ +pub(crate) mod fetch_keep_history_document_lifecycle; pub(crate) mod v0; pub(crate) mod v1; +pub(crate) mod v2; +pub(crate) mod validation_v1; 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 96708df49e8..aeb9aaff2af 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 @@ -12,9 +12,9 @@ use dpp::platform_value::{Identifier, Value}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionRefV1; use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; -use drive::drive::document::lifecycle::DocumentLifecycleState; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::drive::Drive; @@ -210,59 +210,120 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( )) } -// ============================================================================ -// fetch_document_with_id -// ============================================================================ - -/// Versioned facade for `fetch_keep_history_document_lifecycle`. +/// Like [`fetch_documents_for_transitions_knowing_contract_and_document_type`], +/// for transitions viewed through the shell that sees every batch wire format. /// -/// Classifies one keep-history document as active, deleted, erasing or absent -/// and bills the reads it performed. Every stateful check that has to tell a -/// deleted document from one that never existed goes through this; a document -/// type that keeps no history has no lifecycle to read and must not reach it. +/// Generation 2 of the method is the first one reachable through this view; +/// generations 0 and 1 only see the shell of batch formats 0 and 1 and are +/// reached through the sibling function. #[allow(clippy::too_many_arguments)] -pub(crate) fn fetch_keep_history_document_lifecycle( +pub(crate) fn fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, - id: Identifier, + transitions: &[DocumentTransitionRefV1<'_>], epoch: &Epoch, execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, -) -> Result { +) -> Result>, Error> { match platform_version .drive_abci .validation_and_processing .state_transitions .batch_state_transition - .fetch_keep_history_document_lifecycle + .fetch_documents_for_transitions_knowing_contract_and_document_type { - 0 => { - let (state, fee_result) = drive - .fetch_document_lifecycle( - contract, - document_type, - id, - Some(epoch), - transaction, - platform_version, - ) - .map_err(Error::Drive)?; - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - Ok(state) - } + 2 => fetch_documents_for_transitions_knowing_contract_and_document_type_v2( + drive, + contract, + document_type, + transitions, + epoch, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution( crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: "fetch_keep_history_document_lifecycle".to_string(), - known_versions: vec![0], + method: "fetch_documents_for_transitions_knowing_contract_and_document_type" + .to_string(), + known_versions: vec![2], received: version, }, )), } } +/// Generation 2: the billing of generation 1, for transitions of any batch +/// wire format. +#[allow(clippy::too_many_arguments)] +fn fetch_documents_for_transitions_knowing_contract_and_document_type_v2( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + transitions: &[DocumentTransitionRefV1<'_>], + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + if transitions.is_empty() { + return Ok(ConsensusValidationResult::new_with_data(vec![])); + } + + let ids: Vec = transitions + .iter() + .map(|dt| Value::Identifier(dt.get_id().to_buffer())) + .collect(); + + let drive_query = DriveDocumentQuery { + contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: Some(WhereClause { + field: "$id".to_string(), + operator: WhereOperator::In, + value: Value::Array(ids), + }), + primary_key_equal_clause: None, + in_clauses: Vec::new(), + range_clause: None, + equal_clauses: Default::default(), + }, + offset: None, + limit: Some(transitions.len() as u16), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + resolved_time_ranges: vec![], + sub_queries: vec![], + }; + + let documents_outcome = drive.query_documents( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); + + Ok(ConsensusValidationResult::new_with_data( + documents_outcome.documents_owned(), + )) +} + +// ============================================================================ +// fetch_document_with_id +// ============================================================================ + /// Versioned facade for `fetch_document_with_id`. /// /// Dispatches on the per-helper version field on diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index c2de31db690..4fd93181846 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -17,7 +17,6 @@ use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; @@ -145,15 +144,6 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, - DocumentTransitionAction::EraseAction(erase_action) => erase_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { index_only_delete_action.validate_state( platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs new file mode 100644 index 00000000000..2e77dbf1acc --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs @@ -0,0 +1,47 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::state_transitions::batch::transformer::v1::BatchTransitionTransformerV1; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::batch_transition::BatchTransition; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +/// Generation 2 of the batch transformation, selected for protocol version 14 +/// and later: like generation 1 it threads the caller's execution context into +/// the transformer, and it runs transformer generation 1, which sees every batch +/// wire format and produces batch action format 1. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionActionTransformV2 +{ + fn transform_into_action_v2( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl DocumentsBatchStateTransitionActionTransformV2 for BatchTransition { + fn transform_into_action_v2( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error> { + let validation_result = self.try_into_action_v1( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + execution_context, + )?; + + Ok(validation_result.map(StateTransitionAction::BatchActionV1)) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs new file mode 100644 index 00000000000..62b1f0ffc56 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs @@ -0,0 +1,373 @@ +use dpp::block::block_info::BlockInfo; +use dpp::consensus::ConsensusError; +use dpp::consensus::state::state_error::StateError; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::StateTransitionOwned; +use drive::state_transition_action::StateTransitionAction; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use drive::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; +use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use crate::error::Error; +use crate::error::execution::ExecutionError; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_transfer_transition_action::DocumentTransferTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_update_price_transition_action::DocumentUpdatePriceTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_burn_transition_action::TokenBurnTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_claim_transition_action::TokenClaimTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_config_update_transition_action::TokenConfigUpdateTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_destroy_frozen_funds_transition_action::TokenDestroyFrozenFundsTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_emergency_action_transition_action::TokenEmergencyActionTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_freeze_transition_action::TokenFreezeTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_mint_transition_action::TokenMintTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_transfer_transition_action::TokenTransferTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_unfreeze_transition_action::TokenUnfreezeTransitionActionValidation; +use crate::execution::validation::state_transition::batch::data_triggers::{data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutor}; +use crate::platform_types::platform::PlatformStateRef; + +/// Generation 1 validates batch action format 1 against state, which is what a +/// batch of any wire format becomes for protocol version 14 and later, +/// including the erase of a keep-history document. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionValidationV1 +{ + fn validate_state_v1( + &self, + action: BatchTransitionActionV1, + platform: &PlatformStateRef, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl DocumentsBatchStateTransitionValidationV1 for BatchTransition { + fn validate_state_v1( + &self, + mut state_transition_action: BatchTransitionActionV1, + platform: &PlatformStateRef, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut validation_result = ConsensusValidationResult::::new(); + + let owner_id = state_transition_action.owner_id(); + + let mut validated_transitions = vec![]; + + let data_trigger_bindings = if platform.config.execution.use_document_triggers { + data_trigger_bindings_list(platform_version)? + } else { + vec![] + }; + + // Next we need to validate the structure of all actions (this means with the data contract) + for transition in state_transition_action.transitions_take() { + let transition_validation_result = match &transition { + BatchedTransitionActionV1::DocumentErase(erase_action) => erase_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( + document_action, + )) => match document_action { + DocumentTransitionAction::CreateAction(create_action) => create_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + DocumentTransitionAction::ReplaceAction(replace_action) => replace_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + DocumentTransitionAction::TransferAction(transfer_action) => transfer_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + DocumentTransitionAction::DeleteAction(delete_action) => delete_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + DocumentTransitionAction::UpdatePriceAction(update_price_action) => { + update_price_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )? + } + DocumentTransitionAction::PurchaseAction(purchase_action) => purchase_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { + index_only_delete_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )? + } + }, + BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( + token_action, + )) => match token_action { + TokenTransitionAction::BurnAction(burn_action) => burn_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::MintAction(mint_action) => mint_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::TransferAction(transfer_action) => transfer_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::FreezeAction(freeze_action) => freeze_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::UnfreezeAction(unfreeze_action) => unfreeze_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::EmergencyActionAction(emergency_action_action) => { + emergency_action_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )? + } + TokenTransitionAction::DestroyFrozenFundsAction( + destroy_frozen_funds_action, + ) => destroy_frozen_funds_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::ConfigUpdateAction(config_update_action) => { + config_update_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )? + } + TokenTransitionAction::ClaimAction(claim_action) => claim_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + TokenTransitionAction::DirectPurchaseAction(direct_purchase_action) => { + direct_purchase_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )? + } + TokenTransitionAction::SetPriceForDirectPurchaseAction( + set_price_for_direct_purchase_action, + ) => set_price_for_direct_purchase_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, + }, + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(_), + ) => { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "we should never start with a bump identity data contract nonce", + ))); + } + }; + + if !transition_validation_result.is_valid() { + // If a state transition isn't valid we still need to bump the identity data contract nonce + validation_result.add_errors(transition_validation_result.errors); + validated_transitions.push(BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce( + BumpIdentityDataContractNonceAction::try_from_borrowed_batched_transition_action_v1( + &transition, + owner_id, + state_transition_action.user_fee_increase(), + )?, + ), + )); + } else if platform.config.execution.use_document_triggers { + // An erase has no data triggers: it carries no document values + // and acts on a document that is already deleted. + if let BatchedTransitionActionV1::Batched( + BatchedTransitionAction::DocumentAction(document_transition), + ) = &transition + { + // Pre-PR this site allocated a default-initialized local + // `StateTransitionExecutionContext` and passed `&local` to + // the trigger context. The local was dropped on return and + // all of its add_operation calls were silently discarded. + // `_v1` triggers need to actually bill (call add_operation), + // so the trigger context now references the OUTER mutable + // execution_context that the processor threaded in. + // + // PROTOCOL_VERSION_11 consensus-safety: on PV11 the + // per-trigger version fields stay at 0, so wrappers + // dispatch to `_v0` triggers whose bodies are + // byte-identical to v3.1-dev (only their param signature + // gained `&mut`, the body never mutates). _v0 triggers + // do not call `add_operation`, so the outer + // execution_context is read-only from the trigger's + // perspective on PV11 — same chain state as pre-PR. + // + // Non-consensus side-effect on PV11 mempool: the trigger + // now sees the outer ctx's real `dry_run` flag instead of + // the previous-default `false`. During CheckTx with + // `dry_run: true`, _v0 triggers short-circuit their + // `query_documents` (via `query_documents_v0`'s internal + // dry-run guard) and skip the post-query validation. + // Doesn't affect block validation (Validator mode is + // always `dry_run: false`), so chain replay matches + // pre-PR byte-for-byte. Pre-PR also did the query but + // ignored its result during dry-run validation, so the + // net mempool outcome is the same. + let owner_id_value = self.owner_id(); + let mut data_trigger_execution_context = DataTriggerExecutionContext { + platform, + block_info, + transaction, + owner_id: &owner_id_value, + state_transition_execution_context: execution_context, + }; + let data_trigger_execution_result = document_transition + .validate_with_data_triggers( + &data_trigger_bindings, + &mut data_trigger_execution_context, + platform_version, + )?; + + if !data_trigger_execution_result.is_valid() { + // If a state transition isn't valid because of data triggers we still need + // to bump the identity data contract nonce + let consensus_errors: Vec = data_trigger_execution_result + .errors + .into_iter() + .map(|e| ConsensusError::StateError(StateError::DataTriggerError(e))) + .collect(); + validation_result.add_errors(consensus_errors); + validated_transitions.push(BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action( + document_transition.base(), + owner_id, + state_transition_action.user_fee_increase(), + ), + ), + )); + } else { + validated_transitions.push(transition); + } + } else { + validated_transitions.push(transition); + } + } else { + validated_transitions.push(transition); + } + } + + state_transition_action.set_transitions(validated_transitions); + + validation_result.set_data(StateTransitionAction::BatchActionV1( + state_transition_action, + )); + + Ok(validation_result) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs index d7d4249a681..bcf204167bd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs @@ -5052,7 +5052,7 @@ mod creation_tests { assert_matches!( documents_batch_create_transition, - StateTransition::Batch(BatchTransition::V1(_)) + StateTransition::Batch(BatchTransition::V2(_)) ); let documents_batch_create_serialized_transition = documents_batch_create_transition diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index f300d67ea5a..6ac9ef80715 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -16,15 +16,16 @@ use dpp::document::Document; use dpp::identifier::Identifier; use dpp::identity::{Identity, IdentityPublicKey, SecurityLevel}; use dpp::prelude::IdentityNonce; +use dpp::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; use dpp::state_transition::StateTransition; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::TokenPaymentInfo; use dpp::version::PlatformVersion; use dpp::ProtocolError; -use drive::drive::document::history::{ - DocumentHistoryQueryV1, DocumentHistorySelector, DocumentHistoryState, -}; use drive::drive::document::lifecycle::DocumentLifecycleState; +use drive::query::document_history_drive_query::{ + DocumentHistoryDriveQuery, DocumentHistoryFilter, DocumentHistoryState, +}; use drive::util::storage_flags::StorageFlags; use rand::rngs::StdRng; use rand::SeedableRng; @@ -89,7 +90,7 @@ struct Fixture { impl Fixture { async fn new(document_type_name: &str) -> Self { - let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() .with_initial_protocol_version(14) .build_with_mock_rpc() @@ -155,7 +156,7 @@ impl Fixture { } async fn create(&mut self, document: Document, entropy: Bytes32, document_type_name: &str) { - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let transition = BatchTransition::new_document_creation_transition_from_document( document, self.document_type(document_type_name), @@ -182,7 +183,7 @@ impl Fixture { &mut self, document_type_name: &str, ) -> StateTransitionExecutionResult { - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut document = self.document.clone(); document.set_revision(Some(1)); let transition = BatchTransition::new_document_deletion_transition_from_document( @@ -211,7 +212,7 @@ impl Fixture { as_owner: bool, token_payment_info: Option, ) -> StateTransitionExecutionResult { - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut document = self.document.clone(); document.set_revision(Some(1)); if !as_owner { @@ -251,8 +252,8 @@ impl Fixture { self.nonce, 0, &self.owner_signer, - platform_version, None, + platform_version, ) .await .expect("expected an erase transition"); @@ -266,8 +267,8 @@ impl Fixture { self.stranger_nonce, 0, &self.stranger_signer, - platform_version, None, + platform_version, ) .await .expect("expected an erase transition"); @@ -284,7 +285,7 @@ impl Fixture { /// one erase chunk can remove. How they got there does not matter to an /// erase: it reads what the history holds. fn retain_revisions(&mut self, document_type_name: &str, revisions: u64) { - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let contract = self.contract.clone(); let document_type = contract .document_type_for_name(document_type_name) @@ -326,7 +327,7 @@ impl Fixture { /// Builds an erase without processing it, so several can share one block. async fn erase_transition(&mut self, document_type_name: &str, as_owner: bool) -> Vec { - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut document = self.document.clone(); document.set_revision(Some(1)); let (key, signer, nonce) = if as_owner { @@ -346,8 +347,8 @@ impl Fixture { nonce, 0, signer, - platform_version, None, + platform_version, ) .await .expect("expected an erase transition") @@ -356,44 +357,46 @@ impl Fixture { } fn remaining_revisions(&self, document_type_name: &str) -> u64 { - let query = DocumentHistoryQueryV1 { + let query = DocumentHistoryDriveQuery { contract_id: self.contract.id().to_buffer(), document_type_name: document_type_name.to_string(), document_id: self.document.id().to_buffer(), - selector: DocumentHistorySelector::StartAtTime(0), + filter: DocumentHistoryFilter::StartAtTime(0), limit: Some(1), }; self.platform .drive - .fetch_document_history_v1( + .fetch_document_history( &query, self.document_type(document_type_name), None, - PlatformVersion::get(14).unwrap(), + PlatformVersion::latest(), ) .expect("expected to read the history") .lifecycle + .expect("history carries lifecycle metadata") .remaining_revisions } fn history_state(&self, document_type_name: &str) -> DocumentHistoryState { - let query = DocumentHistoryQueryV1 { + let query = DocumentHistoryDriveQuery { contract_id: self.contract.id().to_buffer(), document_type_name: document_type_name.to_string(), document_id: self.document.id().to_buffer(), - selector: DocumentHistorySelector::StartAtTime(0), + filter: DocumentHistoryFilter::StartAtTime(0), limit: Some(1), }; self.platform .drive - .fetch_document_history_v1( + .fetch_document_history( &query, self.document_type(document_type_name), None, - PlatformVersion::get(14).unwrap(), + PlatformVersion::latest(), ) .expect("expected to read the history") .lifecycle + .expect("history carries lifecycle metadata") .state } @@ -406,7 +409,7 @@ impl Fixture { self.document.id(), None, None, - PlatformVersion::get(14).unwrap(), + PlatformVersion::latest(), ) .expect("expected to read the lifecycle") .0 @@ -424,27 +427,27 @@ async fn rewrite_delete_as_erase( ) -> StateTransition { use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransition, DocumentEraseTransition, DocumentTransition, + BatchedTransitionV1, DocumentEraseTransition, DocumentTransitionV1, }; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::BatchTransition; - let StateTransition::Batch(BatchTransition::V1(mut batch)) = transition else { - panic!("expected a v1 batch transition"); + let StateTransition::Batch(BatchTransition::V2(mut batch)) = transition else { + panic!("expected a v2 batch transition"); }; - let BatchedTransition::Document(DocumentTransition::Delete(delete)) = + let BatchedTransitionV1::Document(DocumentTransitionV1::Delete(delete)) = batch.transitions.remove(0) else { panic!("expected a single document delete"); }; batch .transitions - .push(BatchedTransition::Document(DocumentTransition::Erase( + .push(BatchedTransitionV1::Document(DocumentTransitionV1::Erase( DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: delete.base().clone(), }), ))); - let mut rebuilt: StateTransition = BatchTransition::V1(batch).into(); + let mut rebuilt: StateTransition = BatchTransition::V2(batch).into(); rebuilt .sign_external( key, @@ -501,7 +504,7 @@ async fn should_reject_a_create_over_a_deleted_documents_id() { let mut fixture = Fixture::new("note").await; assert_successful(&fixture.delete_as_owner("note").await, "the delete"); - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let document = fixture.document.clone(); // The same id the deleted document had: a document id is derived from its // creator, contract, type and entropy, so re-creating with the same inputs @@ -666,8 +669,7 @@ async fn should_reject_an_erase_of_an_id_that_holds_nothing() { /// keys, their funds or their permission cannot strand a half-erased document. #[tokio::test] async fn should_let_any_identity_finish_an_erasure_its_owner_started() { - let chunk = PlatformVersion::get(14) - .unwrap() + let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition .expect("protocol 14 bounds the chunk") as u64; @@ -707,8 +709,7 @@ async fn should_let_any_identity_finish_an_erasure_its_owner_started() { /// A document whose erasure has begun is not there to be deleted again. #[tokio::test] async fn should_reject_a_delete_of_a_document_whose_erasure_has_begun() { - let chunk = PlatformVersion::get(14) - .unwrap() + let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition .expect("protocol 14 bounds the chunk") as u64; @@ -733,8 +734,7 @@ async fn should_reject_a_delete_of_a_document_whose_erasure_has_begun() { /// validated. #[tokio::test] async fn should_finish_an_erasure_across_two_transitions_in_one_block() { - let chunk = PlatformVersion::get(14) - .unwrap() + let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition .expect("protocol 14 bounds the chunk") as u64; @@ -781,7 +781,7 @@ async fn should_finish_an_erasure_across_two_transitions_in_one_block() { async fn should_delete_and_erase_in_one_block() { let mut fixture = Fixture::new("note").await; - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut document = fixture.document.clone(); document.set_revision(Some(1)); let delete = BatchTransition::new_document_deletion_transition_from_document( @@ -845,7 +845,7 @@ async fn should_charge_an_erase_for_the_refund_recipients_it_can_credit() { let mut fixture = Fixture::new("note").await; assert_successful(&fixture.delete_as_owner("note").await, "the delete"); - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let recipient_cost = fixture .platform .drive @@ -894,7 +894,7 @@ async fn should_persist_the_nonce_bump_on_a_refused_erase() { fixture.contract.id().to_buffer(), true, None, - PlatformVersion::get(14).unwrap(), + PlatformVersion::latest(), ) .expect("expected to read the identity contract nonce") .expect("the create already wrote one") @@ -940,7 +940,7 @@ async fn should_persist_the_nonce_bump_on_a_refused_erase() { async fn should_reject_a_create_that_follows_a_delete_of_the_same_id_in_one_block() { let mut fixture = Fixture::new("note").await; - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut document = fixture.document.clone(); document.set_revision(Some(1)); let delete = BatchTransition::new_document_deletion_transition_from_document( @@ -1019,7 +1019,7 @@ async fn should_reject_a_replace_of_a_deleted_document() { let mut fixture = Fixture::new("note").await; assert_successful(&fixture.delete_as_owner("note").await, "the delete"); - let platform_version = PlatformVersion::get(14).unwrap(); + let platform_version = PlatformVersion::latest(); let mut replacement = fixture.document.clone(); replacement.set_revision(Some(2)); let transition = BatchTransition::new_document_replacement_transition_from_document( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 016d968b111..74aa1edd5a8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -2609,7 +2609,8 @@ mod test_suite { use drive::drive::Drive; use simple_signer::signer::SimpleSigner; use std::sync::Arc; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRefV1; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; @@ -2982,20 +2983,22 @@ mod test_suite { }; // --- Extract TokenTransition from claim_transition --- - let StateTransition::Batch(BatchTransition::V1(batch_v1)) = claim_transition else { - return Err("expected BatchTransition::V1".to_string()); + let StateTransition::Batch(batch) = claim_transition else { + return Err("expected a batch transition".to_string()); }; // Assume only one transition was in the batch - let [batched_transition] = batch_v1.transitions.as_slice() else { + if batch.transitions_len_v1() != 1 { return Err(format!( "expected exactly one batched transition, got {}", - batch_v1.transitions.len() + batch.transitions_len_v1() )); - }; + } - let BatchedTransition::Token(token_transition) = batched_transition else { - return Err("expected BatchedTransition::Token".to_string()); + let Some(BatchedTransitionRefV1::Token(token_transition)) = + batch.first_transition_v1() + else { + return Err("expected a token transition".to_string()); }; // --- Now build expected document --- diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs index 694ae41fabd..487fdac4808 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs @@ -8,10 +8,10 @@ mod token_transfer_tests { use dpp::state_transition::batch_transition::TokenMintTransition; use dpp::data_contract::group::v0::GroupV0; use dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; - use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::{GetDataContractSecurityLevelRequirementFn, StateTransition}; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionMutRef; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionMutRefV1; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::identity::KeyType; @@ -1230,9 +1230,9 @@ mod token_transfer_tests { // here we add fake info if let StateTransition::Batch(batch) = &mut token_transfer_transition { let first_transition = batch - .first_transition_mut() + .first_transition_mut_v1() .expect("expected_first_transition"); - if let BatchedTransitionMutRef::Token(TokenTransition::Transfer(transfer)) = + if let BatchedTransitionMutRefV1::Token(TokenTransition::Transfer(transfer)) = first_transition { transfer diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 69b2943f072..01480cab7fe 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -56,7 +56,6 @@ use dpp::state_transition::batch_transition::batched_transition::document_purcha use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionAction; use drive::state_transition_action::batch::BatchTransitionAction; @@ -867,16 +866,6 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } - DocumentTransition::Erase(document_erase_transition) => { - let (batched_action, fee_result) = DocumentEraseTransitionAction::try_from_document_borrowed_erase_transition_with_contract_lookup(document_erase_transition, owner_id, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - })?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } DocumentTransition::Transfer(document_transfer_transition) => { let validation_result = Self::find_replaced_document_v0(transition, replaced_documents); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs new file mode 100644 index 00000000000..aa44110fbe4 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs @@ -0,0 +1,1162 @@ +// Generation 1 of the batch transformer, selected for protocol version 14 and +// later. It views the batch through the shell that sees every batch wire +// format, so a format 2 batch carrying an erase of a keep-history document is +// transformed like any other, and it produces batch action format 1, whose +// items may include that erase. +// +// The aggregator helpers `ConsensusValidationResult::flatten` / +// `merge_many` are versioned via `dpp.validation.validation_result` on +// `PlatformVersion`. v0 (PROTOCOL_VERSION_11 and below) preserves the +// legacy `Some(empty_vec)`-on-no-data behavior for chain reproducibility; +// v1 (PROTOCOL_VERSION_12+) returns `data: None` in that case so an +// all-failed batch flows down the unpaid path. See issue #2867. + +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; +use std::sync::Arc; + +use crate::error::Error; +use crate::platform_types::platform::PlatformStateRef; +use dpp::consensus::basic::document::{DataContractNotPresentError, InvalidDocumentTypeError}; +use dpp::consensus::basic::value_error::ValueError; +use dpp::consensus::basic::BasicError; + +use dpp::consensus::state::document::document_not_found_error::DocumentNotFoundError; +use dpp::consensus::state::document::document_owner_id_mismatch_error::DocumentOwnerIdMismatchError; + +use dpp::consensus::state::document::invalid_document_revision_error::InvalidDocumentRevisionError; +use dpp::consensus::state::state_error::StateError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; + +use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::document::document_incorrect_purchase_price_error::DocumentIncorrectPurchasePriceError; +use dpp::consensus::state::document::document_not_for_sale_error::DocumentNotForSaleError; +use dpp::document::property_names::PRICE; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::fee::Credits; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::prelude::{Revision, UserFeeIncrease}; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::{consensus::ConsensusError, prelude::Identifier, validation::ConsensusValidationResult}; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransitionRefV1, DocumentTransitionRefV1, +}; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; +use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; +use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionAction; +use drive::state_transition_action::batch::v1::{ + BatchTransitionActionV1, BatchedTransitionActionV1, +}; + +use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; + +use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; +use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use drive::drive::contract::DataContractFetchInfo; +use drive::drive::Drive; +use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::DocumentTransferTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::DocumentUpdatePriceTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_burn_transition_action::TokenBurnTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_config_update_transition_action::TokenConfigUpdateTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_destroy_frozen_funds_transition_action::TokenDestroyFrozenFundsTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_emergency_action_transition_action::TokenEmergencyActionTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_freeze_transition_action::TokenFreezeTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_transition_action::TokenMintTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::TokenClaimTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; +use crate::platform_types::platform_state::PlatformStateV0Methods; + +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait BatchTransitionTransformerV1 +{ + fn try_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + full_validation: bool, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + ) -> Result, Error>; +} + +trait BatchTransitionInternalTransformerV1 { + #[allow(clippy::too_many_arguments)] + fn transform_document_transitions_within_contract_v1( + platform: &PlatformStateRef, + block_info: &BlockInfo, + full_validation: bool, + data_contract_id: &Identifier, + owner_id: Identifier, + document_transitions: &BTreeMap<&String, Vec>>, + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error>; + #[allow(clippy::too_many_arguments)] + fn transform_document_transitions_within_document_type_v1( + platform: &PlatformStateRef, + block_info: &BlockInfo, + full_validation: bool, + data_contract_fetch_info: Arc, + document_type_name: &str, + owner_id: Identifier, + document_transitions: &[DocumentTransitionRefV1<'_>], + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error>; + #[allow(clippy::too_many_arguments)] + fn transform_token_transitions_within_contract_v1( + platform: &PlatformStateRef, + data_contract_id: &Identifier, + block_info: &BlockInfo, + validate_against_state: bool, + owner_id: Identifier, + token_transitions: &[&TokenTransition], + user_fee_increase: UserFeeIncrease, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result>, Error>; + #[allow(clippy::too_many_arguments)] + /// Transfer token transition + fn transform_token_transition_v1( + drive: &Drive, + transaction: TransactionArg, + block_info: &BlockInfo, + validate_against_state: bool, + data_contract_fetch_info: Arc, + transition: &TokenTransition, + owner_id: Identifier, + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error>; + /// The data contract can be of multiple difference versions + #[allow(clippy::too_many_arguments)] + fn transform_document_transition_v1( + drive: &Drive, + transaction: TransactionArg, + full_validation: bool, + block_info: &BlockInfo, + data_contract_fetch_info: Arc, + transition: DocumentTransitionRefV1<'_>, + replaced_documents: &[Document], + user_fee_increase: UserFeeIncrease, + owner_id: Identifier, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error>; + fn find_replaced_document_v1<'a>( + document_transition: DocumentTransitionRefV1<'_>, + fetched_documents: &'a [Document], + ) -> ConsensusValidationResult<&'a Document>; + fn check_ownership_of_old_replaced_document_v1( + document_id: Identifier, + fetched_document: &Document, + owner_id: &Identifier, + ) -> SimpleConsensusValidationResult; + fn check_revision_is_bumped_by_one_during_replace_v1( + transition_revision: Revision, + document_id: Identifier, + original_document: &Document, + ) -> SimpleConsensusValidationResult; + fn failed_per_transition_action( + base_transition: &dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition, + owner_id: Identifier, + errors: Vec, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl BatchTransitionTransformerV1 for BatchTransition { + fn try_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validate_against_state: bool, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + ) -> Result, Error> { + let owner_id = self.owner_id(); + let user_fee_increase = self.user_fee_increase(); + let platform_version = platform.state.current_platform_version()?; + let mut document_transitions_by_contracts_and_types: BTreeMap< + &Identifier, + BTreeMap<&String, Vec>, + > = BTreeMap::new(); + + let mut token_transitions_by_contracts: BTreeMap<&Identifier, Vec<&TokenTransition>> = + BTreeMap::new(); + + // We want to validate by contract, and then for each document type within a contract + for transition in self.transitions_iter_v1() { + match transition { + BatchedTransitionRefV1::Document(document_transition) => { + let document_type = document_transition.base().document_type_name(); + let data_contract_id = document_transition.base().data_contract_id_ref(); + + match document_transitions_by_contracts_and_types.entry(data_contract_id) { + Entry::Vacant(v) => { + v.insert(BTreeMap::from([(document_type, vec![document_transition])])); + } + Entry::Occupied(mut transitions_by_types_in_contract) => { + match transitions_by_types_in_contract + .get_mut() + .entry(document_type) + { + Entry::Vacant(v) => { + v.insert(vec![document_transition]); + } + Entry::Occupied(mut o) => o.get_mut().push(document_transition), + } + } + } + } + BatchedTransitionRefV1::Token(token_transition) => { + let data_contract_id = token_transition.base().data_contract_id_ref(); + + match token_transitions_by_contracts.entry(data_contract_id) { + Entry::Vacant(v) => { + v.insert(vec![token_transition]); + } + Entry::Occupied(mut transitions_by_tokens_in_contract) => { + transitions_by_tokens_in_contract + .get_mut() + .push(token_transition) + } + } + } + } + } + + let validation_result_documents = document_transitions_by_contracts_and_types + .iter() + .map( + |(data_contract_id, document_transitions_by_document_type)| { + Self::transform_document_transitions_within_contract_v1( + platform, + block_info, + validate_against_state, + data_contract_id, + owner_id, + document_transitions_by_document_type, + user_fee_increase, + execution_context, + transaction, + platform_version, + ) + }, + ) + .collect::>>, Error>>( + )?; + + let mut validation_result_tokens = token_transitions_by_contracts + .iter() + .map(|(data_contract_id, token_transitions)| { + Self::transform_token_transitions_within_contract_v1( + platform, + data_contract_id, + block_info, + validate_against_state, + owner_id, + token_transitions, + user_fee_increase, + transaction, + execution_context, + platform_version, + ) + }) + .collect::>>, Error>>( + )?; + + let mut validation_results = validation_result_documents; + + validation_results.append(&mut validation_result_tokens); + + let validation_result = + ConsensusValidationResult::flatten(validation_results, platform_version)?; + + if validation_result.has_data() { + let (transitions, errors) = validation_result.into_data_and_errors()?; + let batch_transition_action = BatchTransitionActionV1 { + owner_id, + transitions, + user_fee_increase, + }; + Ok(ConsensusValidationResult::new_with_data_and_errors( + batch_transition_action, + errors, + )) + } else { + Ok(ConsensusValidationResult::new_with_errors( + validation_result.errors, + )) + } + } +} + +impl BatchTransitionInternalTransformerV1 for BatchTransition { + /// Roll up the per-token-transition results via the versioned + /// [`ConsensusValidationResult::merge_many`] facade. + /// + /// The aggregator's v0→v1 change at PROTOCOL_VERSION_12 (see issue + /// #2867) also affects this token path. **Tokens-always-pay + /// invariant**: every per-token sub-transformer + /// (`try_from_borrowed_token_*_transition_with_contract_lookup`) + /// emits a `BumpIdentityDataContractNonce` action on its + /// base-validation failure path, so each per-token result carries + /// `data: Some(...)` even when validation fails. The v1 aggregator + /// therefore never collapses an all-failed token batch to + /// `data: None`, and token failures continue to land as + /// `PaidConsensusError` (tx stays in the block, user pays for the + /// validation work) under both v11 and v12. + /// + /// A future change to a token sub-transformer that drops the bump + /// emission on a failure path would silently route that failure to + /// `UnpaidConsensusError` (tx removed from the block by + /// `prepare_proposal`). See + /// `tests/token/burn/mod.rs::test_token_burn_trying_to_burn_more_than_we_have` + /// and its `_protocol_version_11` sibling for the regression pin. + fn transform_token_transitions_within_contract_v1( + platform: &PlatformStateRef, + data_contract_id: &Identifier, + block_info: &BlockInfo, + validate_against_state: bool, + owner_id: Identifier, + token_transitions: &[&TokenTransition], + user_fee_increase: UserFeeIncrease, + transaction: TransactionArg, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + let drive = platform.drive; + // Data Contract must exist + let Some(data_contract_fetch_info) = drive + .get_contract_with_fetch_info_and_fee( + data_contract_id.to_buffer(), + None, + false, + transaction, + platform_version, + )? + .1 + else { + return Ok(ConsensusValidationResult::new_with_error( + BasicError::DataContractNotPresentError(DataContractNotPresentError::new( + *data_contract_id, + )) + .into(), + )); + }; + + let validation_result = token_transitions + .iter() + .map(|token_transition| { + Self::transform_token_transition_v1( + platform.drive, + transaction, + block_info, + validate_against_state, + data_contract_fetch_info.clone(), + token_transition, + owner_id, + user_fee_increase, + execution_context, + platform_version, + ) + }) + .map(|result| result.map(|result| result.map(BatchedTransitionActionV1::Batched))) + .collect::>, Error>>( + )?; + let validation_result = + ConsensusValidationResult::merge_many(validation_result, platform_version)?; + Ok(validation_result) + } + fn transform_document_transitions_within_contract_v1( + platform: &PlatformStateRef, + block_info: &BlockInfo, + validate_against_state: bool, + data_contract_id: &Identifier, + owner_id: Identifier, + document_transitions: &BTreeMap<&String, Vec>>, + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + let drive = platform.drive; + // Data Contract must exist + let Some(data_contract_fetch_info) = drive + .get_contract_with_fetch_info_and_fee( + data_contract_id.0 .0, + None, + false, + transaction, + platform_version, + )? + .1 + else { + return Ok(ConsensusValidationResult::new_with_error( + BasicError::DataContractNotPresentError(DataContractNotPresentError::new( + *data_contract_id, + )) + .into(), + )); + }; + + let validation_result = document_transitions + .iter() + .map(|(document_type_name, document_transitions)| { + Self::transform_document_transitions_within_document_type_v1( + platform, + block_info, + validate_against_state, + data_contract_fetch_info.clone(), + document_type_name, + owner_id, + document_transitions, + user_fee_increase, + execution_context, + transaction, + platform_version, + ) + }) + .collect::>>, Error>>( + )?; + Ok(ConsensusValidationResult::flatten( + validation_result, + platform_version, + )?) + } + + fn transform_document_transitions_within_document_type_v1( + platform: &PlatformStateRef, + block_info: &BlockInfo, + validate_against_state: bool, + data_contract_fetch_info: Arc, + document_type_name: &str, + owner_id: Identifier, + document_transitions: &[DocumentTransitionRefV1<'_>], + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + // We use temporary execution context without dry run, + // because despite the dryRun, we need to get the + // data contract to proceed with following logic + // let tmp_execution_context = StateTransitionExecutionContext::default_for_platform_version(platform_version)?; + // + // execution_context.add_operations(tmp_execution_context.operations_slice()); + + let dry_run = false; //maybe reenable + + let data_contract = &data_contract_fetch_info.contract; + + let Some(document_type) = data_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(ConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.to_owned(), data_contract.id()) + .into(), + )); + }; + + let replace_and_transfer_transitions = document_transitions + .iter() + .filter(|transition| { + matches!( + transition, + DocumentTransitionRefV1::Replace(_) + | DocumentTransitionRefV1::Transfer(_) + | DocumentTransitionRefV1::Purchase(_) + | DocumentTransitionRefV1::UpdatePrice(_) + ) + }) + .copied() + .collect::>(); + + // We fetch documents only for replace and transfer transitions + // since we need them to create transition actions. Below we also + // perform state validation for replace and transfer transitions + // only; other transitions are validated in their validate_state + // functions. + // TODO: Think more about this architecture + // + // PROTOCOL_VERSION_11 consensus-safety: the fetch fn now takes + // `&mut execution_context` and bills the query cost internally + // — but only when `transform_into_action: 1`. On v0 it forces + // `epoch=None` (zero cost) and skips `add_operation`, matching + // pre-PR exactly. + let fetched_documents_validation_result = + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type( + platform.drive, + data_contract, + document_type, + replace_and_transfer_transitions.as_slice(), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )?; + + if !fetched_documents_validation_result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + fetched_documents_validation_result.errors, + )); + } + + let replaced_documents = fetched_documents_validation_result.into_data()?; + + Ok(if !dry_run { + let document_transition_actions_validation_result = document_transitions + .iter() + .map(|transition| { + // we validate every transition in this document type + Self::transform_document_transition_v1( + platform.drive, + transaction, + validate_against_state, + block_info, + data_contract_fetch_info.clone(), + *transition, + &replaced_documents, + user_fee_increase, + owner_id, + execution_context, + platform_version, + ) + }) + .collect::>, Error>>( + )?; + + let result = ConsensusValidationResult::merge_many( + document_transition_actions_validation_result, + platform_version, + )?; + + if !result.is_valid() { + return Ok(result); + } + result + } else { + ConsensusValidationResult::default() + }) + } + + /// The data contract can be of multiple difference versions + fn transform_token_transition_v1( + drive: &Drive, + transaction: TransactionArg, + block_info: &BlockInfo, + validate_against_state: bool, + data_contract_fetch_info: Arc, + transition: &TokenTransition, + owner_id: Identifier, + user_fee_increase: UserFeeIncrease, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let approximate_for_costs = !validate_against_state; + match transition { + TokenTransition::Burn(token_burn_transition) => { + let (batched_action, fee_result) = TokenBurnTransitionAction::try_from_borrowed_token_burn_transition_with_contract_lookup(drive, owner_id, token_burn_transition, approximate_for_costs, transaction, block_info,user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Mint(token_mint_transition) => { + let (batched_action, fee_result) = TokenMintTransitionAction::try_from_borrowed_token_mint_transition_with_contract_lookup(drive, owner_id, token_mint_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Transfer(token_transfer_transition) => { + let (token_transfer_action, fee_result) = TokenTransferTransitionAction::try_from_borrowed_token_transfer_transition_with_contract_lookup(drive, owner_id, token_transfer_transition, approximate_for_costs, transaction, block_info, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + let batched_action = BatchedTransitionAction::TokenAction( + TokenTransitionAction::TransferAction(token_transfer_action), + ); + Ok(batched_action.into()) + } + TokenTransition::Freeze(token_freeze_transition) => { + let (batched_action, fee_result) = TokenFreezeTransitionAction::try_from_borrowed_token_freeze_transition_with_contract_lookup(drive, owner_id, token_freeze_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Unfreeze(token_unfreeze_transition) => { + let (batched_action, fee_result) = TokenUnfreezeTransitionAction::try_from_borrowed_token_unfreeze_transition_with_contract_lookup(drive, owner_id, token_unfreeze_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::DestroyFrozenFunds(destroy_frozen_funds) => { + let (batched_action, fee_result) = TokenDestroyFrozenFundsTransitionAction::try_from_borrowed_token_destroy_frozen_funds_transition_with_contract_lookup(drive, owner_id, destroy_frozen_funds, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::EmergencyAction(emergency_action) => { + let (batched_action, fee_result) = TokenEmergencyActionTransitionAction::try_from_borrowed_token_emergency_action_transition_with_contract_lookup(drive, owner_id, emergency_action, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::ConfigUpdate(token_config_update) => { + let (batched_action, fee_result) = TokenConfigUpdateTransitionAction::try_from_borrowed_token_config_update_transition_with_contract_lookup(drive, owner_id, token_config_update, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Claim(claim) => { + let (batched_action, fee_result) = TokenClaimTransitionAction::try_from_borrowed_token_claim_transition_with_contract_lookup(drive, owner_id, claim, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::DirectPurchase(direct_purchase) => { + let (batched_action, fee_result) = TokenDirectPurchaseTransitionAction::try_from_borrowed_token_direct_purchase_transition_with_contract_lookup(drive, owner_id, direct_purchase, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::SetPriceForDirectPurchase(set_price_for_direct_purchase) => { + let (batched_action, fee_result) = TokenSetPriceForDirectPurchaseTransitionAction::try_from_borrowed_token_set_price_for_direct_purchase_transition_with_contract_lookup(drive, owner_id, set_price_for_direct_purchase, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + } + } + + /// Per-transition handler for document arms. + /// + /// Each per-transition failure path (ownership mismatch, revision + /// mismatch, missing target document, etc.) emits a nonce-bump action so + /// the submitter pays for validation work that has already run. + /// + /// The `user_fee_increase` argument passed into each + /// `BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition` + /// call is `0` deliberately: the value gets overridden by the outer + /// Documents Batch's `user_fee_increase` when the per-transition + /// action rolls up into the `BatchTransitionAction`, so any + /// per-site value would be discarded. + fn transform_document_transition_v1<'a>( + drive: &Drive, + transaction: TransactionArg, + validate_against_state: bool, + block_info: &BlockInfo, + data_contract_fetch_info: Arc, + transition: DocumentTransitionRefV1<'_>, + replaced_documents: &[Document], + user_fee_increase: UserFeeIncrease, + owner_id: Identifier, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error> { + if let Some(max_depth) = platform_version.system_limits.max_document_value_depth { + if let Some(actual_depth) = transition.first_data_depth_exceeding(max_depth as usize) { + return Self::failed_per_transition_action( + transition.base(), + owner_id, + vec![ConsensusError::BasicError(BasicError::ValueError( + ValueError::new_from_string(format!( + "document value depth {actual_depth} exceeds system maximum {max_depth}" + )), + ))], + platform_version, + ); + } + } + + match transition { + DocumentTransitionRefV1::Create(document_create_transition) => { + let (document_create_action, fee_result) = DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup( + drive, owner_id, transaction, + document_create_transition, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(document_create_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::Replace(document_replace_transition) => { + let validation_result = + Self::find_replaced_document_v1(transition, replaced_documents); + + if !validation_result.is_valid_with_data() { + // Keep this bump emission inline (not via Self::failed_per_transition_action): + // it's the one legacy v0 bump site, and routing it through the helper would + // drop the bump on PROTOCOL_VERSION_11 and diverge v11 chain replay (#2867). + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( + document_replace_transition.base(), + owner_id, + 0, + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + ), + validation_result.errors, + )); + } + + let original_document = validation_result.into_data()?; + + let validation_result = Self::check_ownership_of_old_replaced_document_v1( + document_replace_transition.base().id(), + original_document, + &owner_id, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_replace_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + if validate_against_state { + //there are situations where we don't want to validate this against the state + // for example when we already applied the state transition action + // and we are just validating it happened + let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( + document_replace_transition.revision(), + document_replace_transition.base().id(), + original_document, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_replace_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + } + + let (document_replace_action, fee_result) = + DocumentReplaceTransitionAction::try_from_borrowed_document_replace_transition( + document_replace_transition, + owner_id, + original_document, + block_info, + user_fee_increase, + |_identifier| Ok(data_contract_fetch_info.clone()), + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(document_replace_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::Delete(document_delete_transition) => { + let (batched_action, fee_result) = DocumentDeleteTransitionAction::try_from_document_borrowed_delete_transition_with_contract_lookup(document_delete_transition, owner_id, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + })?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::IndexOnlyDelete(document_index_only_delete_transition) => { + let (batched_action, fee_result) = DocumentIndexOnlyDeleteTransitionAction::try_from_document_borrowed_index_only_delete_transition_with_contract_lookup(document_index_only_delete_transition, owner_id, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + })?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::Erase(document_erase_transition) => { + let (batched_action, fee_result) = DocumentEraseTransitionAction::try_from_document_borrowed_erase_transition_with_contract_lookup(document_erase_transition, owner_id, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + })?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + DocumentTransitionRefV1::Transfer(document_transfer_transition) => { + let validation_result = + Self::find_replaced_document_v1(transition, replaced_documents); + + if !validation_result.is_valid_with_data() { + return Self::failed_per_transition_action( + document_transfer_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + let original_document = validation_result.into_data()?; + + let validation_result = Self::check_ownership_of_old_replaced_document_v1( + document_transfer_transition.base().id(), + original_document, + &owner_id, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_transfer_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + if validate_against_state { + //there are situations where we don't want to validate this against the state + // for example when we already applied the state transition action + // and we are just validating it happened + let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( + document_transfer_transition.revision(), + document_transfer_transition.base().id(), + original_document, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_transfer_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + } + + let (document_transfer_action, fee_result) = + DocumentTransferTransitionAction::try_from_borrowed_document_transfer_transition( + document_transfer_transition, + owner_id, + original_document.clone(), //todo: remove clone + block_info, + user_fee_increase, + |_identifier| Ok(data_contract_fetch_info.clone()), + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(document_transfer_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::UpdatePrice(document_update_price_transition) => { + let validation_result = + Self::find_replaced_document_v1(transition, replaced_documents); + + if !validation_result.is_valid_with_data() { + return Self::failed_per_transition_action( + document_update_price_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + let original_document = validation_result.into_data()?; + + let validation_result = Self::check_ownership_of_old_replaced_document_v1( + document_update_price_transition.base().id(), + original_document, + &owner_id, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_update_price_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + if validate_against_state { + //there are situations where we don't want to validate this against the state + // for example when we already applied the state transition action + // and we are just validating it happened + let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( + document_update_price_transition.revision(), + document_update_price_transition.base().id(), + original_document, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_update_price_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + } + + let (document_update_price_action, fee_result) = + DocumentUpdatePriceTransitionAction::try_from_borrowed_document_update_price_transition( + document_update_price_transition, + owner_id, + original_document.clone(), //todo: find a way to not have to use cloning + block_info, + user_fee_increase, + |_identifier| Ok(data_contract_fetch_info.clone()), + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(document_update_price_action.map(BatchedTransitionActionV1::Batched)) + } + DocumentTransitionRefV1::Purchase(document_purchase_transition) => { + let validation_result = + Self::find_replaced_document_v1(transition, replaced_documents); + + if !validation_result.is_valid_with_data() { + return Self::failed_per_transition_action( + document_purchase_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + + let original_document = validation_result.into_data()?; + + let Some(listed_price) = original_document + .properties() + .get_optional_integer::(PRICE)? + else { + return Self::failed_per_transition_action( + document_purchase_transition.base(), + owner_id, + vec![ + StateError::DocumentNotForSaleError(DocumentNotForSaleError::new( + original_document.id(), + )) + .into(), + ], + platform_version, + ); + }; + + if listed_price != document_purchase_transition.price() { + return Self::failed_per_transition_action( + document_purchase_transition.base(), + owner_id, + vec![StateError::DocumentIncorrectPurchasePriceError( + DocumentIncorrectPurchasePriceError::new( + original_document.id(), + document_purchase_transition.price(), + listed_price, + ), + ) + .into()], + platform_version, + ); + } + + if validate_against_state { + //there are situations where we don't want to validate this against the state + // for example when we already applied the state transition action + // and we are just validating it happened + let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( + document_purchase_transition.revision(), + document_purchase_transition.base().id(), + original_document, + ); + + if !validation_result.is_valid() { + return Self::failed_per_transition_action( + document_purchase_transition.base(), + owner_id, + validation_result.errors, + platform_version, + ); + } + } + + let (document_purchase_action, fee_result) = + DocumentPurchaseTransitionAction::try_from_borrowed_document_purchase_transition( + document_purchase_transition, + owner_id, + original_document.clone(), //todo: find a way to not have to use cloning + owner_id, + block_info, + user_fee_increase, + |_identifier| Ok(data_contract_fetch_info.clone()), + )?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(document_purchase_action.map(BatchedTransitionActionV1::Batched)) + } + } + } + + fn find_replaced_document_v1<'a>( + document_transition: DocumentTransitionRefV1<'_>, + fetched_documents: &'a [Document], + ) -> ConsensusValidationResult<&'a Document> { + let maybe_fetched_document = fetched_documents + .iter() + .find(|d| d.id() == document_transition.base().id()); + + if let Some(document) = maybe_fetched_document { + ConsensusValidationResult::new_with_data(document) + } else { + ConsensusValidationResult::new_with_error(ConsensusError::StateError( + StateError::DocumentNotFoundError(DocumentNotFoundError::new( + document_transition.base().id(), + )), + )) + } + } + + fn check_ownership_of_old_replaced_document_v1( + document_id: Identifier, + fetched_document: &Document, + owner_id: &Identifier, + ) -> SimpleConsensusValidationResult { + let mut result = SimpleConsensusValidationResult::default(); + if fetched_document.owner_id() != owner_id { + result.add_error(ConsensusError::StateError( + StateError::DocumentOwnerIdMismatchError(DocumentOwnerIdMismatchError::new( + document_id, + owner_id.to_owned(), + fetched_document.owner_id(), + )), + )); + } + result + } + fn check_revision_is_bumped_by_one_during_replace_v1( + transition_revision: Revision, + document_id: Identifier, + original_document: &Document, + ) -> SimpleConsensusValidationResult { + let mut result = SimpleConsensusValidationResult::default(); + + // If there was no previous revision this means that the document_type is not update-able + // However this should have been caught earlier + let Some(previous_revision) = original_document.revision() else { + result.add_error(ConsensusError::StateError( + StateError::InvalidDocumentRevisionError(InvalidDocumentRevisionError::new( + document_id, + None, + transition_revision, + )), + )); + return result; + }; + // no need to check bounds here, because it would be impossible to hit the end on a u64 + let expected_revision = previous_revision + 1; + if transition_revision != expected_revision { + result.add_error(ConsensusError::StateError( + StateError::InvalidDocumentRevisionError(InvalidDocumentRevisionError::new( + document_id, + Some(previous_revision), + transition_revision, + )), + )) + } + result + } + + fn failed_per_transition_action( + base_transition: &dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition, + owner_id: Identifier, + errors: Vec, + _platform_version: &PlatformVersion, + ) -> Result, Error> { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( + base_transition, + owner_id, + 0, + ); + Ok(ConsensusValidationResult::new_with_data_and_errors( + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + ), + errors, + )) + } +} diff --git a/packages/rs-drive-abci/src/query/document_history/mod.rs b/packages/rs-drive-abci/src/query/document_history/mod.rs index 0e6fd2d30ee..489aa551761 100644 --- a/packages/rs-drive-abci/src/query/document_history/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/mod.rs @@ -9,6 +9,7 @@ use dapi_grpc::platform::v0::{GetDocumentHistoryRequest, GetDocumentHistoryRespo use dpp::version::PlatformVersion; mod v0; +mod v1; impl Platform { /// Querying of a document history. @@ -43,8 +44,27 @@ impl Platform { match version { RequestVersion::V0(request) => { - let result = - self.query_document_history_v0(request, platform_state, platform_version)?; + let result = match platform_version + .drive_abci + .query + .document_history_processing + { + 0 => { + self.query_document_history_v0(request, platform_state, platform_version)? + } + 1 => { + self.query_document_history_v1(request, platform_state, platform_version)? + } + processing_version => { + return Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "query_document_history".to_string(), + known_versions: vec![0, 1], + received: processing_version, + }, + )); + } + }; Ok(result.map(|response| GetDocumentHistoryResponse { version: Some(ResponseVersion::V0(response)), })) diff --git a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs index 5eca888f9f5..b27b010d982 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs @@ -138,15 +138,13 @@ impl Platform { lifecycle: history.lifecycle.map(|lifecycle| Lifecycle { state: match lifecycle.state { DocumentHistoryState::Active => State::Active, - DocumentHistoryState::Deleted => State::Deleted, - DocumentHistoryState::Erasing => State::Erasing, DocumentHistoryState::Absent => State::Absent, + DocumentHistoryState::Deleted | DocumentHistoryState::Erasing => { + State::Absent + } } as i32, remaining_revisions: lifecycle.remaining_revisions, - deleted_at_ms: history.lifecycle.times.deleted_at_ms, - erasing_started_at_ms: history.lifecycle.times.erasing_started_at_ms, - erasing_from_time_ms: history.lifecycle.times.erasing_from_time_ms, - erasing_from_revision: history.lifecycle.times.erasing_from_revision, + ..Default::default() }), }) }; diff --git a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs index 7c5175aeac9..5069d151b0f 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs @@ -415,6 +415,8 @@ fn history_api_proof_round_trip(gapped: bool) { match expected_lifecycle.state { DocumentHistoryState::Active => State::Active, DocumentHistoryState::Absent => State::Absent, + DocumentHistoryState::Deleted => State::Deleted, + DocumentHistoryState::Erasing => State::Erasing, } as i32 ); let with_proofs = |mut response: GetDocumentHistoryResponseV0, @@ -577,232 +579,3 @@ fn should_reject_missing_selectors_before_reading_state() { .unwrap() .is_valid()); } - -/// A response that claims a lifecycle other than the one its proofs establish -/// is rejected field by field, for a deleted document and for one whose -/// erasure is under way, not only for a current one. -#[test] -fn should_reject_tampered_lifecycle_claims_for_deleted_and_erasing_histories() { - use dapi_grpc::platform::v0::get_document_history_request::get_document_history_request_v0::Selector; - use dapi_grpc::platform::v0::get_document_history_request::GetDocumentHistoryRequestV0; - use dapi_grpc::platform::v0::get_document_history_response::GetDocumentHistoryResponseV0; - use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; - use drive_proof_verifier::types::DocumentHistoryState; - - let (platform, state, version) = setup_platform(None, Network::Testnet, None); - let mut state = state.as_ref().clone(); - let contract = json_document_to_contract( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json" - ), - false, - version, - ) - .unwrap(); - platform - .drive - .apply_contract(&contract, BlockInfo::default(), true, None, None, version) - .unwrap(); - let document_type = contract.document_type_for_name("profile").unwrap(); - let owner = Identifier::from([8; 32]); - let mut document = json_document_to_document( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" - ), - Some(owner), - document_type, - version, - ) - .unwrap(); - // Enough revisions that one erase chunk leaves some behind. - let chunk = version - .system_limits - .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the erase chunk") as u64; - for revision in 1..=chunk + 2 { - document.set_revision(Some(revision)); - platform - .drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentInfo::DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - revision > 1, - BlockInfo::default_with_time(1_000 + revision), - true, - None, - version, - None, - ) - .unwrap(); - } - let key = SecretKey::::from_hash(b"document-history-quorum"); - let provider = Provider { - contract: Arc::new(contract.clone()), - key: key.public_key().to_bytes().try_into().unwrap(), - }; - state.last_committed_block_info = Some( - dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { - basic_info: BlockInfo { - height: 42, - core_height: 12, - time_ms: 3000, - epoch: Default::default(), - }, - app_hash: [0; 32], - quorum_hash: [9; 32], - block_id_hash: [7; 32], - proposer_pro_tx_hash: [0; 32], - signature: [0; 96], - round: 0, - } - .into(), - ); - let apply = |batch| { - platform - .drive - .apply_batch_low_level_drive_operations(None, None, batch, &mut vec![], &version.drive) - .unwrap(); - }; - let wire_request = GetDocumentHistoryRequestV0 { - data_contract_id: contract.id().to_vec(), - document_type_name: "profile".into(), - document_id: document.id().to_vec(), - limit: None, - prove: true, - selector: Some(Selector::StartAtMs(0)), - }; - let request: GetDocumentHistoryRequest = wire_request.clone().into(); - - // Delete, then start an erasure; after each, sign the new root and check - // that the honest response verifies while every altered claim does not. - let stages: [(&str, Box, DocumentHistoryState); 2] = [ - ( - "deleted", - Box::new(|| { - apply( - platform - .drive - .delete_document_for_contract_operations( - document.id(), - &contract, - document_type, - &BlockInfo::default_with_time(5_000), - Some(owner), - None, - &mut None, - None, - version, - ) - .unwrap(), - ) - }), - DocumentHistoryState::Deleted, - ), - ( - "erasing", - Box::new(|| { - apply( - platform - .drive - .erase_document_for_contract_operations( - document.id(), - &contract, - document_type, - &BlockInfo::default_with_time(6_000), - &mut None, - None, - version, - ) - .unwrap(), - ) - }), - DocumentHistoryState::Erasing, - ), - ]; - for (stage, advance, expected_state) in stages { - advance(); - let root = platform - .drive - .grove - .root_hash(None, &version.drive.grove_version) - .value - .unwrap(); - let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); - let signed = signed_proof( - vec![], - root, - &metadata, - &key, - platform.config.validator_set.quorum_type as u32, - ); - let committed = state.last_committed_block_info.as_mut().unwrap(); - committed.set_app_hash(root); - committed.set_signature(signed.signature.try_into().unwrap()); - - let response = platform - .query_document_history_v0(wire_request.clone(), &state, version) - .unwrap() - .into_data() - .unwrap(); - let verify = |response: GetDocumentHistoryResponseV0| { - DocumentHistory::maybe_from_proof( - request.clone(), - GetDocumentHistoryResponse::from(response), - Network::Testnet, - version, - &provider, - ) - }; - let honest = verify(response.clone()) - .unwrap_or_else(|error| panic!("{stage}: honest response must verify: {error}")) - .unwrap(); - let lifecycle = honest - .lifecycle - .expect("the history response always carries a lifecycle"); - assert_eq!(lifecycle.state, expected_state, "{stage}"); - assert_eq!(lifecycle.times.deleted_at_ms, 5_000, "{stage}"); - - let tamperings: Vec<(&str, Box)> = vec![ - ( - "state", - Box::new(|r| r.lifecycle.as_mut().unwrap().state = 0), - ), - ( - "remaining_revisions", - Box::new(|r| r.lifecycle.as_mut().unwrap().remaining_revisions += 1), - ), - ( - "deleted_at_ms", - Box::new(|r| r.lifecycle.as_mut().unwrap().deleted_at_ms += 1), - ), - ( - "erasing_started_at_ms", - Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_started_at_ms += 1), - ), - ( - "erasing_from_time_ms", - Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_from_time_ms += 1), - ), - ( - "erasing_from_revision", - Box::new(|r| r.lifecycle.as_mut().unwrap().erasing_from_revision += 1), - ), - ]; - for (field, tamper) in tamperings { - let mut tampered = response.clone(); - tamper(&mut tampered); - assert!( - verify(tampered).is_err(), - "{stage}: a response whose {field} differs from its proof must be rejected" - ); - } - } -} diff --git a/packages/rs-drive-abci/src/query/document_history/v1/mod.rs b/packages/rs-drive-abci/src/query/document_history/v1/mod.rs new file mode 100644 index 00000000000..5d9abafa73f --- /dev/null +++ b/packages/rs-drive-abci/src/query/document_history/v1/mod.rs @@ -0,0 +1,163 @@ +use crate::error::{query::QueryError, Error}; +use crate::platform_types::{platform::Platform, platform_state::PlatformState}; +use crate::query::{response_metadata::CheckpointUsed, QueryValidationResult}; +use dapi_grpc::platform::v0::get_document_history_request::{ + get_document_history_request_v0::Filter, GetDocumentHistoryRequestV0, +}; +use dapi_grpc::platform::v0::get_document_history_response::{ + get_document_history_response_v0::{ + lifecycle::State, Entry, History, Lifecycle, Result as ResponseResult, + }, + GetDocumentHistoryResponseV0, +}; +use dpp::check_validation_result_with_data; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; +use dpp::validation::ValidationResult; +use dpp::version::PlatformVersion; +use drive::query::document_history_drive_query::{ + DocumentHistoryDriveQuery, DocumentHistoryFilter, DocumentHistoryState, +}; +use drive::util::grove_operations::GroveDBToUse; + +impl Platform { + pub(super) fn query_document_history_v1( + &self, + request: GetDocumentHistoryRequestV0, + platform_state: &PlatformState, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let contract_id = + check_validation_result_with_data!(request.data_contract_id.try_into().map_err(|_| { + QueryError::InvalidArgument("data_contract_id must be 32 bytes".to_owned()) + })); + let document_id = check_validation_result_with_data!(request + .document_id + .try_into() + .map_err(|_| QueryError::InvalidArgument("document_id must be 32 bytes".to_owned()))); + let filter = check_validation_result_with_data!(request.filter.ok_or_else(|| { + QueryError::InvalidArgument("exactly one history filter is required".to_owned()) + })); + let filter = match filter { + Filter::StartAtMs(time) => DocumentHistoryFilter::StartAtTime(time), + Filter::StartAfter(cursor) => DocumentHistoryFilter::StartAfter { + time_ms: cursor.time_ms, + revision: cursor.revision, + }, + Filter::StartAtRevision(revision) => DocumentHistoryFilter::StartAtRevision(revision), + Filter::Revision(revision) => DocumentHistoryFilter::Revision(revision), + }; + let limit = check_validation_result_with_data!(request + .limit + .map(u16::try_from) + .transpose() + .map_err(|_| QueryError::InvalidArgument("history limit out of bounds".to_owned()))); + let query = DocumentHistoryDriveQuery { + contract_id, + document_type_name: request.document_type_name, + document_id, + filter, + limit, + }; + check_validation_result_with_data!(query + .validate() + .map_err(|error| QueryError::InvalidArgument(error.to_string()))); + let fetched = self + .drive + .fetch_contract(contract_id, None, None, None, platform_version) + .value?; + let fetched = check_validation_result_with_data!( + fetched.ok_or_else(|| QueryError::NotFound("data contract not found".to_owned())) + ); + let contract = &fetched.contract; + let document_type = check_validation_result_with_data!(contract + .document_type_for_name(&query.document_type_name) + .map_err(|_| QueryError::NotFound("document type not found".to_owned()))); + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + if !document_type.documents_keep_history() { + return Ok(QueryValidationResult::new_with_error( + QueryError::InvalidArgument("document type does not keep history".to_owned()), + )); + } + // A query Drive refuses (a revision filter over a gapped history, for + // one) is the caller's mistake and answers as an invalid argument; + // storage and corruption errors still propagate. + let result = if request.prove { + let proof = match self.drive.prove_document_history( + &query, + document_type, + None, + platform_version, + ) { + Ok(proved) => proved, + Err(drive::error::Error::Query(query_error)) => { + return Ok(QueryValidationResult::new_with_error(QueryError::Query( + query_error, + ))); + } + Err(error) => return Err(error.into()), + }; + // The proof travels in the layout the protocol version stores, + // signed once; from protocol version 14 that is one proof object + // carrying both GroveDB proofs. + let proof = self + .response_proof_v0(platform_state, proof, GroveDBToUse::Current)? + .1; + ResponseResult::Proof(proof) + } else { + let history = match self.drive.fetch_document_history( + &query, + document_type, + None, + platform_version, + ) { + Ok(history) => history, + Err(drive::error::Error::Query(query_error)) => { + return Ok(QueryValidationResult::new_with_error(QueryError::Query( + query_error, + ))); + } + Err(error) => return Err(error.into()), + }; + let entries = history + .entries + .into_iter() + .map(|entry| { + Ok(Entry { + time_ms: entry.time_ms, + revision: entry.revision, + document: entry + .document + .serialize(document_type, contract, platform_version) + .map_err(Error::Protocol)?, + }) + }) + .collect::, Error>>()?; + ResponseResult::History(History { + entries, + lifecycle: history.lifecycle.map(|lifecycle| Lifecycle { + state: match lifecycle.state { + DocumentHistoryState::Active => State::Active, + DocumentHistoryState::Deleted => State::Deleted, + DocumentHistoryState::Erasing => State::Erasing, + DocumentHistoryState::Absent => State::Absent, + } as i32, + remaining_revisions: lifecycle.remaining_revisions, + deleted_at_ms: lifecycle.times.deleted_at_ms, + erasing_started_at_ms: lifecycle.times.erasing_started_at_ms, + erasing_from_time_ms: lifecycle.times.erasing_from_time_ms, + erasing_from_revision: lifecycle.times.erasing_from_revision, + }), + }) + }; + Ok(QueryValidationResult::new_with_data( + GetDocumentHistoryResponseV0 { + result: Some(result), + metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)), + }, + )) + } +} + +#[cfg(test)] +mod tests; diff --git a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs new file mode 100644 index 00000000000..f2bba6a8ef0 --- /dev/null +++ b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs @@ -0,0 +1,683 @@ +use super::*; +use crate::query::tests::setup_platform; +use dapi_grpc::platform::v0::get_document_history_request::get_document_history_request_v0::Cursor; +use dapi_grpc::platform::v0::{ + GetDocumentHistoryRequest, GetDocumentHistoryResponse, Proof, ResponseMetadata, +}; +use dpp::block::block_info::BlockInfo; +use dpp::bls_signatures::{Bls12381G2Impl, SecretKey, SignatureSchemes}; +use dpp::dashcore::Network; +use dpp::data_contract::TokenConfiguration; +use dpp::document::{DocumentV0Getters, DocumentV0Setters}; +use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; +use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; +use drive::util::object_size_info::{DocumentAndContractInfo, DocumentInfo, OwnedDocumentInfo}; +use drive::verify::document::DocumentHistoryProof; +use drive_proof_verifier::types::DocumentHistory; +use drive_proof_verifier::{ContextProvider, ContextProviderError, FromProof}; +use std::sync::Arc; +use tenderdash_abci::{ + proto::types::{CanonicalVote, SignedMsgType, StateId}, + signatures::{Hashable, Signable}, +}; + +struct Provider { + contract: Arc, + key: [u8; 48], +} +impl ContextProvider for Provider { + fn get_data_contract( + &self, + id: &Identifier, + _: &PlatformVersion, + ) -> Result>, ContextProviderError> { + Ok((id == &self.contract.id()).then(|| self.contract.clone())) + } + fn get_token_configuration( + &self, + _: &Identifier, + ) -> Result, ContextProviderError> { + Ok(None) + } + fn get_quorum_public_key( + &self, + _: u32, + _: [u8; 32], + _: u32, + ) -> Result<[u8; 48], ContextProviderError> { + Ok(self.key) + } + fn get_platform_activation_height(&self) -> Result { + Ok(1) + } +} + +fn signed_proof( + bytes: Vec, + root: [u8; 32], + metadata: &ResponseMetadata, + key: &SecretKey, + quorum_type: u32, +) -> Proof { + let state = StateId { + app_version: metadata.protocol_version as u64, + core_chain_locked_height: metadata.core_chain_locked_height, + time: metadata.time_ms, + app_hash: root.to_vec(), + height: metadata.height, + }; + let commit = CanonicalVote { + r#type: SignedMsgType::Precommit.into(), + block_id: vec![7; 32], + chain_id: metadata.chain_id.clone(), + height: metadata.height as i64, + round: 0, + state_id: state + .calculate_msg_hash(&metadata.chain_id, metadata.height as i64, 0) + .unwrap(), + }; + let digest = commit + .calculate_sign_hash( + &metadata.chain_id, + quorum_type.try_into().unwrap(), + &[9; 32], + metadata.height as i64, + 0, + ) + .unwrap(); + Proof { + grovedb_proof: bytes, + quorum_hash: vec![9; 32], + signature: key + .sign(SignatureSchemes::Basic, &digest) + .unwrap() + .as_raw_value() + .to_compressed() + .to_vec(), + round: 0, + block_id_hash: vec![7; 32], + quorum_type, + } +} + +/// The proof a proved history response answers with; a proved response never +/// carries the history itself. +fn proof_mut(response: &mut GetDocumentHistoryResponseV0) -> &mut Proof { + match response.result.as_mut() { + Some(ResponseResult::Proof(proof)) => proof, + _ => panic!("a proved history query answers with a proof"), + } +} + +#[test] +fn should_round_trip_history_api_through_quorum_and_grove_proof_verification() { + history_api_proof_round_trip(false); +} + +#[test] +fn should_reject_legacy_proof_envelopes_in_history_v1_through_sdk() { + history_api_proof_round_trip(true); +} + +fn history_api_proof_round_trip(gapped: bool) { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let mut state = state.as_ref().clone(); + let contract = json_document_to_contract(concat!(env!("CARGO_MANIFEST_DIR"), "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json"), false, version).unwrap(); + platform + .drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .unwrap(); + let document_type = contract.document_type_for_name("profile").unwrap(); + let mut document = json_document_to_document( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" + ), + Some([8; 32].into()), + document_type, + version, + ) + .unwrap(); + for revision in 1..=3 { + if gapped && revision == 2 { + continue; + } + document.set_revision(Some(revision)); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(2000), + true, + None, + version, + None, + ) + .unwrap(); + } + let key = SecretKey::::from_hash(b"document-history-quorum"); + let provider = Provider { + contract: Arc::new(contract.clone()), + key: key.public_key().to_bytes().try_into().unwrap(), + }; + state.last_committed_block_info = Some( + dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { + basic_info: BlockInfo { + height: 42, + core_height: 12, + time_ms: 3000, + epoch: Default::default(), + }, + app_hash: [0; 32], + quorum_hash: [9; 32], + block_id_hash: [7; 32], + proposer_pro_tx_hash: [0; 32], + signature: [0; 96], + round: 0, + } + .into(), + ); + let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); + let root = platform + .drive + .grove + .root_hash(None, &version.drive.grove_version) + .value + .unwrap(); + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; + let signed = signed_proof( + vec![], + root, + &metadata, + &key, + platform.config.validator_set.quorum_type as u32, + ); + let committed = state.last_committed_block_info.as_mut().unwrap(); + committed.set_app_hash(root); + committed.set_signature(signed.signature.try_into().unwrap()); + if gapped { + for (wire_selector, filter) in [ + (Filter::Revision(3), DocumentHistoryFilter::Revision(3)), + ( + Filter::StartAtRevision(3), + DocumentHistoryFilter::StartAtRevision(3), + ), + ] { + let request = GetDocumentHistoryRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type_name: "profile".into(), + document_id: document.id().to_vec(), + limit: None, + prove: true, + filter: Some(wire_selector), + }; + // Drive refuses revision reads over a gapped history; the handler + // must answer that as an invalid argument, not as a node error + // the client would retry. + for prove in [true, false] { + let mut refused = request.clone(); + refused.prove = prove; + let result = platform + .query_document_history_v1(refused, &state, version) + .expect("a refused revision read is a query error, not a node error"); + assert!(!result.is_valid()); + assert!( + matches!(result.errors.first(), Some(QueryError::Query(_))), + "prove={prove}: {:?}", + result.errors + ); + } + let mut time_request = request.clone(); + time_request.filter = Some(Filter::StartAtMs(0)); + let mut response = platform + .query_document_history_v1(time_request, &state, version) + .unwrap() + .into_data() + .unwrap(); + let query = DocumentHistoryDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "profile".into(), + document_id: document.id().to_buffer(), + filter, + limit: None, + }; + let proof = proof_mut(&mut response); + let mut proofs = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); + proofs.metadata_proof = + drive::util::test_helpers::history_proof::downgrade_history_count( + &proofs.metadata_proof, + Some(3), + version, + ); + proofs.entries_proof = Some( + platform + .drive + .grove_get_proved_path_query( + &query.construct_path_query(version).unwrap(), + None, + &mut vec![], + &version.drive, + ) + .unwrap(), + ); + proof.grovedb_proof = proofs.to_bytes().unwrap(); + let error = DocumentHistory::maybe_from_proof( + request.clone(), + response.clone(), + Network::Testnet, + version, + &provider, + ) + .expect_err("document history requires GroveDB v1 even with a valid quorum signature"); + assert!(matches!( + error, + drive_proof_verifier::Error::UnsupportedGroveDBProofVersion { .. } + )); + assert!(drive_proof_verifier::types::DocumentHistoryProofInfo::maybe_from_proof_with_metadata( + request, response, Network::Testnet, version, &provider, + ).is_err()); + } + return; + } + let selections = [ + ( + document.id().to_buffer(), + Filter::StartAtMs(2000), + DocumentHistoryFilter::StartAtTime(2000), + ), + ( + document.id().to_buffer(), + Filter::StartAfter(Cursor { + time_ms: 2000, + revision: 1, + }), + DocumentHistoryFilter::StartAfter { + time_ms: 2000, + revision: 1, + }, + ), + ( + document.id().to_buffer(), + Filter::Revision(2), + DocumentHistoryFilter::Revision(2), + ), + ( + document.id().to_buffer(), + Filter::StartAtRevision(65535), + DocumentHistoryFilter::StartAtRevision(65535), + ), + ( + [255; 32], + Filter::Revision(2), + DocumentHistoryFilter::Revision(2), + ), + ]; + for (id, wire_selector, filter) in selections { + let request = GetDocumentHistoryRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type_name: "profile".into(), + document_id: id.to_vec(), + limit: None, + prove: true, + filter: Some(wire_selector), + }; + let request_v0 = request.clone(); + let response = platform + .query_document_history_v1(request.clone(), &state, version) + .unwrap() + .into_data() + .unwrap(); + let query = DocumentHistoryDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "profile".into(), + document_id: id, + limit: None, + filter, + }; + let expected = platform + .drive + .fetch_document_history(&query, document_type, None, version) + .unwrap(); + let request: GetDocumentHistoryRequest = request.into(); + let verify = |response: GetDocumentHistoryResponseV0| { + DocumentHistory::maybe_from_proof( + request.clone(), + GetDocumentHistoryResponse::from(response), + Network::Testnet, + version, + &provider, + ) + }; + let (exported, _, _) = + drive_proof_verifier::types::DocumentHistoryProofInfo::maybe_from_proof_with_metadata( + request.clone(), + GetDocumentHistoryResponse::from(response.clone()), + Network::Testnet, + version, + &provider, + ) + .unwrap(); + let exported = exported.unwrap(); + assert_eq!(exported.response, response); + assert_eq!( + exported + .verify(request.clone(), Network::Testnet, version, &provider) + .unwrap(), + Some(exported.history) + ); + let result = verify(response.clone()).unwrap().unwrap(); + assert_eq!(result.entries, expected.entries); + assert_eq!(result.lifecycle, expected.lifecycle.clone()); + // Without a proof the same query answers with the history itself. + let mut plain_request = request_v0.clone(); + plain_request.prove = false; + let plain = platform + .query_document_history_v1(plain_request, &state, version) + .unwrap() + .into_data() + .unwrap(); + let Some(ResponseResult::History(history)) = plain.result else { + panic!("an unproved history query answers with the history"); + }; + assert_eq!( + history + .entries + .iter() + .map(|entry| (entry.time_ms, entry.revision)) + .collect::>(), + expected + .entries + .iter() + .map(|entry| (entry.time_ms, entry.revision)) + .collect::>() + ); + let lifecycle = history + .lifecycle + .expect("the history carries its lifecycle"); + let expected_lifecycle = expected + .lifecycle + .as_ref() + .expect("pages of the history tree carry a lifecycle"); + assert_eq!( + lifecycle.remaining_revisions, + expected_lifecycle.remaining_revisions + ); + assert_eq!( + lifecycle.state, + match expected_lifecycle.state { + DocumentHistoryState::Active => State::Active, + DocumentHistoryState::Deleted => State::Deleted, + DocumentHistoryState::Erasing => State::Erasing, + DocumentHistoryState::Absent => State::Absent, + } as i32 + ); + let with_proofs = |mut response: GetDocumentHistoryResponseV0, + edit: &dyn Fn(&mut DocumentHistoryProof)| { + let proof = proof_mut(&mut response); + let mut proofs = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); + edit(&mut proofs); + proof.grovedb_proof = proofs.to_bytes().unwrap(); + response + }; + let legacy_metadata = with_proofs(response.clone(), &|proofs| { + proofs.metadata_proof = + drive::util::test_helpers::history_proof::downgrade_history_count( + &proofs.metadata_proof, + None, + version, + ); + }); + let error = + verify(legacy_metadata).expect_err("metadata always requires a GroveDB v1 envelope"); + assert!(matches!( + error, + drive_proof_verifier::Error::UnsupportedGroveDBProofVersion { .. } + )); + let mut torn = response.clone(); + proof_mut(&mut torn).grovedb_proof.push(0); + assert!( + verify(torn).is_err(), + "a proof envelope with trailing bytes is not the proof the node produced" + ); + let mut bad_signature = response.clone(); + proof_mut(&mut bad_signature).signature[0] ^= 1; + assert!(verify(bad_signature).is_err()); + let has_entries_proof = { + let mut probe = response.clone(); + DocumentHistoryProof::from_bytes(&proof_mut(&mut probe).grovedb_proof) + .unwrap() + .entries_proof + .is_some() + }; + if has_entries_proof { + let downgraded = with_proofs(response.clone(), &|proofs| { + let bytes = proofs.entries_proof.as_ref().unwrap(); + proofs.entries_proof = Some( + drive::util::test_helpers::history_proof::downgrade_history_count( + bytes, None, version, + ), + ); + }); + assert!( + verify(downgraded).is_err(), + "entries also require an authenticated V1 envelope" + ); + let missing = with_proofs(response.clone(), &|proofs| proofs.entries_proof = None); + assert!(verify(missing).is_err()); + } + } +} + +#[test] +fn should_reject_missing_selectors_before_reading_state() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let request = GetDocumentHistoryRequestV0 { + data_contract_id: vec![1; 32], + document_type_name: "note".into(), + document_id: vec![2; 32], + filter: None, + limit: None, + prove: false, + }; + assert!(!platform + .query_document_history_v1(request, &state, version) + .unwrap() + .is_valid()); +} + +#[test] +fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let mut state = state.as_ref().clone(); + let contract = json_document_to_contract( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json" + ), + false, + version, + ) + .unwrap(); + platform + .drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .unwrap(); + let document_type = contract.document_type_for_name("profile").unwrap(); + let owner = Identifier::from([8; 32]); + let mut document = json_document_to_document( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" + ), + Some(owner), + document_type, + version, + ) + .unwrap(); + let chunk = version + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 14 bounds the erase chunk") as u64; + for revision in 1..=chunk + 2 { + document.set_revision(Some(revision)); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .unwrap(); + } + + let key = SecretKey::::from_hash(b"document-history-quorum"); + let provider = Provider { + contract: Arc::new(contract.clone()), + key: key.public_key().to_bytes().try_into().unwrap(), + }; + state.last_committed_block_info = Some( + dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { + basic_info: BlockInfo { + height: 42, + core_height: 12, + time_ms: 3_000, + epoch: Default::default(), + }, + app_hash: [0; 32], + quorum_hash: [9; 32], + block_id_hash: [7; 32], + proposer_pro_tx_hash: [0; 32], + signature: [0; 96], + round: 0, + } + .into(), + ); + let wire_request = GetDocumentHistoryRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type_name: "profile".into(), + document_id: document.id().to_vec(), + limit: None, + prove: true, + filter: Some(Filter::StartAtMs(0)), + }; + let request: GetDocumentHistoryRequest = wire_request.clone().into(); + + for (stage, expected_state) in [ + ("deleted", DocumentHistoryState::Deleted), + ("erasing", DocumentHistoryState::Erasing), + ] { + let operations = if stage == "deleted" { + platform + .drive + .delete_document_for_contract_operations_with_lifecycle( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(5_000), + Some(owner), + None, + &mut None, + 5_000, + None, + version, + ) + .unwrap() + } else { + platform + .drive + .erase_document_for_contract_operations( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(6_000), + &mut None, + None, + version, + ) + .unwrap() + }; + platform + .drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &version.drive, + ) + .unwrap(); + + let root = platform + .drive + .grove + .root_hash(None, &version.drive.grove_version) + .value + .unwrap(); + let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); + let signed = signed_proof( + vec![], + root, + &metadata, + &key, + platform.config.validator_set.quorum_type as u32, + ); + let committed = state.last_committed_block_info.as_mut().unwrap(); + committed.set_app_hash(root); + committed.set_signature(signed.signature.try_into().unwrap()); + + let response = platform + .query_document_history_v1(wire_request.clone(), &state, version) + .unwrap() + .into_data() + .unwrap(); + let verify = |response: GetDocumentHistoryResponseV0| { + DocumentHistory::maybe_from_proof( + request.clone(), + GetDocumentHistoryResponse::from(response), + Network::Testnet, + version, + &provider, + ) + }; + let history = verify(response.clone()) + .unwrap_or_else(|error| panic!("{stage}: honest response must verify: {error}")) + .expect("the proof authenticates a history"); + let lifecycle = history + .lifecycle + .expect("the proof authenticates the lifecycle"); + assert_eq!(lifecycle.state, expected_state, "{stage}"); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000, "{stage}"); + + let mut tampered = response; + let proof = proof_mut(&mut tampered); + let mut envelope = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); + let middle = envelope.metadata_proof.len() / 2; + envelope.metadata_proof[middle] ^= 1; + proof.grovedb_proof = envelope.to_bytes().unwrap(); + assert!( + verify(tampered).is_err(), + "{stage}: changing the proof that authenticates lifecycle must be rejected" + ); + } +} diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs index f78e7a8371e..f6d5802f25c 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs @@ -1387,7 +1387,7 @@ mod tests { }; apply( - DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { + DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { document_id, deleter_id: Some(dpp::identifier::Identifier::new([3u8; 32])), contract_info: contract_info(), diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index fb51b793486..00bb6fec1db 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -24,6 +24,7 @@ use drive::error::proof::ProofError; use drive::error::Error as DriveError; use drive::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use drive::state_transition_action::batch::v1::BatchedTransitionActionV1; use drive::state_transition_action::StateTransitionAction; use drive_abci::execution::validation::state_transition::transformer::StateTransitionActionTransformer; use drive_abci::platform_types::platform::PlatformRef; @@ -451,14 +452,34 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( ); } } - StateTransitionAction::BatchAction(batch_transition) => { - if batch_transition.transitions().is_empty() { - panic!("we should have at least one transition"); - } - - let Some(transition_action) = batch_transition.transitions().first() else { + StateTransitionAction::BatchAction(_) | StateTransitionAction::BatchActionV1(_) => { + // Both batch action formats are checked the same way: a + // format 1 action wraps the format 0 items and may add an + // erase, which strategies never generate. + let (batch_owner_id, transition_action) = match action { + StateTransitionAction::BatchAction(batch_transition) => ( + batch_transition.owner_id(), + batch_transition + .transitions() + .first() + .cloned() + .map(BatchedTransitionActionV1::Batched), + ), + StateTransitionAction::BatchActionV1(batch_transition) => ( + batch_transition.owner_id(), + batch_transition.transitions().first().cloned(), + ), + _ => unreachable!("the outer match only admits batch actions here"), + }; + let Some(transition_action) = transition_action else { panic!("we should have at least one transition"); }; + let transition_action = match &transition_action { + BatchedTransitionActionV1::Batched(transition_action) => transition_action, + BatchedTransitionActionV1::DocumentErase(_) => { + panic!("strategies do not generate erases"); + } + }; match transition_action { BatchedTransitionAction::DocumentAction(document_action) => { let contract_fetch_info = @@ -536,7 +557,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( // &document, // Document::try_from_create_transition( // creation_action, - // documents_batch_transition.owner_id(), + // documents_batch_owner_id, // platform_version, // ) // .expect("expected to get document") @@ -544,7 +565,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let mut expected_document = Document::try_from_create_transition_action( creation_action, - batch_transition.owner_id(), + batch_owner_id, platform_version, ) .expect("expected to get document"); @@ -576,7 +597,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let mut expected_document = Document::try_from_replace_transition_action( replace_action, - batch_transition.owner_id(), + batch_owner_id, platform_version, ) .expect("expected to get document"); @@ -600,7 +621,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( document, Document::try_from_replace_transition_action( replace_action, - batch_transition.owner_id(), + batch_owner_id, platform_version, ) .expect("expected to get document") @@ -613,14 +634,6 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( // we expect no document assert!(document.is_none()); } - DocumentTransitionAction::EraseAction(_) => { - // An erase acts on a document that was - // already deleted, so the by-id query this - // harness proves against returns nothing - // whether or not the erase executed; - // strategies do not generate erases today. - assert!(document.is_none()); - } DocumentTransitionAction::IndexOnlyDeleteAction(_) => { // indexOnly documents never have a // primary-storage row, so the by-id query @@ -714,7 +727,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( document_type_name, document_type_keeps_history: false, document_id: token_transition_action - .historical_document_id(batch_transition.owner_id()) + .historical_document_id(batch_owner_id) .to_buffer(), block_time_ms: None, //None because we want latest contested_status: @@ -744,7 +757,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let expected_document = token_transition_action .build_historical_document( token_id, - batch_transition.owner_id(), + batch_owner_id, token_transition_action.base().identity_contract_nonce(), platform .state diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs index f3964a55455..531272d06aa 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs @@ -176,7 +176,6 @@ fn delete_op<'a>( document_id: Identifier, ) -> DriveOperation<'a> { DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { - deleter_id: None, document_id, contract_info: DataContractInfo::BorrowedDataContract(contract), document_type_info: DocumentTypeInfo::DocumentTypeNameAsStr(doctype), diff --git a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs index 56e0434db41..08f3168acd6 100644 --- a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs @@ -146,8 +146,6 @@ impl Drive { *doc_id, &contract, document_type, - block_info, - None, None, estimated_costs_only_with_layer_info, block_info.time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs index 616d27de6c6..f28fd284ba7 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -59,9 +60,19 @@ impl Drive { platform_version, previous_fee_versions, ), + 1 => self.delete_document_for_contract_v1( + document_id, + contract, + document_type_name, + block_info, + apply, + transaction, + platform_version, + previous_fee_versions, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs index a9f52838766..b82d4b25c51 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs @@ -37,10 +37,6 @@ impl Drive { document_id, contract, document_type_name, - &block_info, - // These fee-applying wrappers carry no signer, so a lifecycle record - // they write belongs to no identity and refunds nobody. - None, estimated_costs_only_with_layer_info, block_info.time_ms, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs new file mode 100644 index 00000000000..5cf14f06148 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs @@ -0,0 +1,60 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; +use dpp::fee::fee_result::FeeResult; + +use dpp::fee::default_costs::CachedEpochIndexFeeVersions; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Deletes a document and returns the associated fee. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + block_info: BlockInfo, + apply: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, + ) -> Result { + let mut drive_operations: Vec = vec![]; + let estimated_costs_only_with_layer_info = if apply { + None::> + } else { + Some(HashMap::new()) + }; + self.delete_document_for_contract_apply_and_add_to_operations_v1( + document_id, + contract, + document_type_name, + &block_info, + // These fee-applying wrappers carry no signer, so a lifecycle record + // they write belongs to no identity and refunds nobody. + None, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + &mut drive_operations, + platform_version, + )?; + let fees = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + self.config.epochs_per_era, + platform_version, + previous_fee_versions, + )?; + Ok(fees) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs index 0192d6284ba..343bea486c0 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs @@ -1,10 +1,10 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -35,8 +35,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, estimated_costs_only_with_layer_info: Option< HashMap, >, @@ -56,8 +54,18 @@ impl Drive { document_id, contract, document_type_name, - block_info, - deleter_id, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + drive_operations, + platform_version, + ), + 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( + document_id, + contract, + document_type_name, + &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), + None, estimated_costs_only_with_layer_info, block_time_ms, transaction, @@ -66,7 +74,7 @@ impl Drive { ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_apply_and_add_to_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs index 36f03c47b63..2a65843e041 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs @@ -1,7 +1,6 @@ use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -19,8 +18,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, mut estimated_costs_only_with_layer_info: Option< HashMap, >, @@ -40,8 +37,6 @@ impl Drive { document_id, contract, document_type_name, - block_info, - deleter_id, None, &mut estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs new file mode 100644 index 00000000000..ebf8ce8dd41 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs @@ -0,0 +1,63 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; + +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Deletes a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(in crate::drive::document::delete) fn delete_document_for_contract_apply_and_add_to_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, + mut estimated_costs_only_with_layer_info: Option< + HashMap, + >, + block_time_ms: u64, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + // With no caller transaction, TTL preparation (direct drainage + // writes) and the apply below would each commit on their own; span + // them with one owned transaction so the write is all-or-nothing. + let owned_transaction = (estimated_costs_only_with_layer_info.is_none() + && transaction.is_none()) + .then(|| self.grove.start_transaction()); + let transaction = owned_transaction.as_ref().or(transaction); + let batch_operations = self.delete_document_for_contract_with_named_type_operations_v1( + document_id, + contract, + document_type_name, + block_info, + deleter_id, + None, + &mut estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + )?; + self.apply_batch_low_level_drive_operations( + estimated_costs_only_with_layer_info, + transaction, + batch_operations, + drive_operations, + &platform_version.drive, + )?; + if let Some(owned_transaction) = owned_transaction { + self.commit_transaction(owned_transaction, &platform_version.drive)?; + } + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs index 3737d85c356..1a70a431e92 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use grovedb::TransactionArg; @@ -60,9 +61,19 @@ impl Drive { platform_version, previous_fee_versions, ), + 1 => self.delete_document_for_contract_id_v1( + document_id, + contract_id, + document_type_name, + block_info, + apply, + transaction, + platform_version, + previous_fee_versions, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_id".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs index 09f4b28aec9..50e415bbb06 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs @@ -56,10 +56,6 @@ impl Drive { document_id, contract, document_type_name, - &block_info, - // These fee-applying wrappers carry no signer, so a lifecycle record - // they write belongs to no identity and refunds nobody. - None, estimated_costs_only_with_layer_info, block_info.time_ms, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs new file mode 100644 index 00000000000..1285ef6cd07 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs @@ -0,0 +1,80 @@ +use grovedb::batch::KeyInfoPath; + +use grovedb::{EstimatedLayerInformation, TransactionArg}; + +use std::collections::HashMap; + +use crate::drive::Drive; +use crate::error::document::DocumentError; +use dpp::block::block_info::BlockInfo; +use dpp::fee::default_costs::CachedEpochIndexFeeVersions; + +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::version::PlatformVersion; + +impl Drive { + /// Deletes a document and returns the associated fee. + /// The contract CBOR is given instead of the contract itself. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_id_v1( + &self, + document_id: Identifier, + contract_id: Identifier, + document_type_name: &str, + block_info: BlockInfo, + apply: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, + ) -> Result { + let mut drive_operations: Vec = vec![]; + let estimated_costs_only_with_layer_info = if apply { + None::> + } else { + Some(HashMap::new()) + }; + + let contract_fetch_info = self + .get_contract_with_fetch_info_and_add_to_operations( + contract_id.to_buffer(), + Some(&block_info.epoch), + true, + transaction, + &mut drive_operations, + platform_version, + )? + .ok_or(Error::Document(DocumentError::DataContractNotFound))?; + + let contract = &contract_fetch_info.contract; + + self.delete_document_for_contract_apply_and_add_to_operations_v1( + document_id, + contract, + document_type_name, + &block_info, + // These fee-applying wrappers carry no signer, so a lifecycle record + // they write belongs to no identity and refunds nobody. + None, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + &mut drive_operations, + platform_version, + )?; + let fees = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + self.config.epochs_per_era, + platform_version, + previous_fee_versions, + )?; + + Ok(fees) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs index 9c97474dece..023da2bdd18 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs @@ -1,10 +1,11 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; +use dpp::block::epoch::Epoch; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; @@ -35,8 +36,7 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, + epoch: &Epoch, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -56,8 +56,19 @@ impl Drive { document_id, contract_id, document_type_name, - block_info, - deleter_id, + epoch, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_id_with_named_type_operations_v1( + document_id, + contract_id, + document_type_name, + &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), + None, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -66,7 +77,7 @@ impl Drive { ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_id_with_named_type_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs index 6c15900fad0..1349e7e07a6 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs @@ -10,7 +10,7 @@ use crate::error::document::DocumentError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; +use dpp::block::epoch::Epoch; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::identifier::Identifier; @@ -25,8 +25,7 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, + epoch: &Epoch, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -38,7 +37,7 @@ impl Drive { let mut operations = vec![]; let Some(contract_fetch_info) = self.get_contract_with_fetch_info_and_add_to_operations( contract_id.to_buffer(), - Some(&block_info.epoch), + Some(epoch), true, transaction, &mut operations, @@ -54,8 +53,6 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs new file mode 100644 index 00000000000..60e154fc40c --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs @@ -0,0 +1,66 @@ +use grovedb::batch::KeyInfoPath; + +use grovedb::{EstimatedLayerInformation, TransactionArg}; + +use std::collections::HashMap; + +use crate::drive::Drive; +use crate::error::document::DocumentError; + +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::identifier::Identifier; + +use dpp::version::PlatformVersion; + +impl Drive { + /// Prepares the operations for deleting a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_id_with_named_type_operations_v1( + &self, + document_id: Identifier, + contract_id: Identifier, + document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, + 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> { + let mut operations = vec![]; + let Some(contract_fetch_info) = self.get_contract_with_fetch_info_and_add_to_operations( + contract_id.to_buffer(), + Some(&block_info.epoch), + true, + transaction, + &mut operations, + platform_version, + )? + else { + return Err(Error::Document(DocumentError::DataContractNotFound)); + }; + + let contract = &contract_fetch_info.contract; + let document_type = contract.document_type_for_name(document_type_name)?; + self.delete_document_for_contract_operations_with_lifecycle( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ) + } +} 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 4839e13afbd..b651ac2bda8 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 @@ -23,8 +23,6 @@ impl Drive { /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document. - /// * `block_info`: The block this delete belongs to. - /// * `deleter_id`: The identity credited with the lifecycle record's bytes. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. @@ -39,8 +37,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, - block_info: &BlockInfo, - deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -97,10 +93,103 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_operations_v1( + document_id, + contract, + document_type, + &BlockInfo::default_with_time(block_time_ms), + None, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "delete_document_for_contract_operations".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } + } + + /// Prepares a lifecycle-aware delete using the block and deleter that + /// authored the state transition. + #[allow(clippy::too_many_arguments)] + pub fn delete_document_for_contract_operations_with_lifecycle( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, + 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 estimated_costs_only_with_layer_info.is_none() { + self.prepare_document_time_range_ttl( + contract, + document_type, + block_time_ms, + transaction, + platform_version, + )?; + } + self.delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ) + } + + /// Builds a lifecycle-aware delete against post-drain state. + #[allow(clippy::too_many_arguments)] + pub(crate) fn delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, + 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_document_for_contract_operations + { + 0 => self.delete_document_for_contract_operations_v0( + document_id, + contract, + document_type, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, transaction, platform_version, ), @@ -112,7 +201,6 @@ impl Drive { deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ), @@ -147,8 +235,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, - block_info: &BlockInfo, - deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -168,10 +254,9 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, + block_time_ms, transaction, platform_version, ), @@ -179,11 +264,10 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, + &BlockInfo::default_with_time(block_time_ms), + None, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ), 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 31f3e01d1b7..0134b878d25 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 @@ -3,7 +3,6 @@ use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::document_type::DocumentTypeRef; use std::collections::HashMap; @@ -44,8 +43,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, - _block_info: &BlockInfo, - _deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -64,8 +61,6 @@ impl Drive { document_id, contract, document_type, - _block_info, - _deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -82,8 +77,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, - _block_info: &BlockInfo, - _deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 675dba0a638..462fb56d686 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -108,10 +108,9 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, + block_info.time_ms, transaction, platform_version, ); @@ -245,6 +244,7 @@ impl Drive { &document_and_contract_info, &previous_batch_operations, estimated_costs_only_with_layer_info, + block_info.time_ms, transaction, &mut batch_operations, platform_version, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs index 0fddbe97160..5a1a5ece047 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs @@ -1,11 +1,10 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; - use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -35,8 +34,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -56,8 +53,18 @@ impl Drive { document_id, contract, document_type_name, - block_info, - deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_with_named_type_operations_v1( + document_id, + contract, + document_type_name, + &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), + None, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -66,7 +73,7 @@ impl Drive { ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_with_named_type_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs index df691f5c869..cddde01f1b0 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs @@ -4,7 +4,6 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; -use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use crate::drive::Drive; @@ -26,8 +25,6 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, previous_batch_operations: Option<&mut Vec>, estimated_costs_only_with_layer_info: &mut Option< HashMap, @@ -41,8 +38,6 @@ impl Drive { document_id, contract, document_type, - block_info, - deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs new file mode 100644 index 00000000000..9d2b9cfcac7 --- /dev/null +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs @@ -0,0 +1,53 @@ +use grovedb::batch::KeyInfoPath; + +use grovedb::{EstimatedLayerInformation, TransactionArg}; + +use std::collections::HashMap; + +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::DataContract; + +use crate::drive::Drive; + +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::identifier::Identifier; + +use dpp::version::PlatformVersion; + +impl Drive { + /// Prepares the operations for deleting a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(in crate::drive::document::delete) fn delete_document_for_contract_with_named_type_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, + 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> { + let document_type = contract.document_type_for_name(document_type_name)?; + self.delete_document_for_contract_operations_with_lifecycle( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ) + } +} diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs index 6d41183175c..b9891903c4f 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/mod.rs @@ -51,7 +51,7 @@ impl Drive { .delete .erase_document_for_contract_operations { - 0 => self.erase_document_for_contract_operations_v0( + Some(0) => self.erase_document_for_contract_operations_v0( document_id, contract, document_type, @@ -60,11 +60,15 @@ impl Drive { transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "erase_document_for_contract_operations".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "erase_document_for_contract_operations".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 669cba5a0a7..890bf9407ca 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1489,8 +1489,7 @@ mod tests { random_document_id, nonexistent_contract_id, "profile", - &BlockInfo::default_with_epoch(epoch), - None, + &epoch, None, &mut estimated_costs_only_with_layer_info, 0, diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs index 6694ad99d0c..b9de485a608 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_erase_document/mod.rs @@ -40,18 +40,22 @@ impl Drive { .delete .add_estimation_costs_for_erase_document { - 0 => Self::add_estimation_costs_for_erase_document_v0( + Some(0) => Self::add_estimation_costs_for_erase_document_v0( document_id, contract, document_type, layers, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_estimation_costs_for_erase_document".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "add_estimation_costs_for_erase_document".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs index 7f2d45be9fb..3dce314e1a3 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/fetch/mod.rs @@ -69,7 +69,7 @@ impl Drive { .query .fetch_document_lifecycle { - 0 => self.fetch_document_lifecycle_v0( + Some(0) => self.fetch_document_lifecycle_v0( contract, document_type, document_id, @@ -77,11 +77,15 @@ impl Drive { transaction, platform_version, ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "fetch_document_lifecycle".to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "fetch_document_lifecycle".to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs b/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs index f2f2f117b4f..f6b0aeeb7bc 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/refund_recipients.rs @@ -21,9 +21,10 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; /// Beneficiaries an erase can credit beyond the revisions themselves: the -/// lifecycle record's writer, the per-document history subtree's creator, and -/// the current pointer's, all of which a terminal chunk removes. -const STRUCTURAL_REFUND_RECIPIENTS: u64 = 3; +/// lifecycle record's writer and the per-document history subtree's creator, +/// both of which a terminal chunk removes. Delete already removed the current +/// pointer, so erase cannot refund it again. +const STRUCTURAL_REFUND_RECIPIENTS: u64 = 2; impl Drive { /// Prices the worst-case number of third-party balance updates one erase diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 209154bb6b6..7e2295817fe 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -4,13 +4,14 @@ use super::fetch::DocumentLifecycleState; use super::DocumentLifecycleRecord; -use crate::drive::document::history::{ - DocumentHistoryQueryV1, DocumentHistorySelector, DocumentHistoryState, -}; use crate::drive::document::paths::{document_history_path, document_lifecycle_path}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; +use crate::query::document_history_drive_query::{ + DocumentHistoryDriveQuery, DocumentHistoryFilter, DocumentHistoryLifecycle, + DocumentHistoryState, +}; use crate::query::{ DriveDocumentQuery, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus, }; @@ -18,6 +19,7 @@ use crate::util::grove_operations::BatchDeleteApplyType::StatefulBatchDelete; use crate::util::object_size_info::{DocumentAndContractInfo, DocumentInfo, OwnedDocumentInfo}; use crate::util::storage_flags::StorageFlags; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; +use crate::verify::document::DocumentHistoryProof; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; @@ -139,25 +141,30 @@ fn history_metadata( drive: &Drive, contract: &DataContract, id: Identifier, -) -> crate::drive::document::history::DocumentHistoryLifecycle { - let query = DocumentHistoryQueryV1 { +) -> DocumentHistoryLifecycle { + let query = DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "person".into(), document_id: id.to_buffer(), - selector: DocumentHistorySelector::StartAtTime(0), + filter: DocumentHistoryFilter::StartAtTime(0), limit: Some(10), }; - let (page, proof) = drive - .prove_document_history_v1(&query, document_type_of(contract), None, latest()) + let page = drive + .fetch_document_history(&query, document_type_of(contract), None, latest()) + .expect("expected to fetch history"); + let proof = drive + .prove_document_history(&query, document_type_of(contract), None, latest()) .expect("expected to prove history"); let (_, verified) = - Drive::verify_document_history_v1(&query, &proof, document_type_of(contract), latest()) + Drive::verify_document_history(&query, &proof, document_type_of(contract), latest()) .expect("expected the proof to verify"); assert_eq!( verified, page, "the proved history must match the read history" ); - verified.lifecycle + verified + .lifecycle + .expect("the latest protocol authenticates lifecycle metadata") } fn delete( @@ -170,7 +177,7 @@ fn delete( let mut operations = vec![]; let block_info = BlockInfo::default_with_time(time_ms); let batch = drive - .delete_document_for_contract_operations( + .delete_document_for_contract_operations_with_lifecycle( id, contract, document_type_of(contract), @@ -178,6 +185,7 @@ fn delete( Some(deleter), None, &mut None, + time_ms, None, latest(), ) @@ -546,34 +554,36 @@ fn should_page_and_prove_the_history_of_a_deleted_document() { let (drive, contract, id) = setup_history(12, owner); delete(&drive, &contract, id, Identifier::new(owner), 5_000); - let mut selector = DocumentHistorySelector::StartAtTime(0); + let mut selector = DocumentHistoryFilter::StartAtTime(0); let mut seen = vec![]; loop { - let query = DocumentHistoryQueryV1 { + let query = DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "person".into(), document_id: id.to_buffer(), - selector, + filter: selector, limit: Some(10), }; - let (page, proof) = drive - .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + let page = drive + .fetch_document_history(&query, document_type_of(&contract), None, latest()) + .expect("expected to fetch a page"); + let proof = drive + .prove_document_history(&query, document_type_of(&contract), None, latest()) .expect("expected to prove a page"); - let (_, verified) = Drive::verify_document_history_v1( - &query, - &proof, - document_type_of(&contract), - latest(), - ) - .expect("expected the page proof to verify"); - assert_eq!(verified.lifecycle.state, DocumentHistoryState::Deleted); - assert_eq!(verified.lifecycle.remaining_revisions, 12); - assert_eq!(verified.lifecycle.times.deleted_at_ms, 5_000); + let (_, verified) = + Drive::verify_document_history(&query, &proof, document_type_of(&contract), latest()) + .expect("expected the page proof to verify"); + let lifecycle = verified + .lifecycle + .expect("the latest protocol authenticates lifecycle metadata"); + assert_eq!(lifecycle.state, DocumentHistoryState::Deleted); + assert_eq!(lifecycle.remaining_revisions, 12); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000); seen.extend(page.entries.iter().map(|entry| entry.revision)); let Some(last) = page.entries.last() else { break; }; - selector = DocumentHistorySelector::StartAfter { + selector = DocumentHistoryFilter::StartAfter { time_ms: last.time_ms, revision: last.revision, }; @@ -867,7 +877,7 @@ fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_delete ); let batch = drive - .delete_document_for_contract_operations( + .delete_document_for_contract_operations_with_lifecycle( document.id(), &contract, document_type, @@ -875,6 +885,7 @@ fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_delete Some(Identifier::new([7; 32])), None, &mut None, + 5_000, None, version, ) @@ -1030,7 +1041,7 @@ fn should_reject_an_unsupported_erase_estimation_version() { .document .delete .add_estimation_costs_for_erase_document, - 0, + Some(0), "protocol 14 selects the only implementation there is" ); version @@ -1038,7 +1049,7 @@ fn should_reject_an_unsupported_erase_estimation_version() { .methods .document .delete - .add_estimation_costs_for_erase_document = 1; + .add_estimation_costs_for_erase_document = Some(1); let error = Drive::add_estimation_costs_for_erase_document( id, @@ -1077,20 +1088,33 @@ fn should_price_the_refund_recipients_an_erase_can_credit() { "pricing must not invent a refund or a storage charge of its own" ); - // The same shape at half the bound costs proportionally less, which is what - // makes this a price for the bound rather than a constant. - let mut halved = version.clone(); - halved + let mut no_revisions = version.clone(); + no_revisions .system_limits - .max_document_revisions_erased_per_transition = Some(chunk_size() as u16 / 2); - let smaller = drive - .erase_refund_recipient_cost(&epoch, &halved) - .expect("expected a price at the smaller bound"); - assert!( - smaller.processing_fee < priced.processing_fee, - "a smaller chunk credits fewer beneficiaries: {} is not below {}", - smaller.processing_fee, - priced.processing_fee + .max_document_revisions_erased_per_transition = Some(0); + let structural = drive + .erase_refund_recipient_cost(&epoch, &no_revisions) + .expect("expected the structural-recipient price"); + let mut one_revision = no_revisions.clone(); + one_revision + .system_limits + .max_document_revisions_erased_per_transition = Some(1); + let with_one_revision = drive + .erase_refund_recipient_cost(&epoch, &one_revision) + .expect("expected the one-revision price"); + let one_recipient = with_one_revision + .processing_fee + .checked_sub(structural.processing_fee) + .expect("one additional recipient increases the price"); + assert_eq!( + structural.processing_fee, + one_recipient * 2, + "terminal erase refunds only the lifecycle record and history subtree" + ); + assert_eq!( + priced.processing_fee, + one_recipient * (chunk_size() + 2), + "the full estimate prices every revision plus two structural recipients" ); // Every balance in the drive is untouched: this is a measurement, not a @@ -1176,7 +1200,7 @@ fn should_refund_each_writer_under_the_epoch_they_wrote_in() { ..BlockInfo::default_with_time(5_000) }; let batch = drive - .delete_document_for_contract_operations( + .delete_document_for_contract_operations_with_lifecycle( id, &contract, document_type, @@ -1184,6 +1208,7 @@ fn should_refund_each_writer_under_the_epoch_they_wrote_in() { Some(Identifier::new(early_writer)), None, &mut None, + block_info.time_ms, None, version, ) @@ -1454,7 +1479,7 @@ fn should_refuse_rather_than_half_apply_two_deletes_sharing_a_new_container() { .expect("expected a second document"); let delete_operation = |id: Identifier| { - DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { + DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { document_id: id, deleter_id: Some(Identifier::new(owner)), contract_info: DataContractInfo::BorrowedDataContract(&contract), @@ -1505,87 +1530,85 @@ fn should_refuse_rather_than_half_apply_two_deletes_sharing_a_new_container() { } /// Every state the lifecycle can be in has to survive the proof round trip, and -/// each of the four claimed times has to be the one the proof authenticates: a -/// node that reports a different deletion time, a different erasure start, or a -/// different remaining count must fail verification rather than be believed. +/// corrupting the lifecycle proof must fail verification rather than return +/// unauthenticated metadata. #[test] -fn should_prove_the_erasing_state_and_reject_a_tampered_claim() { +fn should_prove_the_erasing_state_and_reject_a_tampered_proof() { let chunk = chunk_size(); let owner = [47u8; 32]; let (drive, contract, id) = setup_history(chunk + 2, owner); delete(&drive, &contract, id, Identifier::new(owner), 5_000); erase(&drive, &contract, id, 6_000); - let query = |selector| DocumentHistoryQueryV1 { + let query = |filter| DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "person".into(), document_id: id.to_buffer(), // A single-revision read is capped at one entry by the selector's own // rule; the page selectors take the full page. - limit: Some(match selector { - DocumentHistorySelector::Revision(_) => 1, + limit: Some(match filter { + DocumentHistoryFilter::Revision(_) => 1, _ => 10, }), - selector, + filter, }; // A populated page, and a page whose lower bound is past the end of what // survives. Both must authenticate the same erasing metadata. for selector in [ - DocumentHistorySelector::StartAtTime(0), - DocumentHistorySelector::StartAtTime(u64::MAX / 2), - DocumentHistorySelector::Revision(1), + DocumentHistoryFilter::StartAtTime(0), + DocumentHistoryFilter::StartAtTime(u64::MAX / 2), + DocumentHistoryFilter::Revision(1), ] { let query = query(selector); - let (page, proof) = drive - .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + let page = drive + .fetch_document_history(&query, document_type_of(&contract), None, latest()) + .expect("expected to fetch the page"); + let proof = drive + .prove_document_history(&query, document_type_of(&contract), None, latest()) .expect("expected to prove the page"); - let (_, verified) = Drive::verify_document_history_v1( - &query, - &proof, - document_type_of(&contract), - latest(), - ) - .expect("expected the proof to verify"); + let (_, verified) = + Drive::verify_document_history(&query, &proof, document_type_of(&contract), latest()) + .expect("expected the proof to verify"); assert_eq!(verified, page); - assert_eq!(verified.lifecycle.state, DocumentHistoryState::Erasing); - assert_eq!(verified.lifecycle.remaining_revisions, 2); - assert_eq!(verified.lifecycle.times.deleted_at_ms, 5_000); - assert_eq!(verified.lifecycle.times.erasing_started_at_ms, 6_000); - assert_eq!(verified.lifecycle.times.erasing_from_revision, chunk + 2); - - // A claim that differs from the proof in any one field is not the - // proof's claim, and the verifier rebuilds it rather than trusting it. - let mut tampered = verified.lifecycle.clone(); - for corrupt in [ - &mut tampered.times.deleted_at_ms, - &mut tampered.times.erasing_started_at_ms, - &mut tampered.times.erasing_from_time_ms, - &mut tampered.times.erasing_from_revision, - ] { - *corrupt += 1; - } - tampered.remaining_revisions += 1; - tampered.state = DocumentHistoryState::Deleted; - assert_ne!( - tampered, verified.lifecycle, - "the verifier's answer is derived, so a different claim cannot match it" - ); + let lifecycle = verified + .lifecycle + .expect("the latest protocol authenticates lifecycle metadata"); + assert_eq!(lifecycle.state, DocumentHistoryState::Erasing); + assert_eq!(lifecycle.remaining_revisions, 2); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000); + assert_eq!(lifecycle.times.erasing_started_at_ms, 6_000); + assert_eq!(lifecycle.times.erasing_from_revision, chunk + 2); + + let mut tampered = + DocumentHistoryProof::from_bytes(&proof).expect("expected the history proof envelope"); + tampered.metadata_proof.push(0xff); + let tampered = tampered.to_bytes().expect("expected to encode the proof"); + Drive::verify_document_history(&query, &tampered, document_type_of(&contract), latest()) + .expect_err("tampering with the lifecycle proof must fail verification"); } // And the terminal state after the erasure finishes. erase(&drive, &contract, id, 7_000); - let query = query(DocumentHistorySelector::StartAtTime(0)); - let (page, proof) = drive - .prove_document_history_v1(&query, document_type_of(&contract), None, latest()) + let query = query(DocumentHistoryFilter::StartAtTime(0)); + let page = drive + .fetch_document_history(&query, document_type_of(&contract), None, latest()) + .expect("expected to fetch the absent id"); + let proof = drive + .prove_document_history(&query, document_type_of(&contract), None, latest()) .expect("expected to prove the absent id"); let (_, verified) = - Drive::verify_document_history_v1(&query, &proof, document_type_of(&contract), latest()) + Drive::verify_document_history(&query, &proof, document_type_of(&contract), latest()) .expect("expected the absence proof to verify"); assert_eq!(verified, page); - assert_eq!(verified.lifecycle.state, DocumentHistoryState::Absent); - assert_eq!(verified.lifecycle.remaining_revisions, 0); - assert_eq!(verified.lifecycle.times, Default::default()); + let lifecycle = verified + .lifecycle + .expect("the latest protocol authenticates lifecycle metadata"); + assert_eq!(lifecycle.state, DocumentHistoryState::Absent); + assert_eq!(lifecycle.remaining_revisions, 0); + assert_eq!(lifecycle.times, Default::default()); + let proof = + DocumentHistoryProof::from_bytes(&proof).expect("expected the history proof envelope"); assert!( proof.entries_proof.is_none(), "there is no history tree left to page over" @@ -1604,15 +1627,15 @@ fn should_read_revisions_by_position_after_a_partial_erasure() { erase(&drive, &contract, id, 6_000); for revision in 1..=3u64 { - let query = DocumentHistoryQueryV1 { + let query = DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "person".into(), document_id: id.to_buffer(), - selector: DocumentHistorySelector::Revision(revision), + filter: DocumentHistoryFilter::Revision(revision), limit: Some(1), }; let page = drive - .fetch_document_history_v1(&query, document_type_of(&contract), None, latest()) + .fetch_document_history(&query, document_type_of(&contract), None, latest()) .expect("expected to read a surviving revision"); assert_eq!( page.entries.len(), @@ -1624,15 +1647,15 @@ fn should_read_revisions_by_position_after_a_partial_erasure() { // The revisions the chunk removed are gone, not silently answered with a // neighbour. - let query = DocumentHistoryQueryV1 { + let query = DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "person".into(), document_id: id.to_buffer(), - selector: DocumentHistorySelector::Revision(4), + filter: DocumentHistoryFilter::Revision(4), limit: Some(1), }; let page = drive - .fetch_document_history_v1(&query, document_type_of(&contract), None, latest()) + .fetch_document_history(&query, document_type_of(&contract), None, latest()) .expect("expected the read to run"); assert!( page.entries.is_empty(), @@ -1704,22 +1727,22 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { .expect("expected the migration to commit"); let id = document.id(); - let query = |selector| DocumentHistoryQueryV1 { + let query = |filter| DocumentHistoryDriveQuery { contract_id: contract.id().to_buffer(), document_type_name: "profile".into(), document_id: id.to_buffer(), // A single-revision read is capped at one entry by the selector's // own rule; the page selectors take the full page. - limit: Some(match selector { - DocumentHistorySelector::Revision(_) => 1, + limit: Some(match filter { + DocumentHistoryFilter::Revision(_) => 1, _ => 10, }), - selector, + filter, }; let by_revision = [ - DocumentHistorySelector::Revision(3), - DocumentHistorySelector::StartAtRevision(3), - DocumentHistorySelector::Revision(2), + DocumentHistoryFilter::Revision(3), + DocumentHistoryFilter::StartAtRevision(3), + DocumentHistoryFilter::Revision(2), ]; let is_gap_refusal = |error: Error| { let message = error.to_string(); @@ -1733,7 +1756,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { for selector in by_revision.clone() { is_gap_refusal( drive - .fetch_document_history_v1(&query(selector), document_type, None, new) + .fetch_document_history(&query(selector), document_type, None, new) .expect_err("a gapped history refuses by-revision reads"), ); } @@ -1745,7 +1768,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { }; apply( drive - .delete_document_for_contract_operations( + .delete_document_for_contract_operations_with_lifecycle( id, &contract, document_type, @@ -1753,21 +1776,25 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { Some(Identifier::new(owner)), None, &mut None, + 5_000, None, new, ) .expect("expected to build the delete"), ); let page = drive - .fetch_document_history_v1( - &query(DocumentHistorySelector::StartAtTime(0)), + .fetch_document_history( + &query(DocumentHistoryFilter::StartAtTime(0)), document_type, None, new, ) .expect("time pagination still reads a deleted gapped history"); - assert_eq!(page.lifecycle.state, DocumentHistoryState::Deleted); - assert_eq!(page.lifecycle.remaining_revisions, 2); + let lifecycle = page + .lifecycle + .expect("protocol 14 authenticates lifecycle metadata"); + assert_eq!(lifecycle.state, DocumentHistoryState::Deleted); + assert_eq!(lifecycle.remaining_revisions, 2); assert_eq!( page.entries .iter() @@ -1779,12 +1806,12 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { let query = query(selector.clone()); is_gap_refusal( drive - .fetch_document_history_v1(&query, document_type, None, new) + .fetch_document_history(&query, document_type, None, new) .expect_err("a deleted gapped history refuses by-revision reads"), ); is_gap_refusal( drive - .prove_document_history_v1(&query, document_type, None, new) + .prove_document_history(&query, document_type, None, new) .expect_err("a deleted gapped history cannot be proved by revision"), ); } @@ -1807,9 +1834,14 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { ); for selector in by_revision { let page = drive - .fetch_document_history_v1(&query(selector), document_type, None, new) + .fetch_document_history(&query(selector), document_type, None, new) .expect("an unused id reads as an authenticated absence"); - assert_eq!(page.lifecycle.state, DocumentHistoryState::Absent); + assert_eq!( + page.lifecycle + .expect("protocol 14 authenticates lifecycle metadata") + .state, + DocumentHistoryState::Absent + ); assert!(page.entries.is_empty()); } } diff --git a/packages/rs-drive/src/prove/prove_state_transition/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/mod.rs index 707bf78cbf0..18274c076b7 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -34,9 +35,10 @@ impl Drive { ) -> Result>, Error> { match platform_version.drive.methods.prove.prove_state_transition { 0 => self.prove_state_transition_v0(state_transition, transaction, platform_version), + 1 => self.prove_state_transition_v1(state_transition, transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "prove_state_transition".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs new file mode 100644 index 00000000000..d508183cc66 --- /dev/null +++ b/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs @@ -0,0 +1,555 @@ +use crate::drive::{Drive, RootTree}; +use crate::error::proof::ProofError; +use crate::error::Error; +use crate::prove::prove_state_transition::ProofCreationResult; +use crate::query::{ + IdentityBasedVoteDriveQuery, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus, +}; +use crate::verify::state_transition::state_transition_execution_path_queries::TryTransitionIntoPathQuery; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::config::v0::DataContractConfigGettersV0; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::identifier::Identifier; +use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; +use dpp::state_transition::address_funding_from_asset_lock_transition::accessors::AddressFundingFromAssetLockTransitionAccessorsV0; +use dpp::state_transition::address_funds_transfer_transition::accessors::AddressFundsTransferTransitionAccessorsV0; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransitionRefV1, DocumentTransitionRefV1, +}; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; +use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; +use dpp::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0; +use dpp::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; +use dpp::state_transition::identity_credit_transfer_to_addresses_transition::accessors::IdentityCreditTransferToAddressesTransitionAccessorsV0; +use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; +use dpp::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; +use dpp::state_transition::identity_topup_from_addresses_transition::accessors::IdentityTopUpFromAddressesTransitionAccessorsV0; +use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; +use dpp::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0; +use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; +use dpp::state_transition::StateTransitionIdentityIdFromInputs; +use dpp::state_transition::StateTransitionWitnessSigned; +use dpp::state_transition::{StateTransition, StateTransitionLike, StateTransitionOwned}; +use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; +use dpp::voting::votes::Vote; +use grovedb::{PathQuery, TransactionArg}; +use platform_version::version::PlatformVersion; + +fn contract_ids_to_non_historical_path_query(contract_ids: &[Identifier]) -> PathQuery { + let contract_ids: Vec<_> = contract_ids.iter().map(|id| id.to_buffer()).collect(); + + let mut path_query = Drive::fetch_non_historical_contracts_query(&contract_ids); + path_query.query.limit = None; + path_query +} + +fn contract_ids_to_historical_path_query(contract_ids: &[Identifier]) -> PathQuery { + let contract_ids: Vec<_> = contract_ids.iter().map(|id| id.to_buffer()).collect(); + + let mut path_query = Drive::fetch_historical_contracts_query(&contract_ids); + path_query.query.limit = None; + path_query +} + +impl Drive { + /// Generation 1 sees a batch of any wire format through the shell that + /// knows the erase kind. An erase acts on a document that is already + /// deleted, so it is proved by the same by-id query as a delete: the proof + /// shows the document absent. + pub(super) fn prove_state_transition_v1( + &self, + state_transition: &StateTransition, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + let path_query = match state_transition { + StateTransition::DataContractCreate(st) => { + if st.data_contract().config().keeps_history() { + contract_ids_to_historical_path_query(&st.modified_data_ids()) + } else { + contract_ids_to_non_historical_path_query(&st.modified_data_ids()) + } + } + StateTransition::DataContractUpdate(st) => { + if st.data_contract().config().keeps_history() { + contract_ids_to_historical_path_query(&st.modified_data_ids()) + } else { + contract_ids_to_non_historical_path_query(&st.modified_data_ids()) + } + } + StateTransition::Batch(st) => { + if st.transitions_len_v1() > 1 { + return Ok(ProofCreationResult::new_with_error( + ProofError::InvalidTransition( + "batch state transition must have only one batched transition" + .to_string(), + ), + )); + } + let Some(transition) = st.first_transition_v1() else { + return Ok(ProofCreationResult::new_with_error( + ProofError::InvalidTransition( + "batch state transition must have one batched transition".to_string(), + ), + )); + }; + + let owner_id = st.owner_id(); + + match transition { + BatchedTransitionRefV1::Document(document_transition) => { + let data_contract_id = document_transition.data_contract_id(); + + let Some(contract_fetch_info) = self.get_contract_with_fetch_info( + data_contract_id.to_buffer(), + false, + None, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::UnknownContract(format!( + "unknown contract with id {} in document proving", + data_contract_id + )))); + }; + + let contract = &contract_fetch_info.contract; + + let document_type = contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| { + Error::Proof(ProofError::UnknownContract(format!( + "cannot fetch contract for document {} with id {}: {}", + document_transition.document_type_name(), + document_transition.data_contract_id(), + e + ))) + })?; + + // indexOnly documents have no primary row to prove by + // id: the executed transition is proven against the + // entry its values produce under the proof index — + // the same single-entry path query the verifier + // rebuilds from the transition. + { + use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; + use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; + if document_type.index_only() { + let values = match document_transition { + DocumentTransitionRefV1::Create(create_transition) => { + create_transition.data().clone() + } + // The indexOnlyDelete kind always carries + // its values — no fallible access needed. + DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { + delete_transition.data().clone() + } + _ => { + return Ok(ProofCreationResult::new_with_error( + ProofError::InvalidTransition( + "indexOnly documents only support create and \ + indexOnlyDelete" + .to_string(), + ), + )); + } + }; + crate::query::index_only_synthesis::index_only_transition_entry_path_query( + contract.id(), + document_type, + &values, + owner_id, + platform_version, + )? + } else { + let contested_status = + if let DocumentTransitionRefV1::Create(create_transition) = + document_transition + { + if create_transition.prefunded_voting_balance().is_some() { + SingleDocumentDriveQueryContestedStatus::Contested + } else { + SingleDocumentDriveQueryContestedStatus::NotContested + } + } else { + SingleDocumentDriveQueryContestedStatus::NotContested + }; + + let query = SingleDocumentDriveQuery { + contract_id: document_transition + .data_contract_id() + .into_buffer(), + document_type_name: document_transition + .document_type_name() + .clone(), + document_type_keeps_history: document_type + .documents_keep_history(), + document_id: document_transition.base().id().into_buffer(), + block_time_ms: None, //None because we want latest + contested_status, + }; + + let mut path_query = + query.construct_path_query(platform_version)?; + path_query.query.limit = None; + path_query + } + } + } + BatchedTransitionRefV1::Token(token_transition) => { + let data_contract_id = token_transition.data_contract_id(); + + let Some(contract_fetch_info) = self.get_contract_with_fetch_info( + data_contract_id.to_buffer(), + false, + None, + platform_version, + )? + else { + return Ok(ProofCreationResult::new_with_error( + ProofError::UnknownContract(format!( + "unknown contract with id {} in token proving", + data_contract_id + )), + )); + }; + + let contract = &contract_fetch_info.contract; + token_transition.try_transition_into_path_query_with_contract( + contract, + owner_id, + platform_version, + )? + } + } + } + StateTransition::IdentityCreate(st) => Drive::full_identity_query( + &st.identity_id().into_buffer(), + &platform_version.drive.grove_version, + )?, + StateTransition::IdentityTopUp(st) => { + // we expect to get a new balance and revision + Drive::revision_and_balance_path_query( + st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )? + } + StateTransition::IdentityCreditWithdrawal(st) => { + Drive::identity_balance_query(&st.identity_id().to_buffer()) + } + StateTransition::IdentityUpdate(st) => Drive::identity_all_keys_query( + &st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )?, + StateTransition::IdentityCreditTransfer(st) => { + let sender_query = Drive::identity_balance_query(&st.identity_id().into_buffer()); + let recipient_query = + Drive::identity_balance_query(&st.recipient_id().into_buffer()); + + PathQuery::merge( + vec![&sender_query, &recipient_query], + &platform_version.drive.grove_version, + )? + } + StateTransition::MasternodeVote(st) => { + let pro_tx_hash = st.pro_tx_hash(); + + match st.vote() { + Vote::ResourceVote(resource_vote) => { + let query = IdentityBasedVoteDriveQuery { + identity_id: pro_tx_hash, + vote_poll: resource_vote.vote_poll().clone(), + }; + + // The path query construction can only fail if the serialization fails. + // Because the serialization will pretty much never fail, we can do this. + let mut path_query = query.construct_path_query()?; + path_query.query.limit = None; + path_query + } + } + } + StateTransition::IdentityCreditTransferToAddresses(st) => { + let identity_query = Drive::revision_and_balance_path_query( + st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )?; + let mut addresses_query = + Drive::balances_for_clear_addresses_query(st.recipient_addresses().keys()); + + // TODO: fix this limit setting - "can not merge pathqueries with limits, consider setting the limit after the merge" + addresses_query.query.limit = None; + + PathQuery::merge( + vec![&identity_query, &addresses_query], + &platform_version.drive.grove_version, + )? + } + StateTransition::IdentityCreateFromAddresses(st) => { + let identity_id = st.identity_id_from_inputs().map_err(|e| { + Error::Proof(ProofError::CorruptedProof(format!( + "Failed to calculate identity_id from inputs: {}", + e + ))) + })?; + let identity_query = Drive::full_identity_query( + &identity_id.into_buffer(), + &platform_version.drive.grove_version, + )?; + let change_output = st.output().into_iter().map(|(address, _)| address); + let addresses_to_check = st.inputs().keys().chain(change_output); + + let mut addresses_query = + Drive::balances_for_clear_addresses_query(addresses_to_check); + + // TODO: fix this limit setting - "can not merge pathqueries with limits, consider setting the limit after the merge" + addresses_query.query.limit = None; + + PathQuery::merge( + vec![&identity_query, &addresses_query], + &platform_version.drive.grove_version, + )? + } + StateTransition::IdentityTopUpFromAddresses(st) => { + // we expect to get a new balance and revision + let identity_query = Drive::revision_and_balance_path_query( + st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )?; + let change_output = st.output().into_iter().map(|(address, _)| address); + let addresses_to_check = st.inputs().keys().chain(change_output); + let addresses_query = Drive::balances_for_clear_addresses_query(addresses_to_check); + + // TODO: not sure if just setting this to unlimited is correct + let mut addresses_query = addresses_query; + addresses_query.query.limit = None; + + PathQuery::merge( + vec![&identity_query, &addresses_query], + &platform_version.drive.grove_version, + )? + } + StateTransition::AddressFundsTransfer(st) => Drive::balances_for_clear_addresses_query( + st.inputs().keys().chain(st.outputs().keys()), + ), + StateTransition::AddressFundingFromAssetLock(st) => { + Drive::balances_for_clear_addresses_query( + st.inputs().keys().chain(st.outputs().keys()), + ) + } + StateTransition::AddressCreditWithdrawal(st) => { + let addresses_to_check = st + .inputs() + .keys() + .chain(st.output().into_iter().map(|(address, _)| address)); + Drive::balances_for_clear_addresses_query(addresses_to_check) + } + StateTransition::ShieldedTransfer(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::state_transition::shielded_transfer_transition::accessors::ShieldedTransferTransitionAccessorsV0; + + let nullifier_keys: Vec> = st.nullifiers(); + let count = nullifier_keys.len() as u16; + + let mut query = grovedb::Query::new(); + query.insert_keys(nullifier_keys); + + PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(query, Some(count), None), + ) + } + StateTransition::Shield(st) => { + Drive::balances_for_clear_addresses_query(st.inputs().keys()) + } + StateTransition::Unshield(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::state_transition::unshield_transition::accessors::UnshieldTransitionAccessorsV0; + + let nullifier_keys: Vec> = st.nullifiers(); + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys); + let nullifier_pq = PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let mut address_pq = + Drive::balances_for_clear_addresses_query(std::iter::once(st.output_address())); + address_pq.query.limit = None; + + PathQuery::merge( + vec![&nullifier_pq, &address_pq], + &platform_version.drive.grove_version, + )? + } + StateTransition::ShieldedWithdrawal(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::data_contracts::withdrawals_contract; + use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; + use dpp::document::Document; + use dpp::state_transition::shielded_withdrawal_transition::accessors::ShieldedWithdrawalTransitionAccessorsV0; + + let nullifier_keys: Vec> = st.nullifiers(); + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys.clone()); + let nullifier_pq = PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + // Compute withdrawal document ID deterministically + let first_nullifier = nullifier_keys.first().ok_or_else(|| { + Error::Proof(ProofError::InvalidTransition( + "shielded withdrawal has no nullifiers".to_string(), + )) + })?; + let mut entropy = Vec::new(); + entropy.extend_from_slice(first_nullifier); + entropy.extend_from_slice(st.output_script().as_bytes()); + let document_id = Document::generate_document_id_v0( + &withdrawals_contract::ID, + &withdrawals_contract::OWNER_ID, + withdrawal::NAME, + &entropy, + ); + + let doc_query = SingleDocumentDriveQuery { + contract_id: withdrawals_contract::ID.to_buffer(), + document_type_name: withdrawal::NAME.to_string(), + document_type_keeps_history: false, + document_id: document_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + }; + let mut doc_pq = doc_query.construct_path_query(platform_version)?; + doc_pq.query.limit = None; + + PathQuery::merge( + vec![&nullifier_pq, &doc_pq], + &platform_version.drive.grove_version, + )? + } + StateTransition::ShieldFromAssetLock(st) => { + use dpp::identity::state_transition::AssetLockProved; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + + let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { + Error::Proof(ProofError::InvalidTransition( + "shield from asset lock has no outpoint".to_string(), + )) + })?; + let outpoint_bytes: [u8; 36] = outpoint.into(); + + let mut query = grovedb::Query::new(); + query.insert_key(outpoint_bytes.to_vec()); + + let outpoint_pq = PathQuery::new( + vec![vec![RootTree::SpentAssetLockTransactions as u8]], + grovedb::SizedQuery::new(query, Some(1), None), + ); + + // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. + let ShieldFromAssetLockTransition::V0(v0) = st; + match &v0.surplus_output { + Some(surplus_address) => { + // Mirror the Unshield arm: also prove the balance of the signed + // surplus-output address so a light client can confirm the surplus + // credit landed there. `PathQuery::merge` rejects sub-queries that carry + // limits, so clear both before merging. The verifier rebuilds this exact + // merged query (same sub-queries, same cleared limits, same merge) and + // verifies it STRICTLY, so the proof cannot carry any extra data beyond + // {outpoint, surplus-address}. + let mut outpoint_pq = outpoint_pq; + outpoint_pq.query.limit = None; + + let mut address_pq = Drive::balances_for_clear_addresses_query( + std::iter::once(surplus_address), + ); + address_pq.query.limit = None; + + PathQuery::merge( + vec![&outpoint_pq, &address_pq], + &platform_version.drive.grove_version, + )? + } + None => outpoint_pq, + } + } + StateTransition::IdentityCreateFromShieldedPool(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::state_transition::identity_create_from_shielded_pool_transition::accessors::IdentityCreateFromShieldedPoolTransitionAccessorsV0; + + // Prove BOTH the spent nullifiers AND the newly-created identity in a single merged + // multi-root proof. Built STRICT from day one (per #3812): the verifier rebuilds this + // exact merged query and verifies it with `verify_query` (succinctness on), so the + // proof cannot carry any branch beyond {nullifiers, identity}. The absence-proof + // variant is unusable here: it enumerates the query's terminal keys, which is + // impossible for `full_identity_query`'s unbounded all-keys range. + let nullifier_keys: Vec> = st.nullifiers(); + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys); + // `PathQuery::merge` rejects sub-queries that carry a limit, so leave it None. + let nullifier_pq = PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let mut identity_pq = Drive::full_identity_query( + &st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )?; + identity_pq.query.limit = None; + + PathQuery::merge( + vec![&nullifier_pq, &identity_pq], + &platform_version.drive.grove_version, + )? + } + StateTransition::IdentityTopUpFromShieldedPool(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::state_transition::identity_top_up_from_shielded_pool_transition::accessors::IdentityTopUpFromShieldedPoolTransitionAccessorsV0; + + // Spent nullifiers AND the credited identity in one STRICT merged proof, + // exactly the IdentityCreateFromShieldedPool shape. + let nullifier_keys: Vec> = st.nullifiers(); + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys); + let nullifier_pq = PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let mut identity_pq = Drive::full_identity_query( + &st.identity_id().to_buffer(), + &platform_version.drive.grove_version, + )?; + identity_pq.query.limit = None; + + PathQuery::merge( + vec![&nullifier_pq, &identity_pq], + &platform_version.drive.grove_version, + )? + } + StateTransition::ShieldFromIdentity(st) => { + // The identity's post-debit balance; the shielded note is not proven + // (the client learns it through shielded sync, as after `Shield`). + use dpp::state_transition::shield_from_identity_transition::accessors::ShieldFromIdentityTransitionAccessorsV0; + Drive::identity_balance_query(&st.identity_id().to_buffer()) + } + }; + + let proof = self.grove_get_proved_path_query( + &path_query, + transaction, + &mut vec![], + &platform_version.drive, + )?; + + Ok(ProofCreationResult::new_with_data(proof)) + } +} diff --git a/packages/rs-drive/src/query/document_history_drive_query.rs b/packages/rs-drive/src/query/document_history_drive_query.rs index beb1c971056..77327e395e1 100644 --- a/packages/rs-drive/src/query/document_history_drive_query.rs +++ b/packages/rs-drive/src/query/document_history_drive_query.rs @@ -235,12 +235,7 @@ impl DocumentHistoryDriveQuery { /// Queries the pointer, lifecycle reservation, and raw history tree separately. pub fn metadata_path_query(&self, version: &PlatformVersion) -> Result { self.validate()?; - let queries = [ - 0, - DOCUMENT_LIFECYCLE_TREE_KEY, - DOCUMENT_HISTORY_TREE_KEY, - ] - .map(|branch| { + let queries = [0, DOCUMENT_LIFECYCLE_TREE_KEY, DOCUMENT_HISTORY_TREE_KEY].map(|branch| { let mut path = contract_document_type_path_vec(&self.contract_id, &self.document_type_name); path.push(vec![branch]); @@ -327,6 +322,9 @@ impl DocumentHistoryDriveQuery { "a current document cannot also carry a lifecycle record", )); } + if count == Some(0) { + return Err(corrupt("an empty history tree must not remain in storage")); + } if active && count.unwrap_or_default() == 0 || !active && record.is_none() && count.unwrap_or_default() > 0 { diff --git a/packages/rs-drive/src/query/filter.rs b/packages/rs-drive/src/query/filter.rs index 8fc9bd6fa8c..5913805cf02 100644 --- a/packages/rs-drive/src/query/filter.rs +++ b/packages/rs-drive/src/query/filter.rs @@ -324,10 +324,6 @@ impl DriveDocumentQueryFilter<'_> { TransitionCheckResult::Fail } } - // An erase carries no document values and acts on a document that - // is already invisible to every document query, so no - // content-based subscription filter can describe it. - DocumentTransition::Erase(_) => TransitionCheckResult::Fail, DocumentTransition::IndexOnlyDelete(index_only_delete) => { if let DocumentActionMatchClauses::Delete { original_document_clauses, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs new file mode 100644 index 00000000000..83dc0bcb858 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs @@ -0,0 +1,138 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; +use crate::state_transition_action::batch::v1::{ + BatchTransitionActionV1, BatchedTransitionActionV1, +}; +use crate::util::batch::DriveOperation; +use dpp::block::epoch::Epoch; +use dpp::prelude::Identifier; +use dpp::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for BatchedTransitionActionV1 { + fn into_high_level_batch_drive_operations<'b>( + self, + epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match self { + BatchedTransitionActionV1::Batched(batched_action) => batched_action + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + BatchedTransitionActionV1::DocumentErase(erase_action) => erase_action + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + } + } +} + +/// Generation 1 of the batch conversion: consumes batch action format 1, whose +/// items may include the erase of a keep-history document, and emits the +/// operations of every item in the order the batch listed them. +impl DriveHighLevelOperationConverter for BatchTransitionActionV1 { + fn into_high_level_drive_operations<'b>( + self, + epoch: &Epoch, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .documents_batch_transition + { + 1 => { + let owner_id = self.owner_id(); + let transitions = self.transitions_owned(); + Ok(transitions + .into_iter() + .map(|transition| { + transition.into_high_level_batch_drive_operations( + epoch, + owner_id, + platform_version, + ) + }) + .collect::>, Error>>()? + .into_iter() + .flatten() + .collect()) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "BatchTransitionActionV1::into_high_level_drive_operations".to_string(), + known_versions: vec![1], + received: version, + })), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; + use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{ + BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0, + }; + use crate::util::batch::IdentityOperationType; + + fn bump(nonce: u64) -> BatchedTransitionActionV1 { + BatchedTransitionActionV1::Batched(BatchedTransitionAction::BumpIdentityDataContractNonce( + BumpIdentityDataContractNonceAction::V0(BumpIdentityDataContractNonceActionV0 { + identity_id: Identifier::from([0xAA; 32]), + data_contract_id: Identifier::from([0xBB; 32]), + identity_contract_nonce: nonce, + user_fee_increase: 0, + }), + )) + } + + #[test] + fn should_emit_the_operations_of_every_item_in_batch_order() { + let action = BatchTransitionActionV1 { + owner_id: Identifier::from([0xCC; 32]), + transitions: vec![bump(7), bump(8)], + user_fee_increase: 0, + }; + let epoch = Epoch::new(0).expect("epoch"); + + let operations = action + .into_high_level_drive_operations(&epoch, PlatformVersion::latest()) + .expect("operations"); + + let nonces: Vec = operations + .iter() + .map(|operation| match operation { + DriveOperation::IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { nonce, .. }, + ) => *nonce, + other => panic!("expected a nonce update, got {other:?}"), + }) + .collect(); + assert_eq!(nonces, vec![7, 8]); + } + + #[test] + fn should_refuse_a_protocol_version_whose_converter_only_knows_format_0() { + let action = BatchTransitionActionV1 { + owner_id: Identifier::from([0xCC; 32]), + transitions: vec![bump(7)], + user_fee_increase: 0, + }; + let epoch = Epoch::new(0).expect("epoch"); + + let result = action.into_high_level_drive_operations( + &epoch, + PlatformVersion::get(13).expect("protocol 13"), + ); + + assert!(matches!( + result, + Err(Error::Drive(DriveError::UnknownVersionMismatch { + received: 0, + .. + })) + )); + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs index 14d0a3631cc..faf8216f940 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs @@ -49,7 +49,6 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { }), DocumentOperation(DocumentOperationType::DeleteDocument { document_id: base.id(), - deleter_id: Some(owner_id), contract_info: DataContractInfo::DataContractFetchInfo( base.data_contract_fetch_info(), ), @@ -84,10 +83,59 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { Ok(ops) } + 1 => { + let base = self.base_owned(); + let contract_fetch_info = base.data_contract_fetch_info(); + let data_contract_id = base.data_contract_id(); + let identity_contract_nonce = base.identity_contract_nonce(); + let document_deletion_token_cost = base.token_cost(); + + let mut ops = vec![ + IdentityOperation(IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }), + DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { + document_id: base.id(), + deleter_id: Some(owner_id), + contract_info: DataContractInfo::DataContractFetchInfo( + base.data_contract_fetch_info(), + ), + document_type_info: DocumentTypeInfo::DocumentTypeName( + base.document_type_name_owned(), + ), + }), + ]; + + if let Some((token_id, effect, cost)) = document_deletion_token_cost { + match effect { + DocumentActionTokenEffect::TransferTokenToContractOwner => { + if owner_id != contract_fetch_info.contract.owner_id() { + ops.push(TokenOperation(TokenOperationType::TokenTransfer { + token_id, + sender_id: owner_id, + recipient_id: contract_fetch_info.contract.owner_id(), + amount: cost, + })); + } + } + DocumentActionTokenEffect::BurnToken => { + ops.push(TokenOperation(TokenOperationType::TokenBurn { + token_id, + identity_balance_holder_id: owner_id, + burn_amount: cost, + })); + } + } + } + + Ok(ops) + } version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "DocumentDeleteTransitionAction::into_high_level_document_drive_operations" .to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs index 96b7c9f3d82..0ef738b3131 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_erase_transition.rs @@ -27,7 +27,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentEraseTransitionAction { .convert_to_high_level_operations .document_erase_transition { - 0 => { + Some(0) => { let base = self.base_owned(); let data_contract_id = base.data_contract_id(); let identity_contract_nonce = base.identity_contract_nonce(); @@ -52,12 +52,17 @@ impl DriveHighLevelBatchOperationConverter for DocumentEraseTransitionAction { }), ]) } - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "DocumentEraseTransitionAction::into_high_level_document_drive_operations" .to_string(), known_versions: vec![0], received: version, })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "DocumentEraseTransitionAction::into_high_level_document_drive_operations" + .to_string(), + known_versions: vec![0], + })), } } } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs index 2178cdb16a3..6a6deca3152 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs @@ -56,13 +56,6 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransitionAction { platform_version, ) } - DocumentTransitionAction::EraseAction(document_erase_transition) => { - document_erase_transition.into_high_level_batch_drive_operations( - epoch, - owner_id, - platform_version, - ) - } DocumentTransitionAction::IndexOnlyDeleteAction( document_index_only_delete_transition, ) => document_index_only_delete_transition.into_high_level_batch_drive_operations( diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs index 99c1c40408a..07729a1862e 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs @@ -5,6 +5,7 @@ use dpp::platform_value::Identifier; use dpp::version::PlatformVersion; mod batch_transition; +mod batch_transition_v1; mod document; mod token; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs index a3a676e7851..db8279bdfb8 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs @@ -44,6 +44,9 @@ impl DriveHighLevelOperationConverter for StateTransitionAction { StateTransitionAction::BatchAction(documents_batch_transition) => { documents_batch_transition.into_high_level_drive_operations(epoch, platform_version) } + StateTransitionAction::BatchActionV1(documents_batch_transition) => { + documents_batch_transition.into_high_level_drive_operations(epoch, platform_version) + } StateTransitionAction::IdentityCreateAction(identity_create_transition) => { identity_create_transition.into_high_level_drive_operations(epoch, platform_version) } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs index d603791944a..b98a4618e1d 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs @@ -1,7 +1,7 @@ use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::{DocumentEraseTransitionAction, DocumentEraseTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; use dpp::fee::fee_result::FeeResult; use dpp::platform_value::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; @@ -18,7 +18,7 @@ impl DocumentEraseTransitionAction { get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, ) -> Result< ( - ConsensusValidationResult, + ConsensusValidationResult, FeeResult, ), Error, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs index 5f1262561ae..5a1dada3c15 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs @@ -2,8 +2,8 @@ use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; -use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::consensus::basic::document::InvalidDocumentTransitionActionError; use dpp::consensus::basic::BasicError; @@ -26,7 +26,7 @@ impl DocumentEraseTransitionActionV0 { get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, ) -> Result< ( - ConsensusValidationResult, + ConsensusValidationResult, FeeResult, ), Error, @@ -48,7 +48,9 @@ impl DocumentEraseTransitionActionV0 { ); return Ok(( ConsensusValidationResult::new_with_data_and_errors( - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + ), vec![ConsensusError::BasicError( BasicError::InvalidDocumentTransitionActionError( InvalidDocumentTransitionActionError::new(format!( @@ -81,8 +83,9 @@ impl DocumentEraseTransitionActionV0 { owner_id, user_fee_increase, ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + let batched_action = BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + ); return Ok(( ConsensusValidationResult::new_with_data_and_errors( @@ -95,9 +98,9 @@ impl DocumentEraseTransitionActionV0 { }; Ok(( - BatchedTransitionAction::DocumentAction(DocumentTransitionAction::EraseAction( + BatchedTransitionActionV1::DocumentErase( DocumentEraseTransitionActionV0 { base }.into(), - )) + ) .into(), FeeResult::default(), )) diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs index 3a88a2ab849..4a36b30372a 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs @@ -8,7 +8,6 @@ impl DocumentTransitionActionTypeGetter for DocumentTransitionAction { match self { DocumentTransitionAction::CreateAction(_) => DocumentTransitionActionType::Create, DocumentTransitionAction::DeleteAction(_) => DocumentTransitionActionType::Delete, - DocumentTransitionAction::EraseAction(_) => DocumentTransitionActionType::Erase, DocumentTransitionAction::ReplaceAction(_) => DocumentTransitionActionType::Replace, DocumentTransitionAction::TransferAction(_) => DocumentTransitionActionType::Transfer, DocumentTransitionAction::PurchaseAction(_) => DocumentTransitionActionType::Purchase, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs index 65cbdff1911..7f984f2de1b 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs @@ -28,8 +28,6 @@ use crate::state_transition_action::batch::batched_transition::document_transiti use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::{DocumentTransferTransitionAction, DocumentTransferTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::{DocumentUpdatePriceTransitionAction, DocumentUpdatePriceTransitionActionAccessorsV0}; @@ -56,9 +54,6 @@ pub enum DocumentTransitionAction { /// indexOnly delete-by-values — carries the document's property /// values, since there is no primary-storage row to fetch them from IndexOnlyDeleteAction(DocumentIndexOnlyDeleteTransitionAction), - /// Removes the retained revisions of an already deleted keep-history - /// document, a chunk at a time. - EraseAction(DocumentEraseTransitionAction), } impl DocumentTransitionAction { @@ -72,7 +67,6 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base(), DocumentTransitionAction::UpdatePriceAction(d) => d.base(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base(), - DocumentTransitionAction::EraseAction(d) => d.base(), } } @@ -86,7 +80,6 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base_owned(), DocumentTransitionAction::UpdatePriceAction(d) => d.base_owned(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base_owned(), - DocumentTransitionAction::EraseAction(d) => d.base_owned(), } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/mod.rs b/packages/rs-drive/src/state_transition_action/batch/mod.rs index 2bbc3ca7a38..5bc3ad969bc 100644 --- a/packages/rs-drive/src/state_transition_action/batch/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/mod.rs @@ -14,6 +14,8 @@ use crate::state_transition_action::batch::batched_transition::document_transiti pub mod batched_transition; /// v0 pub mod v0; +/// v1 +pub mod v1; #[cfg(test)] mod tests; diff --git a/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs b/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs new file mode 100644 index 00000000000..461d32456de --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs @@ -0,0 +1,272 @@ +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; +use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::batch::BatchTransitionAction; +use crate::state_transition_action::batch::v0::BatchTransitionActionV0; +use derive_more::From; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::fee::Credits; +use dpp::identifier::Identifier; +use dpp::identity::SecurityLevel; +use dpp::prelude::UserFeeIncrease; +use dpp::ProtocolError; + +/// One item of a batch action in format 1. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, From)] +pub enum BatchedTransitionActionV1 { + /// An action that batch action format 0 can express: a document, token or + /// identity contract nonce bump action. + Batched(BatchedTransitionAction), + /// The erase of an already deleted keep-history document, which only + /// batch action format 1 can express. + DocumentErase(DocumentEraseTransitionAction), +} + +impl BatchedTransitionActionV1 { + /// The data contract the item acts on. + pub fn data_contract_id(&self) -> Identifier { + match self { + BatchedTransitionActionV1::Batched(action) => action.data_contract_id(), + BatchedTransitionActionV1::DocumentErase(action) => action.base().data_contract_id(), + } + } + + /// The item as a format 0 action, when it is one. + pub fn as_batched_action(&self) -> Option<&BatchedTransitionAction> { + match self { + BatchedTransitionActionV1::Batched(action) => Some(action), + BatchedTransitionActionV1::DocumentErase(_) => None, + } + } + + /// The item as an erase action, when it is one. + pub fn as_document_erase_action(&self) -> Option<&DocumentEraseTransitionAction> { + match self { + BatchedTransitionActionV1::Batched(_) => None, + BatchedTransitionActionV1::DocumentErase(action) => Some(action), + } + } +} + +/// Batch action format 1: what a batch transition becomes once it has been +/// transformed into an action for a protocol version whose batch wire format +/// can carry an erase. +/// +/// It is a sibling of [`BatchTransitionAction`] rather than a variant of it. The +/// typed accessors of [`BatchTransitionAction`] hand out format 0 items, and +/// the validation generations selected by earlier protocol versions consume +/// them; a format that carries an erase cannot satisfy those signatures, so it +/// reaches those consumers through its own state transition action arm instead +/// and is only ever produced by the generations that know it. +#[derive(Default, Debug, Clone)] +pub struct BatchTransitionActionV1 { + /// The owner making the transitions + pub owner_id: Identifier, + /// The inner transitions, in the order the batch listed them + pub transitions: Vec, + /// fee multiplier + pub user_fee_increase: UserFeeIncrease, +} + +impl From for BatchTransitionActionV1 { + fn from(value: BatchTransitionActionV0) -> Self { + let BatchTransitionActionV0 { + owner_id, + transitions, + user_fee_increase, + } = value; + Self { + owner_id, + transitions: transitions + .into_iter() + .map(BatchedTransitionActionV1::Batched) + .collect(), + user_fee_increase, + } + } +} + +impl From for BatchTransitionActionV1 { + fn from(value: BatchTransitionAction) -> Self { + match value { + BatchTransitionAction::V0(v0) => v0.into(), + } + } +} + +impl BatchTransitionActionV1 { + /// owner id + pub fn owner_id(&self) -> Identifier { + self.owner_id + } + + /// transitions + pub fn transitions(&self) -> &Vec { + &self.transitions + } + + /// transitions + pub fn transitions_mut(&mut self) -> &mut Vec { + &mut self.transitions + } + + /// transitions + pub fn transitions_take(&mut self) -> Vec { + std::mem::take(&mut self.transitions) + } + + /// transitions owned + pub fn transitions_owned(self) -> Vec { + self.transitions + } + + /// set transitions + pub fn set_transitions(&mut self, transitions: Vec) { + self.transitions = transitions + } + + /// fee multiplier + pub fn user_fee_increase(&self) -> UserFeeIncrease { + self.user_fee_increase + } + + /// The sum of all purchases amount and all conflicting index collateral voting funds + pub fn all_used_balances(&self) -> Result, ProtocolError> { + Ok(match (self.all_purchases_amount()?, self.all_conflicting_index_collateral_voting_funds()?) { + (Some(all_purchases_amount), Some(all_conflicting_index_collateral_voting_funds)) => Some(all_purchases_amount.checked_add(all_conflicting_index_collateral_voting_funds).ok_or(ProtocolError::Overflow("overflow between all_purchases_amount and all_conflicting_index_collateral_voting_funds"))?), + (Some(all_purchases_amount), None) => Some(all_purchases_amount), + (None, Some(all_conflicting_index_collateral_voting_funds)) => Some(all_conflicting_index_collateral_voting_funds), + (None, None) => None, + }) + } + + /// The sum of all purchases amounts for all purchase transitions in the batch. + /// An erase purchases nothing. + pub fn all_purchases_amount(&self) -> Result, ProtocolError> { + let (total, any_purchases): (Option, bool) = self + .transitions + .iter() + .filter_map(|transition| match transition { + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( + DocumentTransitionAction::PurchaseAction(document_purchase), + )) => Some(document_purchase.price()), + BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( + TokenTransitionAction::DirectPurchaseAction(token_purchase), + )) => Some(token_purchase.total_agreed_price()), + _ => None, + }) + .fold((None, false), |(acc, _), price| match acc { + Some(acc_val) => acc_val + .checked_add(price) + .map_or((None, true), |sum| (Some(sum), true)), + None => (Some(price), true), + }); + + match (total, any_purchases) { + (Some(total), _) => Ok(Some(total)), + (None, true) => Err(ProtocolError::Overflow("overflow in all purchases amount")), + _ => Ok(None), + } + } + + /// The sum of all conflicting index collateral voting funds for all document create transitions in the batch. + /// An erase prefunds nothing. + pub fn all_conflicting_index_collateral_voting_funds( + &self, + ) -> Result, ProtocolError> { + let (total, any_voting_funds): (Option, bool) = self + .transitions + .iter() + .filter_map(|transition| match transition { + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( + DocumentTransitionAction::CreateAction(document_create_transition_action), + )) => document_create_transition_action + .prefunded_voting_balance() + .iter() + .try_fold(0u64, |acc, &(_, val)| acc.checked_add(val)), + _ => None, + }) + .fold((None, false), |(acc, _), price| match acc { + Some(acc_val) => acc_val + .checked_add(price) + .map_or((None, true), |sum| (Some(sum), true)), + None => (Some(price), true), + }); + + match (total, any_voting_funds) { + (Some(total), _) => Ok(Some(total)), + (None, true) => Err(ProtocolError::Overflow( + "overflow in all voting funds amount", + )), + _ => Ok(None), + } + } + + /// The security levels a key must have to sign this batch: the highest + /// level any document type or token action in the batch requires. + /// + /// An erase acts on a document type like any other document action, so it + /// contributes that type's requirement. + pub fn combined_security_level_requirement(&self) -> Result, ProtocolError> { + let mut highest_security_level = SecurityLevel::lowest_level(); + + for transition in self.transitions.iter() { + let document_security_level = match transition { + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( + document_transition, + )) => { + let document_type_name = document_transition.base().document_type_name(); + let data_contract_info = document_transition.base().data_contract_fetch_info(); + + data_contract_info + .contract + .document_type_for_name(document_type_name)? + .security_level_requirement() + } + BatchedTransitionActionV1::DocumentErase(erase_transition) => { + let document_type_name = erase_transition.base().document_type_name(); + let data_contract_info = erase_transition.base().data_contract_fetch_info(); + + data_contract_info + .contract + .document_type_for_name(document_type_name)? + .security_level_requirement() + } + BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction(_)) => { + SecurityLevel::CRITICAL + } + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(_), + ) => continue, + }; + + // lower enum representation means higher in security + if document_security_level < highest_security_level { + highest_security_level = document_security_level + } + } + + Ok(if highest_security_level == SecurityLevel::MASTER { + vec![SecurityLevel::MASTER] + } else { + // this might seem wrong until you realize that master is 0, critical 1, etc + (SecurityLevel::CRITICAL as u8..=highest_security_level as u8) + .map(|security_level| { + SecurityLevel::try_from(security_level).map_err(|_| { + ProtocolError::CorruptedCodeExecution(format!( + "security level {security_level} is between two known levels" + )) + }) + }) + .collect::, _>>()? + }) + } +} diff --git a/packages/rs-drive/src/state_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/mod.rs index ee88bc1cf2e..9c3719e8615 100644 --- a/packages/rs-drive/src/state_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/mod.rs @@ -17,6 +17,7 @@ pub mod shielded; use crate::state_transition_action::address_funds::address_credit_withdrawal::AddressCreditWithdrawalTransitionAction; use crate::state_transition_action::address_funds::address_funding_from_asset_lock::AddressFundingFromAssetLockTransitionAction; use crate::state_transition_action::address_funds::address_funds_transfer::AddressFundsTransferTransitionAction; +use crate::state_transition_action::batch::v1::BatchTransitionActionV1; use crate::state_transition_action::batch::BatchTransitionAction; use crate::state_transition_action::contract::data_contract_create::DataContractCreateTransitionAction; use crate::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; @@ -62,6 +63,10 @@ pub enum StateTransitionAction { DataContractUpdateAction(DataContractUpdateTransitionAction), /// batch BatchAction(BatchTransitionAction), + /// batch, in the action format that can carry an erase of a keep-history + /// document; produced only for protocol versions whose batch wire format + /// carries one + BatchActionV1(BatchTransitionActionV1), /// identity create IdentityCreateAction(IdentityCreateTransitionAction), /// identity create from addresses @@ -125,6 +130,7 @@ impl StateTransitionAction { StateTransitionAction::DataContractCreateAction(action) => action.user_fee_increase(), StateTransitionAction::DataContractUpdateAction(action) => action.user_fee_increase(), StateTransitionAction::BatchAction(action) => action.user_fee_increase(), + StateTransitionAction::BatchActionV1(action) => action.user_fee_increase(), StateTransitionAction::IdentityCreateAction(action) => action.user_fee_increase(), StateTransitionAction::IdentityTopUpAction(action) => action.user_fee_increase(), StateTransitionAction::IdentityCreditWithdrawalAction(action) => { diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs index d20d7616265..2a746d36839 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs @@ -1,7 +1,7 @@ use dpp::platform_value::Identifier; use dpp::prelude::UserFeeIncrease; use dpp::ProtocolError; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRef, BatchedTransitionRefV1}; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; @@ -9,8 +9,10 @@ use dpp::state_transition::batch_transition::document_base_transition::DocumentB use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; use crate::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0}; @@ -37,6 +39,52 @@ impl BumpIdentityDataContractNonceAction { } } + /// from a borrowed transition of any batch format + pub fn from_batched_transition_ref_v1( + value: BatchedTransitionRefV1, + identity_id: Identifier, + user_fee_increase: UserFeeIncrease, + ) -> Self { + match value { + BatchedTransitionRefV1::Document(document) => { + Self::from_borrowed_document_base_transition( + document.base(), + identity_id, + user_fee_increase, + ) + } + BatchedTransitionRefV1::Token(token) => Self::from_borrowed_token_base_transition( + token.base(), + identity_id, + user_fee_increase, + ), + } + } + + /// helper method for an item of batch action format 1 + pub fn try_from_borrowed_batched_transition_action_v1( + value: &BatchedTransitionActionV1, + identity_id: Identifier, + user_fee_increase: UserFeeIncrease, + ) -> Result { + match value { + BatchedTransitionActionV1::Batched(batched_action) => { + Self::try_from_borrowed_batched_transition_action( + batched_action, + identity_id, + user_fee_increase, + ) + } + BatchedTransitionActionV1::DocumentErase(erase_action) => { + Ok(Self::from_borrowed_document_base_transition_action( + erase_action.base(), + identity_id, + user_fee_increase, + )) + } + } + } + /// helper method pub fn try_from_batched_transition_action( value: BatchedTransitionAction, 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 1fb0ff2fa22..1f8b48cee1f 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 @@ -98,9 +98,17 @@ pub enum DocumentOperationType<'a> { DeleteDocument { /// The document id document_id: Identifier, - /// The identity credited with the bytes of the lifecycle record a - /// keep-history delete writes, and refunded when an erase removes it. - /// `None` where no signer stands behind the delete. + /// Data Contract info to potentially be resolved if needed + contract_info: DataContractInfo<'a>, + /// Document type + document_type_info: DocumentTypeInfo<'a>, + }, + /// Deletes a document while retaining the lifecycle metadata needed by a + /// keep-history document. + DeleteDocumentWithLifecycle { + /// The document id + document_id: Identifier, + /// The identity credited with the lifecycle record's bytes. deleter_id: Option, /// Data Contract info to potentially be resolved if needed contract_info: DataContractInfo<'a>, @@ -224,6 +232,11 @@ impl DocumentOperationType<'_> { document_type_info, .. } + | Self::DeleteDocumentWithLifecycle { + contract_info, + document_type_info, + .. + } | Self::DeleteIndexOnlyDocument { contract_info, document_type_info, @@ -265,7 +278,9 @@ impl DocumentOperationType<'_> { Ok(()) } // These write to system contracts, which have no TTL indexes. - Self::AddWithdrawalDocument { .. } | Self::DocumentHistory { .. } => Ok(()), + Self::AddWithdrawalDocument { .. } + | Self::DocumentHistory { .. } + | Self::EraseDocument { .. } => Ok(()), } } @@ -436,7 +451,6 @@ impl DocumentOperationType<'_> { } DocumentOperationType::DeleteDocument { document_id, - deleter_id, contract_info, document_type_info, } => { @@ -452,6 +466,34 @@ impl DocumentOperationType<'_> { let document_type = document_type_info.resolve(contract)?; drive.delete_document_for_contract_operations_without_ttl_drain( + document_id, + contract, + document_type, + None, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + platform_version, + ) + } + DocumentOperationType::DeleteDocumentWithLifecycle { + document_id, + deleter_id, + contract_info, + document_type_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 = document_type_info.resolve(contract)?; + + drive.delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( document_id, contract, document_type, diff --git a/packages/rs-drive/src/verify/document/verify_document_history/tests.rs b/packages/rs-drive/src/verify/document/verify_document_history/tests.rs index e9db91b5ae8..6df417b9187 100644 --- a/packages/rs-drive/src/verify/document/verify_document_history/tests.rs +++ b/packages/rs-drive/src/verify/document/verify_document_history/tests.rs @@ -232,16 +232,36 @@ fn should_authenticate_history_pages_metadata_and_absence() { .value .unwrap(); query.filter = DocumentHistoryFilter::StartAtTime(0); - let (page, proof) = prove(&drive, &query, document_type, version); - assert!( - proof.entries_proof.is_some(), - "an empty but present tree still needs an entries proof" - ); - let (_, verified) = verify(&query, &proof, document_type, version).unwrap(); - assert_eq!(page, verified); - let mut missing = proof; - missing.entries_proof = None; - assert!(verify(&query, &missing, document_type, version).is_err()); + assert!(drive + .fetch_document_history(&query, document_type, None, version) + .is_err()); + assert!(drive + .prove_document_history(&query, document_type, None, version) + .is_err()); + + // A hostile peer can still assemble the two individually valid GroveDB + // proofs, so the verifier must reject the corrupt empty-present tree too. + let proof = DocumentHistoryProof { + metadata_proof: drive + .grove_get_proved_path_query( + &query.metadata_path_query(version).unwrap(), + None, + &mut vec![], + &version.drive, + ) + .unwrap(), + entries_proof: Some( + drive + .grove_get_proved_path_query( + &query.construct_path_query(version).unwrap(), + None, + &mut vec![], + &version.drive, + ) + .unwrap(), + ), + }; + assert!(verify(&query, &proof, document_type, version).is_err()); } #[test] diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs index 3863413a0e2..80ba10a04b7 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs @@ -11,6 +11,7 @@ use dpp::state_transition::StateTransition; use dpp::version::PlatformVersion; mod v0; +mod v1; impl Drive { /// Verifies a state transition against a provided proof, returning the @@ -70,9 +71,16 @@ impl Drive { known_contracts_provider_fn, platform_version, ), + 1 => Drive::verify_state_transition_was_executed_with_proof_v1( + state_transition, + block_info, + proof, + known_contracts_provider_fn, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "verify_state_transition_was_executed_with_proof".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 7a80fb40d4f..47a467910e8 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -503,24 +503,6 @@ impl Drive { .to_string(), ))) } - DocumentTransition::Erase(erase_transition) => { - // An erase acts on a document that was already - // deleted, so the by-id proof shows it absent - // both before and after. The absence is the - // state the transition affected, not evidence - // that this erase ran; the classifier below - // reports it as such. - if document.is_some() { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained a current document for erased id {}", erase_transition.base().id())))); - } - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - erase_transition.base().id(), - None, - )])), - )) - } } } BatchedTransitionRef::Token(token_transition) => { @@ -2357,17 +2339,10 @@ impl Drive { data_contract_id ))), )?; - // EXCEPT an erase: the document it acts on was already - // deleted, so the by-id proof shows the same absence - // whether or not this erase ran. - if matches!(document_transition, DocumentTransition::Erase(_)) { - false - } else { - !contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? - .index_only() - } + !contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? + .index_only() } Some(BatchedTransitionRef::Token(token_transition)) => { let data_contract_id = token_transition.data_contract_id(); @@ -3442,160 +3417,6 @@ mod tests { } } - /// An erase acts on a document that was already deleted, so the by-id - /// proof shows the same absence whether or not it ran. The classifier must - /// report that as affected state, while a delete over the very same proof - /// stays execution-proved: presence before and absence after do bind a - /// delete's execution. - #[test] - fn verify_batch_document_erase_is_affected_state_not_execution_proved() { - use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; - use dpp::document::DocumentV0Setters; - use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::{ - DocumentEraseTransition, DocumentEraseTransitionV0, - }; - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::{ - DocumentDeleteTransition, DocumentDeleteTransitionV0, - }; - use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV0}; - use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; - - let platform_version = PlatformVersion::latest(); - let drive = setup_drive_with_initial_state_structure(None); - let contract = json_document_to_contract( - "tests/supporting_files/contract/family/family-contract-with-history.json", - false, - platform_version, - ) - .expect("expected the family history contract"); - drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - None, - None, - platform_version, - ) - .expect("expected to apply the contract"); - let document_type = contract - .document_type_for_name("person") - .expect("expected the person type"); - let mut document = json_document_to_document( - "tests/supporting_files/contract/family/person0.json", - Some([4u8; 32].into()), - document_type, - platform_version, - ) - .expect("expected a person"); - for revision in 1..=2u64 { - document.set_revision(Some(revision)); - drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - revision > 1, - BlockInfo::default_with_time(1_000 + revision), - true, - None, - platform_version, - None, - ) - .expect("expected to write a revision"); - } - let doc_id = document.id(); - drive - .delete_document_for_contract( - doc_id, - &contract, - "person", - BlockInfo::default_with_time(5_000), - true, - None, - platform_version, - None, - ) - .expect("expected the delete to succeed"); - - let path_query = SingleDocumentDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "person".to_string(), - document_type_keeps_history: true, - document_id: doc_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - } - .construct_path_query(platform_version) - .expect("expected a by-id path query"); - let proof = drive - .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) - .expect("expected an absence proof"); - - let base = || { - DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: doc_id, - identity_contract_nonce: 1, - document_type_name: "person".to_string(), - data_contract_id: contract.id(), - }) - }; - let batch = |transition: DocumentTransition| { - StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![transition], - ..Default::default() - })) - }; - let contract_arc = Arc::new(contract.clone()); - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); - let verify = |state_transition: StateTransition| { - Drive::verify_state_transition_was_executed_with_proof( - &state_transition, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ) - .expect("expected verification to succeed") - .1 - }; - - let erase_outcome = verify(batch(DocumentTransition::Erase( - DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: base() }), - ))); - match erase_outcome { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedDocuments(documents), - ) => { - assert_eq!(documents.len(), 1); - let (id, found) = documents.into_iter().next().unwrap(); - assert_eq!(id, doc_id); - assert!(found.is_none(), "the document is absent by id"); - } - other => panic!("an erase must be classified as affected state, got {other:?}"), - } - - let delete_outcome = verify(batch(DocumentTransition::Delete( - DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { base: base() }), - ))); - assert!( - matches!( - delete_outcome, - StateTransitionProofOutcome::ExecutionProved(_) - ), - "a delete over the same proof stays execution-proved, got {delete_outcome:?}" - ); - } - // ----------------------------------------------------------------------- // Batch: document create happy path // ----------------------------------------------------------------------- diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs new file mode 100644 index 00000000000..e90b07c857f --- /dev/null +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs @@ -0,0 +1,6409 @@ +use std::collections::BTreeMap; +use dpp::address_funds::PlatformAddress; +use dpp::balances::credits::TokenAmount; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::config::v0::DataContractConfigGettersV0; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_keeps_history_rules::accessors::v0::TokenKeepsHistoryRulesV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::serialized_version::DataContractInSerializationFormat; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::document::document_methods::DocumentMethodsV0; +use dpp::document::property_names::PRICE; +use dpp::fee::Credits; +use dpp::group::group_action_status::GroupActionStatus; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{IdentityPublicKey, KeyID, PartialIdentity}; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::prelude::{AddressNonce, Identifier}; +use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; +use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRefV1, DocumentTransitionRefV1}; +use dpp::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0; +use dpp::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; +use dpp::state_transition::identity_credit_transfer_to_addresses_transition::accessors::IdentityCreditTransferToAddressesTransitionAccessorsV0; +use dpp::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0; +use dpp::state_transition::{StateTransition, StateTransitionOwned, StateTransitionWitnessSigned}; +use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; +use dpp::state_transition::batch_transition::document_create_transition::DocumentFromCreateTransition; +use dpp::state_transition::batch_transition::document_replace_transition::DocumentFromReplaceTransition; +use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; +use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::token_freeze_transition::v0::v0_methods::TokenFreezeTransitionV0Methods; +use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_methods::TokenMintTransitionV0Methods; +use dpp::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; +use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; +use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; +use dpp::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; +use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; +use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; +use dpp::state_transition::proof_result::StateTransitionProofOutcome; +use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule}; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; +use dpp::voting::vote_polls::VotePoll; +use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; +use dpp::voting::votes::Vote; +use platform_version::TryIntoPlatformVersioned; +use platform_version::version::PlatformVersion; +use crate::drive::Drive; +use crate::drive::identity::key::fetch::IdentityKeysRequest; +use crate::verify::RootHash; +use crate::error::Error; +use crate::error::proof::ProofError; +use crate::query::{ContractLookupFn, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + +impl Drive { + #[inline(always)] + pub(super) fn verify_state_transition_was_executed_with_proof_v1( + state_transition: &StateTransition, + block_info: &BlockInfo, + proof: &[u8], + known_contracts_provider_fn: &ContractLookupFn, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, StateTransitionProofOutcome), Error> { + let (root_hash, result) = match state_transition { + StateTransition::DataContractCreate(data_contract_create) => { + // we expect to get a contract that matches the state transition + let keeps_history = data_contract_create + .data_contract() + .config() + .keeps_history(); + let (root_hash, contract) = Drive::verify_contract( + proof, + Some(keeps_history), + false, + true, + data_contract_create.data_contract().id().into_buffer(), + platform_version, + )?; + let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (create)", data_contract_create.data_contract().id()))))?; + let contract_for_serialization: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version)?; + + if let Some(mismatch) = + contract_for_serialization.first_mismatch(data_contract_create.data_contract()) + { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after create with id {}: {}", data_contract_create.data_contract().id(), mismatch)))); + } + + Ok((root_hash, VerifiedDataContract(contract))) + } + StateTransition::DataContractUpdate(data_contract_update) => { + // we expect to get a contract that matches the state transition + let keeps_history = data_contract_update + .data_contract() + .config() + .keeps_history(); + let (root_hash, contract) = Drive::verify_contract( + proof, + Some(keeps_history), + false, + true, + data_contract_update.data_contract().id().into_buffer(), + platform_version, + )?; + let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (update", data_contract_update.data_contract().id()))))?; + let contract_for_serialization: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version)?; + if let Some(mismatch) = + contract_for_serialization.first_mismatch(data_contract_update.data_contract()) + { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", data_contract_update.data_contract().id(), mismatch)))); + } + Ok((root_hash, VerifiedDataContract(contract))) + } + StateTransition::Batch(documents_batch_transition) => { + if documents_batch_transition.transitions_len_v1() > 1 { + return Err(Error::Proof(ProofError::InvalidTransition(format!("version {} does not support more than one document in a document batch transition", platform_version.protocol_version)))); + } + let Some(transition) = documents_batch_transition.first_transition_v1() else { + return Err(Error::Proof(ProofError::InvalidTransition( + "no transition in a document batch transition".to_string(), + ))); + }; + + let owner_id = documents_batch_transition.owner_id(); + + match transition { + BatchedTransitionRefV1::Document(document_transition) => { + let data_contract_id = document_transition.data_contract_id(); + + let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( + Error::Proof(ProofError::UnknownContract(format!( + "unknown contract with id {} in document verification", + data_contract_id + ))), + )?; + + let document_type = contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| { + Error::Proof(ProofError::UnknownContract(format!( + "cannot fetch contract for document {} with id {}: {}", + document_transition.document_type_name(), + document_transition.data_contract_id(), + e + ))) + })?; + + // indexOnly documents have no primary row: the executed + // transition is verified against the single entry its + // values produce under the proof index — rebuilt here + // from the transition through the same builder the + // prover used. + { + use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; + use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; + if document_type.index_only() { + let values = match document_transition { + DocumentTransitionRefV1::Create(create_transition) => { + create_transition.data().clone() + } + // The indexOnlyDelete kind always + // carries its values. + DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { + delete_transition.data().clone() + } + _ => { + return Err(Error::Proof(ProofError::IncorrectProof( + "indexOnly documents only support create and \ + indexOnlyDelete" + .to_string(), + ))); + } + }; + let path_query = crate::query::index_only_synthesis::index_only_transition_entry_path_query( + contract.id(), + document_type, + &values, + documents_batch_transition.owner_id(), + platform_version, + )?; + let (root_hash, mut proved) = grovedb::GroveDb::verify_query( + proof, + &path_query, + &platform_version.drive.grove_version, + )?; + let entry_element = + proved.pop().and_then(|(_path, _key, element)| element); + + let (root_hash, result) = match document_transition { + DocumentTransitionRefV1::Create(create_transition) => { + let expected_document = + Document::try_from_create_transition( + create_transition, + documents_batch_transition.owner_id(), + block_info, + &contract, + &document_type, + platform_version, + )?; + // The entry's element shape follows the + // proof index's sum axis: a summable + // index stores `ItemWithSumItem( + // commitment, amount)`, a plain one + // stores `Item(commitment)`. The shapes + // are checked strictly — an element of + // the wrong shape is a wrong proof, and + // for the sum-bearing shape the proved + // amount must equal the created + // document's contribution (the value + // the write path froze into the + // element). + let proof_index = crate::query::index_only_synthesis::index_only_proof_index(&document_type)?; + let payload = match ( + entry_element, + proof_index.summable.as_deref(), + ) { + (Some(grovedb::Element::Item(payload, _)), None) => { + payload + } + ( + Some(grovedb::Element::ItemWithSumItem( + payload, + sum_value, + _, + )), + Some(sum_property), + ) => { + let expected_sum = + crate::drive::document::read_document_sum_contribution( + &expected_document, + sum_property, + )?; + if sum_value != expected_sum { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "the proved indexOnly entry's sum contribution does not match the created document {}", + create_transition.base().id() + )))); + } + payload + } + _ => { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "proof did not contain the indexOnly entry item expected to exist after create of {}", + create_transition.base().id() + )))); + } + }; + // Entry presence alone only proves that + // SOME row projects onto this position; + // the stored row commitment binds the + // entry to its document's full tuple, + // so a pre-existing row with the same + // projection but different values in + // other indexes cannot masquerade as + // this create. (`$createdAt`, when the + // type requires it, enters the + // commitment from the caller-supplied + // block info — the same execution-block + // assumption the expected document is + // built under.) + let expected_commitment = + crate::drive::document::index_only_row_commitment( + &expected_document, + document_type, + platform_version, + )?; + if payload != expected_commitment { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "the proved indexOnly entry's row commitment does not match the created document {}: the entry belongs to a different row", + create_transition.base().id() + )))); + } + ( + root_hash, + VerifiedDocuments(BTreeMap::from([( + expected_document.id(), + Some(expected_document), + )])), + ) + } + DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { + if entry_element.is_some() { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "proof still contained the indexOnly entry after delete of {}", + delete_transition.base().id() + )))); + } + ( + root_hash, + VerifiedDocuments(BTreeMap::from([( + delete_transition.base().id(), + None, + )])), + ) + } + _ => { + return Err(Error::Proof(ProofError::IncorrectProof( + "indexOnly documents only support create and \ + indexOnlyDelete" + .to_string(), + ))) + } + }; + + // The classifier below is the single + // authority on binding, and it returns false + // for every indexOnly document transition: a + // second create with identical owner/values + // (different entropy) shares the proven entry + // while only one of them executed, and a + // delete's absence proof may describe state + // that was already absent — the proof attests + // the resulting STATE (`AffectedState`), not + // the execution. + let outcome = if Self::state_transition_proof_binds_execution_v1( + state_transition, + known_contracts_provider_fn, + )? { + StateTransitionProofOutcome::ExecutionProved(result) + } else { + StateTransitionProofOutcome::AffectedState(result) + }; + return Ok((root_hash, outcome)); + } + } + + let contested_status = + if let DocumentTransitionRefV1::Create(create_transition) = + document_transition + { + if create_transition.prefunded_voting_balance().is_some() { + SingleDocumentDriveQueryContestedStatus::Contested + } else { + SingleDocumentDriveQueryContestedStatus::NotContested + } + } else { + SingleDocumentDriveQueryContestedStatus::NotContested + }; + + let query = SingleDocumentDriveQuery { + contract_id: document_transition.data_contract_id().into_buffer(), + document_type_name: document_transition.document_type_name().clone(), + document_type_keeps_history: document_type.documents_keep_history(), + document_id: document_transition.base().id().into_buffer(), + block_time_ms: None, //None because we want latest + contested_status, + }; + let (root_hash, document) = + query.verify_proof(false, proof, document_type, platform_version)?; + + match document_transition { + DocumentTransitionRefV1::Create(create_transition) => { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (create)", create_transition.base().id()))))?; + let expected_document = Document::try_from_create_transition( + create_transition, + documents_batch_transition.owner_id(), + block_info, + &contract, + &document_type, + platform_version, + )?; + + let transient_fields = document_type + .transient_fields() + .iter() + .map(|a| a.as_str()) + .collect(); + + if !document.is_equal_ignoring_time_based_fields( + &expected_document, + Some(transient_fields), + platform_version, + )? { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document (time fields were not checked) after create, got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, create_transition)))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + document.id(), + Some(document), + )])), + )) + } + DocumentTransitionRefV1::Replace(replace_transition) => { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (replace)", replace_transition.base().id()))))?; + let expected_document = Document::try_from_replace_transition( + replace_transition, + documents_batch_transition.owner_id(), + document.created_at(), //we can trust the created at (as we don't care) + document.created_at_block_height(), //we can trust the created at block height (as we don't care) + document.created_at_core_block_height(), //we can trust the created at core block height (as we don't care) + document.created_at(), //we can trust the created at (as we don't care) + document.created_at_block_height(), //we can trust the created at block height (as we don't care) + document.created_at_core_block_height(), //we can trust the created at core block height (as we don't care) + document.creator_id(), + block_info, + &document_type, + platform_version, + )?; + + let transient_fields = document_type + .transient_fields() + .iter() + .map(|a| a.as_str()) + .collect(); + + if !document.is_equal_ignoring_time_based_fields( + &expected_document, + Some(transient_fields), + platform_version, + )? { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document (time fields were not checked) after replace, got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, replace_transition)))); + } + + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + document.id(), + Some(document), + )])), + )) + } + DocumentTransitionRefV1::Transfer(transfer_transition) => { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (transfer)", transfer_transition.base().id()))))?; + let recipient_owner_id = transfer_transition.recipient_owner_id(); + + if document.owner_id() != recipient_owner_id { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not have the transfer executed after expected transfer with id {}", transfer_transition.base().id())))); + } + + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + document.id(), + Some(document), + )])), + )) + } + DocumentTransitionRefV1::Delete(delete_transition) => { + if document.is_some() { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained document after delete with id {}", delete_transition.base().id())))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + delete_transition.base().id(), + None, + )])), + )) + } + DocumentTransitionRefV1::UpdatePrice(update_price_transition) => { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (update price)", update_price_transition.base().id()))))?; + let new_document_price : Credits = document.properties().get_integer(PRICE).map_err(|e| Error::Proof(ProofError::IncorrectProof(format!("proof did not contain a document that contained a price field with id {} expected to exist because of state transition (update price): {}", update_price_transition.base().id(), e))))?; + if new_document_price != update_price_transition.price() { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document update of price after price update with id {}", update_price_transition.base().id())))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + document.id(), + Some(document), + )])), + )) + } + DocumentTransitionRefV1::Purchase(purchase_transition) => { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (purchase)", purchase_transition.base().id()))))?; + + if document.owner_id() != owner_id { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not have the transfer executed after expected transfer with id {}", purchase_transition.base().id())))); + } + + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + document.id(), + Some(document), + )])), + )) + } + DocumentTransitionRefV1::IndexOnlyDelete(_) => { + // Only reachable when the doctype is NOT + // indexOnly (the indexOnly branch above + // returns first): an indexOnlyDelete aimed at + // a stored type can never have executed, so + // there is nothing a proof could attest. + Err(Error::Proof(ProofError::InvalidTransition( + "an indexOnlyDelete cannot execute against a stored \ + document type" + .to_string(), + ))) + } + DocumentTransitionRefV1::Erase(erase_transition) => { + // An erase acts on a document that was already + // deleted, so the by-id proof shows it absent + // both before and after. The absence is the + // state the transition affected, not evidence + // that this erase ran; the classifier below + // reports it as such. + if document.is_some() { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained a current document for erased id {}", erase_transition.base().id())))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + erase_transition.base().id(), + None, + )])), + )) + } + } + } + BatchedTransitionRefV1::Token(token_transition) => { + let data_contract_id = token_transition.data_contract_id(); + let token_id = token_transition.token_id(); + + let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( + Error::Proof(ProofError::UnknownContract(format!( + "unknown contract with id {} in token verification", + data_contract_id + ))), + )?; + + let identity_contract_nonce = + token_transition.base().identity_contract_nonce(); + + let token_history_document_type_name = + token_transition.historical_document_type_name().to_string(); + + let token_history_contract = load_system_data_contract( + SystemDataContract::TokenHistory, + platform_version, + )?; + + let token_history_document_type = + token_transition.historical_document_type(&token_history_contract)?; + + let token_config = contract.expected_token_configuration( + token_transition.base().token_contract_position(), + )?; + let keeps_historical_document = token_config.keeps_history(); + + let historical_query = || { + let query = SingleDocumentDriveQuery { + contract_id: token_history_contract.id().into_buffer(), + document_type_name: token_history_document_type_name, + document_type_keeps_history: false, + document_id: token_transition + .historical_document_id(owner_id) + .to_buffer(), + block_time_ms: None, //None because we want latest + contested_status: + SingleDocumentDriveQueryContestedStatus::NotContested, + }; + + let is_group_action = + token_transition.base().using_group_info().is_some(); + + let (root_hash, document) = query.verify_proof( + is_group_action, // it will be a subset if it is a group action + proof, + token_history_document_type, + platform_version, + )?; + + if let Some(document) = &document { + let expected_document = token_transition + .build_historical_document( + token_id, + owner_id, + identity_contract_nonce, + &BlockInfo::default(), + token_config, + platform_version, + )?; + + // Some fields are populated by the drive, + // so we need to ignore them + let ignore_fields = match token_transition { + TokenTransition::DestroyFrozenFunds(_) => { + Some(vec!["destroyedAmount", "note"]) + } + TokenTransition::Burn(_) => Some(vec!["burnFromId", "note"]), + TokenTransition::Claim(_) => Some(vec!["amount"]), + TokenTransition::DirectPurchase(_) => { + let purchase_cost: Credits = + document.properties().get_integer("purchaseCost")?; + let agreed_to_purchase_cost: Credits = expected_document + .properties() + .get_integer("purchaseCost")?; + if purchase_cost > agreed_to_purchase_cost { + return Err(Error::Proof(ProofError::UnexpectedResultProof(format!("proof of state transition execution showed a purchase price of {}, whereas we only agreed to {}, state transition is [{}]", purchase_cost, agreed_to_purchase_cost, token_transition)))); + } + Some(vec!["purchaseCost"]) + } + TokenTransition::Mint(_) + | TokenTransition::Freeze(_) + | TokenTransition::Unfreeze(_) + | TokenTransition::EmergencyAction(_) + | TokenTransition::ConfigUpdate(_) + | TokenTransition::SetPriceForDirectPurchase(_) + if token_transition.base().using_group_info().is_some() => + { + Some(vec!["note"]) + } + _ => None, + }; + + if !document.is_equal_ignoring_time_based_fields( + &expected_document, + ignore_fields, + platform_version, + )? { + return Err(Error::Proof(ProofError::UnexpectedResultProof(format!("proof of state transition execution did not show the correct historical document got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, token_transition)))); + } + } + + if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let action_status = if document.is_some() { + GroupActionStatus::ActionClosed + } else { + GroupActionStatus::ActionActive + }; + let sum_power = Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + Some(action_status), + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )? + .2; + Ok(( + root_hash, + VerifiedTokenGroupActionWithDocument(sum_power, document), + )) + } else { + let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document of type `{}` expected to exist because the token keeps historical documents", token_transition.historical_document_type_name()))))?; + Ok((root_hash, VerifiedTokenActionWithDocument(document))) + } + }; + match token_transition { + TokenTransition::Burn(_) => { + if keeps_historical_document.keeps_burning_history() { + historical_query() + } else if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, balance) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + owner_id.into_buffer(), + true, + platform_version, + )?; + if status == GroupActionStatus::ActionClosed + && balance.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token burn)", owner_id)))); + }; + + Ok(( + root_hash, + VerifiedTokenGroupActionWithTokenBalance( + sum_power, status, balance, + ), + )) + } else { + { + let (root_hash, Some(balance)) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + owner_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token burn)", owner_id)))); + }; + Ok((root_hash, VerifiedTokenBalance(owner_id, balance))) + } + } + } + TokenTransition::Mint(token_mint_transition) => { + if keeps_historical_document.keeps_minting_history() { + historical_query() + } else if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let recipient_id = + token_mint_transition.recipient_id(token_config)?; + + let (root_hash, balance) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + recipient_id.into_buffer(), + true, + platform_version, + )?; + if status == GroupActionStatus::ActionClosed + && balance.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token mint)", owner_id)))); + }; + + Ok(( + root_hash, + VerifiedTokenGroupActionWithTokenBalance( + sum_power, status, balance, + ), + )) + } else { + { + let recipient_id = + token_mint_transition.recipient_id(token_config)?; + let (root_hash, Some(balance)) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + recipient_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token mint)", recipient_id)))); + }; + Ok((root_hash, VerifiedTokenBalance(recipient_id, balance))) + } + } + } + TokenTransition::Transfer(token_transfer_transition) => { + if keeps_historical_document.keeps_transfer_history() { + historical_query() + } else { + { + let recipient_id = token_transfer_transition.recipient_id(); + let identity_ids = + [owner_id.to_buffer(), recipient_id.to_buffer()]; + let (root_hash, balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_token_balances_for_identity_ids( + proof, + token_id.into_buffer(), + &identity_ids, + false, + platform_version, + )?; + + let balances = balances.into_iter().map(|(id, maybe_balance)| { + let balance = maybe_balance.ok_or(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token transfer)", id))))?; + Ok((id, balance)) + }).collect::>()?; + + Ok((root_hash, VerifiedTokenIdentitiesBalances(balances))) + } + } + } + TokenTransition::Freeze(token_freeze_transition) => { + if keeps_historical_document.keeps_freezing_history() { + historical_query() + } else if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, identity_token_info) = + Drive::verify_token_info_for_identity_id( + proof, + token_id.into_buffer(), + token_freeze_transition + .frozen_identity_id() + .into_buffer(), + true, + platform_version, + )?; + if status == GroupActionStatus::ActionClosed + && identity_token_info.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token freeze)", owner_id)))); + }; + + Ok(( + root_hash, + VerifiedTokenGroupActionWithTokenIdentityInfo( + sum_power, + status, + identity_token_info, + ), + )) + } else { + let (root_hash, Some(identity_token_info)) = + Drive::verify_token_info_for_identity_id( + proof, + token_id.into_buffer(), + token_freeze_transition + .frozen_identity_id() + .into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token info for identity {} expected to exist because of state transition (token freeze)", token_freeze_transition.frozen_identity_id())))); + }; + if !identity_token_info.frozen() { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof contained token info saying this token was not frozen for identity {}", token_freeze_transition.frozen_identity_id())))); + } + Ok(( + root_hash, + VerifiedTokenIdentityInfo(owner_id, identity_token_info), + )) + } + } + TokenTransition::Unfreeze(token_unfreeze_transition) => { + if keeps_historical_document.keeps_freezing_history() { + historical_query() + } else if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, identity_token_info) = + Drive::verify_token_info_for_identity_id( + proof, + token_id.into_buffer(), + token_unfreeze_transition + .frozen_identity_id() + .into_buffer(), + true, + platform_version, + )?; + if status == GroupActionStatus::ActionClosed + && identity_token_info.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token unfreeze)", owner_id)))); + }; + + Ok(( + root_hash, + VerifiedTokenGroupActionWithTokenIdentityInfo( + sum_power, + status, + identity_token_info, + ), + )) + } else { + let (root_hash, Some(identity_token_info)) = + Drive::verify_token_info_for_identity_id( + proof, + token_id.into_buffer(), + token_unfreeze_transition + .frozen_identity_id() + .into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token info for identity {} expected to exist because of state transition (token freeze)", token_unfreeze_transition.frozen_identity_id())))); + }; + if identity_token_info.frozen() { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof contained token info saying this token was frozen for identity {} when we just unfroze it", token_unfreeze_transition.frozen_identity_id())))); + } + Ok(( + root_hash, + VerifiedTokenIdentityInfo(owner_id, identity_token_info), + )) + } + } + TokenTransition::DirectPurchase(_) => { + if keeps_historical_document.keeps_direct_purchase_history() { + historical_query() + } else { + let (root_hash, Some(balance)) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + owner_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token direct purchase)", owner_id)))); + }; + Ok((root_hash, VerifiedTokenBalance(owner_id, balance))) + } + } + TokenTransition::SetPriceForDirectPurchase(_) => { + if keeps_historical_document.keeps_direct_pricing_history() { + historical_query() + } else if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, token_pricing_schedule) = + Drive::verify_token_direct_selling_price( + proof, + token_id.into_buffer(), + true, + platform_version, + )?; + if status == GroupActionStatus::ActionClosed + && token_pricing_schedule.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token set price for direct purchase)", owner_id)))); + }; + + Ok(( + root_hash, + VerifiedTokenGroupActionWithTokenPricingSchedule( + sum_power, + status, + token_pricing_schedule, + ), + )) + } else { + let (root_hash, token_pricing_schedule) = + Drive::verify_token_direct_selling_price( + proof, + token_id.into_buffer(), + false, + platform_version, + )?; + Ok(( + root_hash, + VerifiedTokenPricingSchedule( + owner_id, + token_pricing_schedule, + ), + )) + } + } + TokenTransition::DestroyFrozenFunds(_) + | TokenTransition::EmergencyAction(_) + | TokenTransition::ConfigUpdate(_) + | TokenTransition::Claim(_) => historical_query(), + } + } + } + } + StateTransition::IdentityCreate(identity_create_transition) => { + // we expect to get an identity that matches the state transition + let (root_hash, identity) = Drive::verify_full_identity_by_identity_id( + proof, + false, + identity_create_transition.identity_id().into_buffer(), + platform_version, + )?; + let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain identity {} expected to exist because of state transition (create)", identity_create_transition.identity_id()))))?; + let expected_keys: BTreeMap = identity_create_transition + .public_keys() + .iter() + .map(|key| { + let stored_key: IdentityPublicKey = key.into(); + (stored_key.id(), stored_key) + }) + .collect(); + if expected_keys.len() != identity_create_transition.public_keys().len() { + return Err(Error::Proof(ProofError::InvalidTransition( + "identity create transition contains duplicate public key ids".to_string(), + ))); + } + if identity.public_keys() != &expected_keys { + return Err(Error::Proof(ProofError::IncorrectProof( + "identity create proof contains public keys that do not match the transition" + .to_string(), + ))); + } + Ok((root_hash, VerifiedIdentity(identity))) + } + StateTransition::IdentityTopUp(identity_top_up_transition) => { + // snapshot of the identity's balance and revision at the proof's block + let identity_id = identity_top_up_transition.identity_id(); + let (root_hash, Some((balance, revision))) = + Drive::verify_identity_balance_and_revision_for_identity_id( + proof, + identity_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain balance for identity {} expected to exist because of state transition (top up)", identity_id)))); + }; + Ok(( + root_hash, + VerifiedPartialIdentity(PartialIdentity { + id: *identity_top_up_transition.identity_id(), + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: Some(revision), + not_found_public_keys: Default::default(), + }), + )) + } + StateTransition::IdentityCreditWithdrawal(identity_credit_withdrawal_transition) => { + { + // snapshot of the identity's balance at the proof's block + let (root_hash, balance) = Drive::verify_identity_balance_for_identity_id( + proof, + identity_credit_withdrawal_transition + .identity_id() + .into_buffer(), + false, + platform_version, + )?; + let balance = balance.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity {} expected to exist because of state transition (withdrawal)", identity_credit_withdrawal_transition.identity_id()))))?; + Ok(( + root_hash, + VerifiedPartialIdentity(PartialIdentity { + id: identity_credit_withdrawal_transition.identity_id(), + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: None, + not_found_public_keys: Default::default(), + }), + )) + } + } + StateTransition::IdentityUpdate(identity_update_transition) => { + // we expect to get an identity that matches the state transition + let (root_hash, identity) = Drive::verify_identity_keys_by_identity_id( + proof, + IdentityKeysRequest::new_all_keys_query( + &identity_update_transition.identity_id().into_buffer(), + None, + ), + true, + false, + false, + platform_version, + )?; + let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain update for identity {} expected to exist because of state transition (update)", identity_update_transition.identity_id()))))?; + + if identity.revision != Some(identity_update_transition.revision()) { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "identity update proof contains revision {:?}, expected {}", + identity.revision, + identity_update_transition.revision() + )))); + } + + for key in identity_update_transition.public_keys_to_add() { + let expected_key: IdentityPublicKey = key.into(); + if identity.loaded_public_keys.get(&expected_key.id()) != Some(&expected_key) { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "identity update proof does not contain the expected state for added key {}", + expected_key.id() + )))); + } + } + + for key_id in identity_update_transition.public_key_ids_to_disable() { + let Some(proved_key) = identity.loaded_public_keys.get(key_id) else { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "identity update proof does not contain disabled key {}", + key_id + )))); + }; + // The proof may be generated at a block later than the one that + // executed the update; `disabled_at` never changes afterwards, so + // require the key to be disabled at or before the proof's block + // rather than exactly at it. + match proved_key.disabled_at() { + Some(disabled_at) if disabled_at <= block_info.time_ms => {} + _ => { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "identity update proof contains an unexpected disabled timestamp for key {}", + key_id + )))); + } + } + } + Ok((root_hash, VerifiedPartialIdentity(identity))) + } + StateTransition::IdentityCreditTransfer(identity_credit_transfer) => { + // snapshot of the sender's and recipient's balances at the proof's block + let (root_hash_identity, balance_identity) = + Drive::verify_identity_balance_for_identity_id( + proof, + identity_credit_transfer.identity_id().into_buffer(), + true, + platform_version, + )?; + + let (root_hash_recipient, balance_recipient) = + Drive::verify_identity_balance_for_identity_id( + proof, + identity_credit_transfer.recipient_id().into_buffer(), + true, + platform_version, + )?; + + if root_hash_identity != root_hash_recipient { + return Err(Error::Proof(ProofError::CorruptedProof("proof is expected to have same root hash for all subsets (identity transfer)".to_string()))); + } + + let balance_identity = balance_identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity sender {} expected to exist because of state transition (transfer)", identity_credit_transfer.identity_id()))))?; + let balance_recipient = balance_recipient.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity recipient {} expected to exist because of state transition (transfer)", identity_credit_transfer.recipient_id()))))?; + + Ok(( + root_hash_identity, + VerifiedBalanceTransfer( + PartialIdentity { + id: identity_credit_transfer.identity_id(), + loaded_public_keys: Default::default(), + balance: Some(balance_identity), + revision: None, + not_found_public_keys: Default::default(), + }, + PartialIdentity { + id: identity_credit_transfer.recipient_id(), + loaded_public_keys: Default::default(), + balance: Some(balance_recipient), + revision: None, + not_found_public_keys: Default::default(), + }, + ), + )) + } + StateTransition::MasternodeVote(masternode_vote) => { + let pro_tx_hash = masternode_vote.pro_tx_hash(); + let vote = masternode_vote.vote(); + let contract = match vote { + Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() { + VotePoll::ContestedDocumentResourceVotePoll( + contested_document_resource_vote_poll, + ) => known_contracts_provider_fn( + &contested_document_resource_vote_poll.contract_id, + )? + .ok_or(Error::Proof( + ProofError::UnknownContract(format!( + "unknown contract with id {} in resource vote verification", + contested_document_resource_vote_poll.contract_id + )), + ))?, + }, + }; + + // we expect to get a vote that matches the state transition + let (root_hash, vote) = Drive::verify_masternode_vote( + proof, + pro_tx_hash.to_buffer(), + vote, + &contract, + false, + platform_version, + )?; + let vote = vote.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain actual vote for masternode {} expected to exist because of state transition (masternode vote)", masternode_vote.pro_tx_hash()))))?; + Ok((root_hash, VerifiedMasternodeVote(vote))) + } + StateTransition::IdentityCreditTransferToAddresses(st) => { + let identity_id = st.identity_id(); + // verify_subset_of_proof=true because we verify identity balance/revision + // and address balances as separate subsets of the same merged proof + let (root_hash_identity, Some((balance, revision)), address_balances) = + Drive::verify_identity_balance_revision_and_addresses_from_inputs( + proof, + identity_id.to_buffer(), + st.recipient_addresses().keys(), + true, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain balance for identity {} expected to exist because of state transition (identity credit transfer to addresses)", identity_id) + ))); + }; + + Ok(( + root_hash_identity, + VerifiedIdentityWithAddressInfos( + PartialIdentity { + id: identity_id, + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: Some(revision), + not_found_public_keys: Default::default(), + }, + address_balances, + ), + )) + } + StateTransition::IdentityCreateFromAddresses(st) => { + use dpp::state_transition::StateTransitionIdentityIdFromInputs; + let identity_id = st.identity_id_from_inputs().map_err(|e| { + Error::Proof(ProofError::CorruptedProof(format!( + "Failed to calculate identity id from inputs: {}", + e + ))) + })?; + let (root_hash_identity, identity) = Drive::verify_full_identity_by_identity_id( + proof, + true, + identity_id.into_buffer(), + platform_version, + )?; + let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain identity {} expected to exist because of state transition (create from addresses)", identity_id))))?; + + let expected_keys: BTreeMap = st + .public_keys() + .iter() + .map(|key| { + let stored_key: IdentityPublicKey = key.into(); + (stored_key.id(), stored_key) + }) + .collect(); + if expected_keys.len() != st.public_keys().len() { + return Err(Error::Proof(ProofError::InvalidTransition( + "identity create from addresses transition contains duplicate public key ids" + .to_string(), + ))); + } + if identity.public_keys() != &expected_keys { + return Err(Error::Proof(ProofError::IncorrectProof( + "identity create from addresses proof contains public keys that do not match the transition" + .to_string(), + ))); + } + + let addresses_to_check = st + .inputs() + .keys() + .chain(st.output().into_iter().map(|(address, _)| address)); + + let (root_hash_addresses, address_balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_addresses_infos( + proof, + addresses_to_check, + true, + platform_version, + )?; + + if root_hash_identity != root_hash_addresses { + return Err(Error::Proof(ProofError::CorruptedProof( + "proof is expected to have same root hash for identity and address infos" + .to_string(), + ))); + } + + Ok(( + root_hash_identity, + VerifiedIdentityFullWithAddressInfos(identity, address_balances), + )) + } + StateTransition::IdentityTopUpFromAddresses(st) => { + // Verify revision and balance for the identity + use dpp::state_transition::identity_topup_from_addresses_transition::accessors::IdentityTopUpFromAddressesTransitionAccessorsV0; + let identity_id = st.identity_id(); + let addresses_to_check = st + .inputs() + .keys() + .chain(st.output().into_iter().map(|(address, _)| address)); + // verify_subset_of_proof=true because we verify identity balance/revision + // and address balances as separate subsets of the same merged proof + let (root_hash_identity, Some((balance, revision)), address_balances) = + Drive::verify_identity_balance_revision_and_addresses_from_inputs( + proof, + identity_id.to_buffer(), + addresses_to_check, + true, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain balance for identity {} expected to exist because of state transition (top up from addresses)", identity_id)))); + }; + + Ok(( + root_hash_identity, + VerifiedIdentityWithAddressInfos( + PartialIdentity { + id: *identity_id, + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: Some(revision), + not_found_public_keys: Default::default(), + }, + address_balances, + ), + )) + } + StateTransition::AddressFundsTransfer(st) => { + // snapshot of the input and output addresses at the proof's block + use dpp::state_transition::address_funds_transfer_transition::accessors::AddressFundsTransferTransitionAccessorsV0; + use dpp::state_transition::StateTransitionWitnessSigned; + let (root_hash, address_balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_addresses_infos( + proof, + st.inputs().keys().chain(st.outputs().keys()), + false, + platform_version, + )?; + + Ok((root_hash, VerifiedAddressInfos(address_balances))) + } + StateTransition::AddressFundingFromAssetLock(st) => { + // snapshot of the input and output addresses at the proof's block + use dpp::state_transition::address_funding_from_asset_lock_transition::accessors::AddressFundingFromAssetLockTransitionAccessorsV0; + let (root_hash, balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_addresses_infos( + proof, + st.inputs().keys().chain(st.outputs().keys()), + false, + platform_version, + )?; + + Ok((root_hash, VerifiedAddressInfos(balances))) + } + StateTransition::AddressCreditWithdrawal(st) => { + // snapshot of the input and output addresses at the proof's block + use dpp::state_transition::StateTransitionWitnessSigned; + let addresses_to_check = st + .inputs() + .keys() + .chain(st.output().into_iter().map(|(address, _)| address)); + let (root_hash, balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_addresses_infos( + proof, + addresses_to_check, + false, + platform_version, + )?; + + Ok((root_hash, VerifiedAddressInfos(balances))) + } + StateTransition::Shield(st) => { + // snapshot of the input addresses at the proof's block + use dpp::state_transition::StateTransitionWitnessSigned; + let (root_hash, balances): ( + RootHash, + BTreeMap>, + ) = Drive::verify_addresses_infos( + proof, + st.inputs().keys(), + false, + platform_version, + )?; + Ok((root_hash, VerifiedAddressInfos(balances))) + } + StateTransition::Unshield(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos; + use dpp::state_transition::unshield_transition::accessors::UnshieldTransitionAccessorsV0; + use grovedb::Element; + + let nullifier_keys: Vec> = st.nullifiers(); + + // Reconstruct the prove side's merged query — nullifier spend-status ∪ + // output-address balance — and verify it strictly. See + // `verify_merged_query_strict` for why a single strict merged verify is + // sound and rejects proofs padded with extra subtree branches. + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys); + let nullifier_pq = grovedb::PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let address_pq = + Drive::balances_for_clear_addresses_query(std::iter::once(st.output_address())); + + let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( + proof, + vec![nullifier_pq, address_pq], + platform_version, + )?; + + // Partition the proved key/values by path: entries under the nullifiers tree are + // spend statuses, entries under the clear-address pool are address balances. + let nullifiers_path = shielded_credit_pool_nullifiers_path_vec(); + let addresses_path = Drive::clear_addresses_path(); + + let mut statuses: Vec<(Vec, bool)> = Vec::new(); + let mut balances: BTreeMap> = + BTreeMap::new(); + + for (path, key, element) in proved_key_values { + if path == nullifiers_path { + // A present element means the nullifier is spent; absence means unspent. + statuses.push((key, element.is_some())); + } else if path == addresses_path { + // Mirror `verify_addresses_infos_v0`: reconstruct the address from the key + // and decode the `ItemWithSumItem` (nonce, balance) element. + let address = PlatformAddress::from_bytes(&key).map_err(|e| { + Error::Proof(ProofError::CorruptedProof(format!( + "failed to deserialize output PlatformAddress: {}", + e + ))) + })?; + + let balance_info = element + .map(|element| { + let Element::ItemWithSumItem(nonce_vec, balance_i64, _) = element + else { + return Err(Error::Proof(ProofError::CorruptedProof( + "expected an item with sum item element".to_string(), + ))); + }; + + let nonce_bytes: [u8; 4] = nonce_vec.try_into().map_err(|_| { + Error::Proof(ProofError::IncorrectValueSize( + "nonce should be 4 bytes", + )) + })?; + let nonce = AddressNonce::from_be_bytes(nonce_bytes); + + if balance_i64 < 0 { + return Err(Error::Proof(ProofError::CorruptedProof( + "balance cannot be negative".to_string(), + ))); + } + + Ok((nonce, balance_i64 as Credits)) + }) + .transpose()?; + + // Mirror ShieldedWithdrawal's singleton-subtree invariant: the + // address sub-query targets exactly one key, so a second entry under + // the clear-address pool is a malformed proof, not last-write-wins. + if balances.contains_key(&address) { + return Err(Error::Proof(ProofError::CorruptedProof( + "unshield proof contained more than one output-address entry" + .to_string(), + ))); + } + balances.insert(address, balance_info); + } else { + return Err(Error::Proof(ProofError::CorruptedProof( + "unshield proof contained an entry outside the nullifier and address subtrees".to_string(), + ))); + } + } + + // Every nullifier must be present and marked spent. + for (nf, is_spent) in &statuses { + if !is_spent { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "nullifier {} was not found as spent in the unshield proof", + hex::encode(nf) + )))); + } + } + + Ok(( + root_hash, + VerifiedShieldedNullifiersWithAddressInfos(statuses, balances), + )) + } + StateTransition::ShieldedTransfer(st) => { + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiers; + use dpp::state_transition::shielded_transfer_transition::accessors::ShieldedTransferTransitionAccessorsV0; + + let nullifier_keys: Vec> = st.nullifiers(); + + let (root_hash, statuses) = Drive::verify_shielded_nullifiers( + proof, + &nullifier_keys, + false, + platform_version, + )?; + + // All nullifiers must be marked as spent + for (nf, is_spent) in &statuses { + if !is_spent { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "nullifier {} was not found as spent in the proof", + hex::encode(nf) + )))); + } + } + + Ok((root_hash, VerifiedShieldedNullifiers(statuses))) + } + StateTransition::ShieldedWithdrawal(st) => { + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::data_contracts::withdrawals_contract; + use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; + use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; + use dpp::document::Document; + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiersWithWithdrawalDocument; + use dpp::state_transition::shielded_withdrawal_transition::accessors::ShieldedWithdrawalTransitionAccessorsV0; + use grovedb::Element; + + let nullifier_keys: Vec> = st.nullifiers(); + + // Compute withdrawal document ID deterministically (same as prove side). + let first_nullifier = nullifier_keys.first().ok_or_else(|| { + Error::Proof(ProofError::InvalidTransition( + "shielded withdrawal has no nullifiers".to_string(), + )) + })?; + let mut entropy = Vec::new(); + entropy.extend_from_slice(first_nullifier); + entropy.extend_from_slice(st.output_script().as_bytes()); + let document_id = Document::generate_document_id_v0( + &withdrawals_contract::ID, + &withdrawals_contract::OWNER_ID, + withdrawal::NAME, + &entropy, + ); + + let contract = + known_contracts_provider_fn(&withdrawals_contract::ID)?.ok_or_else(|| { + Error::Proof(ProofError::UnknownContract( + "withdrawals contract not available for shielded withdrawal verification" + .to_string(), + )) + })?; + let document_type = + contract + .document_type_for_name(withdrawal::NAME) + .map_err(|e| { + Error::Proof(ProofError::UnknownContract(format!( + "cannot fetch withdrawal document type: {}", + e + ))) + })?; + + let doc_query = SingleDocumentDriveQuery { + contract_id: withdrawals_contract::ID.to_buffer(), + document_type_name: withdrawal::NAME.to_string(), + document_type_keeps_history: false, + document_id: document_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + }; + + // Reconstruct the prove side's merged query — nullifier spend-status ∪ + // withdrawal document — and verify it strictly. See + // `verify_merged_query_strict` for why a single strict merged verify is + // sound and rejects proofs padded with extra subtree branches. + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys); + let nullifier_pq = grovedb::PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let doc_pq = doc_query.construct_path_query(platform_version)?; + let document_path = doc_pq.path.clone(); + + let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( + proof, + vec![nullifier_pq, doc_pq], + platform_version, + )?; + + // Partition the proved key/values by path: entries under the nullifiers tree are + // spend statuses; the single entry under the withdrawal-document tree is the + // proven document. + let nullifiers_path = shielded_credit_pool_nullifiers_path_vec(); + + let mut statuses: Vec<(Vec, bool)> = Vec::new(); + let mut document_element: Option> = None; + + for (path, key, element) in proved_key_values { + if path == nullifiers_path { + statuses.push((key, element.is_some())); + } else if path == document_path { + if document_element.is_some() { + return Err(Error::Proof(ProofError::CorruptedProof( + "shielded withdrawal proof contained more than one withdrawal document".to_string(), + ))); + } + document_element = Some(element); + } else { + return Err(Error::Proof(ProofError::CorruptedProof( + "shielded withdrawal proof contained an entry outside the nullifier and document subtrees".to_string(), + ))); + } + } + + // Every nullifier must be present and marked spent. + for (nf, is_spent) in &statuses { + if !is_spent { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "nullifier {} was not found as spent in the shielded withdrawal proof", + hex::encode(nf) + )))); + } + } + + let document_element = document_element.ok_or_else(|| { + Error::Proof(ProofError::CorruptedProof( + "shielded withdrawal document key absent from proof".to_string(), + )) + })?; + + let doc = match document_element { + Some(Element::Item(serialized, _)) => Document::from_bytes( + serialized.as_slice(), + document_type, + platform_version, + )?, + Some(_) => { + return Err(Error::Proof(ProofError::CorruptedProof( + "expected an item element for withdrawal document".to_string(), + ))); + } + None => { + return Err(Error::Proof(ProofError::CorruptedProof( + "shielded withdrawal was executed but withdrawal document is missing from proof".to_string(), + ))); + } + }; + let documents = BTreeMap::from([(document_id, Some(doc))]); + + Ok(( + root_hash, + VerifiedShieldedNullifiersWithWithdrawalDocument(statuses, documents), + )) + } + StateTransition::ShieldFromAssetLock(st) => { + use crate::drive::RootTree; + use dpp::asset_lock::reduced_asset_lock_value::AssetLockValue; + use dpp::asset_lock::StoredAssetLockInfo; + use dpp::identity::state_transition::AssetLockProved; + use dpp::serialization::PlatformDeserializableUntrusted; + use dpp::state_transition::proof_result::StateTransitionProofResult::{ + VerifiedAssetLockConsumed, VerifiedAssetLockConsumedWithAddressInfos, + }; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use grovedb::Element; + + let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { + Error::Proof(ProofError::InvalidTransition( + "shield from asset lock has no outpoint".to_string(), + )) + })?; + let outpoint_bytes: [u8; 36] = outpoint.into(); + + // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. + let ShieldFromAssetLockTransition::V0(v0) = st; + let surplus_output = &v0.surplus_output; + + // Build the outpoint sub-query exactly as the prove side does (same path, same key). + let mut outpoint_query = grovedb::Query::new(); + outpoint_query.insert_key(outpoint_bytes.to_vec()); + let outpoint_pq = grovedb::PathQuery::new( + vec![vec![RootTree::SpentAssetLockTransactions as u8]], + grovedb::SizedQuery::new(outpoint_query, Some(1), None), + ); + + match surplus_output { + Some(surplus_address) => { + // Reconstruct the prove side's merged query — asset-lock outpoint ∪ + // surplus-address balance — and verify it strictly. See + // `verify_merged_query_strict` for why a single strict merged verify + // is sound and rejects proofs padded with extra subtree branches. + let address_pq = Drive::balances_for_clear_addresses_query( + std::iter::once(surplus_address), + ); + + let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( + proof, + vec![outpoint_pq, address_pq], + platform_version, + )?; + + // Partition the proved key/values: exactly one entry is the asset-lock + // outpoint (36-byte key), every other entry is a surplus-address balance + // (21-byte key). Anything else is a corrupted proof. + let outpoint_key = outpoint_bytes.to_vec(); + let mut outpoint_element: Option> = None; + let mut balances: BTreeMap< + PlatformAddress, + Option<(AddressNonce, Credits)>, + > = BTreeMap::new(); + + for (_path, key, element) in proved_key_values { + if key == outpoint_key { + outpoint_element = Some(element); + continue; + } + + // Mirror `verify_addresses_infos_v0`: reconstruct the address from the + // key and decode the `ItemWithSumItem` (nonce, balance) element. + let address = PlatformAddress::from_bytes(&key).map_err(|e| { + Error::Proof(ProofError::CorruptedProof(format!( + "failed to deserialize surplus PlatformAddress: {}", + e + ))) + })?; + + let balance_info = element + .map(|element| { + let Element::ItemWithSumItem(nonce_vec, balance_i64, _) = + element + else { + return Err(Error::Proof(ProofError::CorruptedProof( + "expected an item with sum item element".to_string(), + ))); + }; + + let nonce_bytes: [u8; 4] = + nonce_vec.try_into().map_err(|_| { + Error::Proof(ProofError::IncorrectValueSize( + "nonce should be 4 bytes", + )) + })?; + let nonce = AddressNonce::from_be_bytes(nonce_bytes); + + if balance_i64 < 0 { + return Err(Error::Proof(ProofError::CorruptedProof( + "balance cannot be negative".to_string(), + ))); + } + let balance = balance_i64 as Credits; + + Ok((nonce, balance)) + }) + .transpose()?; + + balances.insert(address, balance_info); + } + + // The asset-lock outpoint MUST be present in the merged proof. + let outpoint_element = outpoint_element.ok_or_else(|| { + Error::Proof(ProofError::CorruptedProof( + "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), + )) + })?; + + let info = match outpoint_element { + Some(Element::Item(bytes, _)) => { + if bytes.is_empty() { + StoredAssetLockInfo::FullyConsumed + } else { + StoredAssetLockInfo::PartiallyConsumed( + AssetLockValue::deserialize_from_bytes_untrusted(&bytes)?, + ) + } + } + Some(_) => { + return Err(Error::Proof(ProofError::CorruptedProof( + "expected an item element for asset lock outpoint".to_string(), + ))); + } + None => { + return Err(Error::Proof(ProofError::CorruptedProof( + "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), + ))); + } + }; + + Ok(( + root_hash, + VerifiedAssetLockConsumedWithAddressInfos(info, balances), + )) + } + None => { + // No surplus output: the proof covers only the outpoint, verified strictly + // with the `Some(1)` limit exactly as before (unchanged behavior). + let (root_hash, mut proved_key_values) = + grovedb::GroveDb::verify_query_with_absence_proof( + proof, + &outpoint_pq, + &platform_version.drive.grove_version, + )?; + + if proved_key_values.len() > 1 { + return Err(Error::Proof(ProofError::TooManyElements( + "expected at most 1 element for asset lock outpoint", + ))); + } + + let info = if let Some(proved) = proved_key_values.pop() { + match proved.2 { + Some(Element::Item(bytes, _)) => { + if bytes.is_empty() { + StoredAssetLockInfo::FullyConsumed + } else { + StoredAssetLockInfo::PartiallyConsumed( + AssetLockValue::deserialize_from_bytes_untrusted( + &bytes, + )?, + ) + } + } + Some(_) => { + return Err(Error::Proof(ProofError::CorruptedProof( + "expected an item element for asset lock outpoint" + .to_string(), + ))); + } + None => { + return Err(Error::Proof(ProofError::CorruptedProof( + "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), + ))); + } + } + } else { + return Err(Error::Proof(ProofError::CorruptedProof( + "shield from asset lock was executed but no proved key values returned" + .to_string(), + ))); + }; + + Ok((root_hash, VerifiedAssetLockConsumed(info))) + } + } + } + StateTransition::IdentityCreateFromShieldedPool(st) => { + use crate::drive::balances::balance_path; + use crate::drive::identity::IdentityRootStructure::IdentityTreeRevision; + use crate::drive::identity::{identity_key_tree_path, identity_path}; + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{IdentityPublicKey, IdentityV0, KeyID}; + use dpp::prelude::Revision; + use dpp::serialization::PlatformDeserializableUntrusted; + use dpp::state_transition::identity_create_from_shielded_pool_transition::accessors::IdentityCreateFromShieldedPoolTransitionAccessorsV0; + use dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions; + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers; + use std::collections::{BTreeMap, BTreeSet}; + + // Recompute the id from the actions (the canonical value) instead of trusting the + // wire field, and reject a tampered transition whose wire id doesn't match: so a + // client verifying a proof cannot be fed a transition that reuses these nullifiers + // while pointing `identity_id` at a different identity. (Consensus enforces the same + // equality in `validate_structure`; this independently re-checks it here so the + // SDK proof path is sound even on a hand-constructed transition object.) + let derived_id = derive_identity_id_from_actions(st.actions()); + if st.identity_id() != derived_id { + return Err(Error::Proof(ProofError::IncorrectProof( + "identity create from shielded pool: identity_id does not match the value derived from the spend nullifiers".to_string(), + ))); + } + let identity_id = derived_id.to_buffer(); + let nullifier_keys: Vec> = st.nullifiers(); + + // Rebuild the BYTE-IDENTICAL merged query the prove side built: the nullifier + // sub-query over the nullifier tree + the full-identity sub-query, each with its + // limit cleared (PathQuery::merge rejects limited sub-queries). + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys.clone()); + let nullifier_pq = grovedb::PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let mut identity_pq = Drive::full_identity_query( + &identity_id, + &platform_version.drive.grove_version, + )?; + identity_pq.query.limit = None; + + let merged_pq = grovedb::PathQuery::merge( + vec![&nullifier_pq, &identity_pq], + &platform_version.drive.grove_version, + )?; + + // STRICT verification via `verify_query` (succinctness on). Unlike the other + // shielded merged queries (which target only explicit keys and go through + // `verify_merged_query_strict`), this one embeds `full_identity_query`, whose + // all-keys sub-query is an unbounded RangeFull: and + // `verify_query_with_absence_proof` enumerates the query's terminal keys, which + // is impossible for unbounded ranges ("terminal keys are not supported with + // unbounded ranges"). Absence synthesis isn't needed here anyway: every queried + // element (the spent nullifiers and the created identity) must be PRESENT, so + // presence is checked directly against the result set below. The succinctness + // check still rejects proofs padded with branches beyond {nullifiers, identity} + // (the strict-from-day-one guarantee of #3812), and the limit stays None exactly + // as the prove side built it, so no layer's result loop can break early. + let (root_hash, proved_key_values) = grovedb::GroveDb::verify_query( + proof, + &merged_pq, + &platform_version.drive.grove_version, + )?; + + // Partition the proved key/values by PATH (NOT key length: nullifier keys and the + // identity id are both 32 bytes): nullifier-tree entries vs the identity subtrees + // (balance / revision / keys). Reconstruct the identity exactly as + // `verify_full_identity_by_identity_id_v0` does. + let nullifier_path = shielded_credit_pool_nullifiers_path_vec(); + let balance_path = balance_path(); + let identity_path = identity_path(identity_id.as_slice()); + let identity_keys_path = identity_key_tree_path(identity_id.as_slice()); + + let mut spent_nullifiers = BTreeSet::>::new(); + let mut balance: Option = None; + let mut revision: Option = None; + let mut keys = BTreeMap::::new(); + + for (path, key, maybe_element) in proved_key_values { + if path == nullifier_path { + if !nullifier_keys.contains(&key) { + return Err(Error::Proof(ProofError::CorruptedProof( + "identity create from shielded pool proof contains a nullifier \ + entry that was not requested" + .to_string(), + ))); + } + if maybe_element.is_some() { + spent_nullifiers.insert(key); + } + } else if path == balance_path && key == identity_id { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::IncompleteProof( + "balance wasn't provided for the created identity", + )) + })?; + let signed_balance = element.as_sum_item_value().map_err(Error::from)?; + if signed_balance < 0 { + return Err(Error::Proof(ProofError::Overflow( + "balance can't be negative", + ))); + } + balance = Some(signed_balance as Credits); + } else if path == identity_path && key == vec![IdentityTreeRevision as u8] { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::IncompleteProof( + "revision wasn't provided for the created identity", + )) + })?; + let item_bytes = element.into_item_bytes().map_err(Error::from)?; + revision = Some(Revision::from_be_bytes(item_bytes.try_into().map_err( + |_| { + Error::Proof(ProofError::IncorrectValueSize( + "revision should be 8 bytes", + )) + }, + )?)); + } else if path == identity_keys_path { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::CorruptedProof( + "received an absence proof for a key but didn't request one" + .to_string(), + )) + })?; + let item_bytes = element.into_item_bytes().map_err(Error::from)?; + let public_key = + IdentityPublicKey::deserialize_from_bytes_untrusted(&item_bytes)?; + keys.insert(public_key.id(), public_key); + } else { + return Err(Error::Proof(ProofError::TooManyElements( + "identity create from shielded pool proof contains an element outside \ + the nullifier tree and the created identity", + ))); + } + } + + // Without absence synthesis an unspent nullifier yields no result entry (or a + // bare absence entry), so each expected nullifier's spend status is its + // membership in the proved-present set. + let statuses: Vec<(Vec, bool)> = nullifier_keys + .iter() + .map(|nf| (nf.clone(), spent_nullifiers.contains(nf))) + .collect(); + + // Every funding nullifier must be present (spent) in the post-execution state. + for (nf, is_spent) in &statuses { + if !is_spent { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "nullifier {} was not found as spent in the identity-create-from-shielded-pool proof", + hex::encode(nf) + )))); + } + } + + // The created identity MUST be fully present. + let (balance, revision) = match (balance, revision, keys.is_empty()) { + (Some(balance), Some(revision), false) => (balance, revision), + _ => { + return Err(Error::Proof(ProofError::IncompleteProof( + "identity create from shielded pool was executed but the created identity is absent or incomplete in the proof", + ))) + } + }; + + // Bind the proof to the transition's declared key set: the proven identity must hold + // EXACTLY the keys the transition created (the same conversion the action transformer + // used to build the identity). This stops a tampered transition from swapping in a + // different key set while reusing a valid {nullifiers, identity} proof. + // + // The balance is deliberately NOT checked against `denomination`: the identity holds + // `denomination - total_fee`, and `total_fee` is metered at execution and not + // recoverable here, so a balance/denomination equality check would reject every + // honest proof. (`denomination` is bound into the Orchard `extra_sighash_data` at + // consensus, which is where that binding is enforced.) + let expected_keys: BTreeMap = st + .public_keys() + .iter() + .map(|key| { + let public_key: IdentityPublicKey = key.into(); + (public_key.id(), public_key) + }) + .collect(); + if keys != expected_keys { + return Err(Error::Proof(ProofError::IncorrectProof( + "identity create from shielded pool: the proven identity's keys do not match the transition's declared public keys".to_string(), + ))); + } + + let identity: dpp::prelude::Identity = IdentityV0 { + id: Identifier::from(identity_id), + public_keys: keys, + balance, + revision, + } + .into(); + + Ok(( + root_hash, + VerifiedIdentityWithShieldedNullifiers(identity, statuses), + )) + } + StateTransition::IdentityTopUpFromShieldedPool(st) => { + use crate::drive::balances::balance_path; + use crate::drive::identity::IdentityRootStructure::IdentityTreeRevision; + use crate::drive::identity::{identity_key_tree_path, identity_path}; + use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::identity::{IdentityPublicKey, IdentityV0, KeyID}; + use dpp::prelude::Revision; + use dpp::serialization::PlatformDeserializableUntrusted; + use dpp::state_transition::identity_top_up_from_shielded_pool_transition::accessors::IdentityTopUpFromShieldedPoolTransitionAccessorsV0; + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers; + use std::collections::{BTreeMap, BTreeSet}; + + // The credited identity is the transition's declared `identity_id`; consensus + // binds it into the Orchard sighash, so a proof for these nullifiers can only + // have been produced for a transition crediting this identity. + let identity_id = st.identity_id().to_buffer(); + let nullifier_keys: Vec> = st.nullifiers(); + + // Rebuild the BYTE-IDENTICAL merged query the prove side built: the nullifier + // sub-query over the nullifier tree + the full-identity sub-query, each with its + // limit cleared (PathQuery::merge rejects limited sub-queries). + let mut nf_query = grovedb::Query::new(); + nf_query.insert_keys(nullifier_keys.clone()); + let nullifier_pq = grovedb::PathQuery::new( + shielded_credit_pool_nullifiers_path_vec(), + grovedb::SizedQuery::new(nf_query, None, None), + ); + + let mut identity_pq = Drive::full_identity_query( + &identity_id, + &platform_version.drive.grove_version, + )?; + identity_pq.query.limit = None; + + let merged_pq = grovedb::PathQuery::merge( + vec![&nullifier_pq, &identity_pq], + &platform_version.drive.grove_version, + )?; + + // STRICT verification via `verify_query` (succinctness on). Unlike the other + // shielded merged queries (which target only explicit keys and go through + // `verify_merged_query_strict`), this one embeds `full_identity_query`, whose + // all-keys sub-query is an unbounded RangeFull: and + // `verify_query_with_absence_proof` enumerates the query's terminal keys, which + // is impossible for unbounded ranges ("terminal keys are not supported with + // unbounded ranges"). Absence synthesis isn't needed here anyway: every queried + // element (the spent nullifiers and the credited identity) must be PRESENT, so + // presence is checked directly against the result set below. The succinctness + // check still rejects proofs padded with branches beyond {nullifiers, identity} + // (the strict-from-day-one guarantee of #3812), and the limit stays None exactly + // as the prove side built it, so no layer's result loop can break early. + let (root_hash, proved_key_values) = grovedb::GroveDb::verify_query( + proof, + &merged_pq, + &platform_version.drive.grove_version, + )?; + + // Partition the proved key/values by PATH (NOT key length: nullifier keys and the + // identity id are both 32 bytes): nullifier-tree entries vs the identity subtrees + // (balance / revision / keys). Reconstruct the identity exactly as + // `verify_full_identity_by_identity_id_v0` does. + let nullifier_path = shielded_credit_pool_nullifiers_path_vec(); + let balance_path = balance_path(); + let identity_path = identity_path(identity_id.as_slice()); + let identity_keys_path = identity_key_tree_path(identity_id.as_slice()); + + let mut spent_nullifiers = BTreeSet::>::new(); + let mut balance: Option = None; + let mut revision: Option = None; + let mut keys = BTreeMap::::new(); + + for (path, key, maybe_element) in proved_key_values { + if path == nullifier_path { + if !nullifier_keys.contains(&key) { + return Err(Error::Proof(ProofError::CorruptedProof( + "identity top up from shielded pool proof contains a nullifier \ + entry that was not requested" + .to_string(), + ))); + } + if maybe_element.is_some() { + spent_nullifiers.insert(key); + } + } else if path == balance_path && key == identity_id { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::IncompleteProof( + "balance wasn't provided for the topped-up identity", + )) + })?; + let signed_balance = element.as_sum_item_value().map_err(Error::from)?; + if signed_balance < 0 { + return Err(Error::Proof(ProofError::Overflow( + "balance can't be negative", + ))); + } + balance = Some(signed_balance as Credits); + } else if path == identity_path && key == vec![IdentityTreeRevision as u8] { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::IncompleteProof( + "revision wasn't provided for the topped-up identity", + )) + })?; + let item_bytes = element.into_item_bytes().map_err(Error::from)?; + revision = Some(Revision::from_be_bytes(item_bytes.try_into().map_err( + |_| { + Error::Proof(ProofError::IncorrectValueSize( + "revision should be 8 bytes", + )) + }, + )?)); + } else if path == identity_keys_path { + let element = maybe_element.ok_or_else(|| { + Error::Proof(ProofError::CorruptedProof( + "received an absence proof for a key but didn't request one" + .to_string(), + )) + })?; + let item_bytes = element.into_item_bytes().map_err(Error::from)?; + let public_key = + IdentityPublicKey::deserialize_from_bytes_untrusted(&item_bytes)?; + keys.insert(public_key.id(), public_key); + } else { + return Err(Error::Proof(ProofError::TooManyElements( + "identity top up from shielded pool proof contains an element outside \ + the nullifier tree and the topped-up identity", + ))); + } + } + + // Without absence synthesis an unspent nullifier yields no result entry (or a + // bare absence entry), so each expected nullifier's spend status is its + // membership in the proved-present set. + let statuses: Vec<(Vec, bool)> = nullifier_keys + .iter() + .map(|nf| (nf.clone(), spent_nullifiers.contains(nf))) + .collect(); + + // Every funding nullifier must be present (spent) in the post-execution state. + for (nf, is_spent) in &statuses { + if !is_spent { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "nullifier {} was not found as spent in the identity-top-up-from-shielded-pool proof", + hex::encode(nf) + )))); + } + } + + // The credited identity MUST be fully present (it existed before the top-up). + let (balance, revision) = match (balance, revision, keys.is_empty()) { + (Some(balance), Some(revision), false) => (balance, revision), + _ => { + return Err(Error::Proof(ProofError::IncompleteProof( + "identity top up from shielded pool was executed but the topped-up identity is absent or incomplete in the proof", + ))) + } + }; + + // The balance is deliberately NOT checked against `top_up_amount`: the proof is a + // post-execution snapshot of an identity that already held credits, so the + // pre-top-up balance and the flat fee are not recoverable here. (`top_up_amount` + // is bound into the Orchard `extra_sighash_data` at consensus.) + let identity: dpp::prelude::Identity = IdentityV0 { + id: Identifier::from(identity_id), + public_keys: keys, + balance, + revision, + } + .into(); + + Ok(( + root_hash, + VerifiedIdentityWithShieldedNullifiers(identity, statuses), + )) + } + StateTransition::ShieldFromIdentity(st) => { + use dpp::state_transition::shield_from_identity_transition::accessors::ShieldFromIdentityTransitionAccessorsV0; + // snapshot of the identity's balance at the proof's block + let identity_id = st.identity_id(); + let (root_hash, balance) = Drive::verify_identity_balance_for_identity_id( + proof, + identity_id.into_buffer(), + false, + platform_version, + )?; + let balance = balance.ok_or(Error::Proof(ProofError::IncorrectProof(format!( + "proof did not contain balance for identity {} expected to exist because of state transition (shield from identity)", + identity_id + ))))?; + Ok(( + root_hash, + VerifiedPartialIdentity(PartialIdentity { + id: identity_id, + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: None, + not_found_public_keys: Default::default(), + }), + )) + } + }?; + + let outcome = if Self::state_transition_proof_binds_execution_v1( + state_transition, + known_contracts_provider_fn, + )? { + StateTransitionProofOutcome::ExecutionProved(result) + } else { + StateTransitionProofOutcome::AffectedState(result) + }; + + Ok((root_hash, outcome)) + } + + /// Whether a valid proof for this state transition binds the execution of + /// this specific transition (`ExecutionProved`) or can only authenticate + /// the state the transition affects at the committed block + /// (`AffectedState`). + /// + /// This classifier is deliberately an exhaustive `match` over every + /// transition family — and, for tokens, over the contract's keeps-history + /// configuration and group usage — so adding a new variant fails to + /// compile until it is classified here: no transition can silently + /// inherit the stronger `ExecutionProved` guarantee. + fn state_transition_proof_binds_execution_v1( + state_transition: &StateTransition, + known_contracts_provider_fn: &ContractLookupFn, + ) -> Result { + let binds = match state_transition { + // The created contract's id derives from its owner and entropy, + // and the proven body must equal the transition's declared + // contract, which cannot exist without this create executing. + StateTransition::DataContractCreate(_) => true, + // The proven contract body equaling the update's target does not + // prove that THIS update executed: the proven version may predate + // the request, whose signature and nonce are not in the proof. + StateTransition::DataContractUpdate(_) => false, + StateTransition::Batch(batch) => match batch.first_transition_v1() { + // The verifier errors on an empty batch before the tag is + // consulted; classify fail-closed regardless. + None => false, + // Document proofs bind the exact document (or its absence + // after deletion), including contested status and history — + // EXCEPT indexOnly types: their snapshot authenticates the + // commitment-checked entry (or its absence), which carries + // neither id, entropy nor nonce and so cannot bind one + // specific transition's execution. + Some(BatchedTransitionRefV1::Document(document_transition)) => { + use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; + let data_contract_id = document_transition.data_contract_id(); + let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( + Error::Proof(ProofError::UnknownContract(format!( + "unknown contract with id {} in document verification", + data_contract_id + ))), + )?; + // EXCEPT an erase: the document it acts on was already + // deleted, so the by-id proof shows the same absence + // whether or not this erase ran. + if matches!(document_transition, DocumentTransitionRefV1::Erase(_)) { + false + } else { + !contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? + .index_only() + } + } + Some(BatchedTransitionRefV1::Token(token_transition)) => { + let data_contract_id = token_transition.data_contract_id(); + let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( + Error::Proof(ProofError::UnknownContract(format!( + "unknown contract with id {} in token verification", + data_contract_id + ))), + )?; + let token_config = contract.expected_token_configuration( + token_transition.base().token_contract_position(), + )?; + let keeps = token_config.keeps_history(); + // A group action's proof binds the action id and the + // signer's recorded participation; a history-keeping + // token's proof binds the exact historical document. + // Without either, only the resulting state is proven. + let grouped = token_transition.base().using_group_info().is_some(); + match token_transition { + TokenTransition::Burn(_) => keeps.keeps_burning_history() || grouped, + TokenTransition::Mint(_) => keeps.keeps_minting_history() || grouped, + TokenTransition::Transfer(_) => keeps.keeps_transfer_history(), + TokenTransition::Freeze(_) | TokenTransition::Unfreeze(_) => { + keeps.keeps_freezing_history() || grouped + } + TokenTransition::DirectPurchase(_) => keeps.keeps_direct_purchase_history(), + TokenTransition::SetPriceForDirectPurchase(_) => { + keeps.keeps_direct_pricing_history() || grouped + } + TokenTransition::DestroyFrozenFunds(_) + | TokenTransition::EmergencyAction(_) + | TokenTransition::ConfigUpdate(_) + | TokenTransition::Claim(_) => true, + } + } + }, + // The proven identity holds exactly the transition's declared + // key set, which cannot exist without this create executing. + StateTransition::IdentityCreate(_) => true, + // Balance/revision snapshots: values as of the proof's block, + // not bindable to one specific top-up/withdrawal/transfer. + StateTransition::IdentityTopUp(_) => false, + StateTransition::IdentityCreditWithdrawal(_) => false, + StateTransition::IdentityCreditTransfer(_) => false, + // Binds the transition's revision and its exact key additions + // and disabling timestamps. + StateTransition::IdentityUpdate(_) => true, + // The proven vote is stored under the masternode's identity and + // must equal the transition's declared vote. + StateTransition::MasternodeVote(_) => true, + // Current identity and address balances, without the requested + // amounts, nonces, or the submitted request itself. + StateTransition::IdentityCreditTransferToAddresses(_) => false, + StateTransition::IdentityTopUpFromAddresses(_) => false, + // Binds the created identity's declared public keys. + StateTransition::IdentityCreateFromAddresses(_) => true, + // Address funds families: post-state address balances only. + StateTransition::AddressFundsTransfer(_) => false, + StateTransition::AddressFundingFromAssetLock(_) => false, + StateTransition::AddressCreditWithdrawal(_) => false, + StateTransition::Shield(_) => false, + // Nullifier-spend proofs bind the exact Orchard actions of this + // transition. + StateTransition::Unshield(_) => true, + StateTransition::ShieldedTransfer(_) => true, + StateTransition::ShieldedWithdrawal(_) => true, + // Only the consumed outpoint (and current surplus-address state) + // is proven; competing shields over the same outpoint share the + // same query and result. + StateTransition::ShieldFromAssetLock(_) => false, + // Spent nullifiers plus the resulting identity do not bind the + // complete Orchard request, denomination context, or fallback + // address. + StateTransition::IdentityCreateFromShieldedPool(_) => false, + // Only the identity's post-debit balance is proven; the shielded note + // and the requested amount are not bound. + StateTransition::ShieldFromIdentity(_) => false, + // A spent nullifier is stored as an empty item shared by every + // spend family, so its presence cannot tell this top-up apart from + // a competing spend of the same notes that credits another + // identity, and the credited identity's balance is a snapshot at + // the proof's block. + StateTransition::IdentityTopUpFromShieldedPool(_) => false, + }; + + Ok(binds) + } + + /// Reconstruct the prove side's merged multi-root query and verify it STRICTLY. + /// + /// The shielded prove paths merge several sub-queries into a SINGLE multi-root + /// proof. [`grovedb::PathQuery::merge`] rejects sub-queries that still carry a + /// limit, so every sub-query's limit is cleared here before the merge. The strict + /// [`grovedb::GroveDb::verify_query_with_absence_proof`] in turn *requires* a + /// limit, but it must never be exhausted by the legitimate result set: the + /// per-layer succinctness check that rejects extra proof branches runs AFTER that + /// layer's result loop, and the result loop only breaks early once the limit hits + /// 0 — so a limit smaller than the real result count could break before a layer's + /// succinctness check runs and falsely reject an honest proof. Every shielded + /// merged query targets a fixed, tiny set of explicit keys ({nullifiers} plus one + /// address/document/outpoint), so an unreachable `u16::MAX` limit is sound: it + /// guarantees every layer is fully traversed while the (limit-independent) + /// succinctness check still rejects any proof padded with extra subtree branches. + /// + /// Returns the proof root hash and every proved `(path, key, element)` trio, left + /// for the caller to partition against the sub-query paths. + #[allow(clippy::type_complexity)] + fn verify_merged_query_strict_v1( + proof: &[u8], + mut sub_queries: Vec, + platform_version: &PlatformVersion, + ) -> Result< + ( + RootHash, + Vec, + ), + Error, + > { + for sub_query in &mut sub_queries { + sub_query.query.limit = None; + } + + let mut merged_pq = grovedb::PathQuery::merge( + sub_queries.iter().collect(), + &platform_version.drive.grove_version, + )?; + merged_pq.query.limit = Some(u16::MAX); + + Ok(grovedb::GroveDb::verify_query_with_absence_proof( + proof, + &merged_pq, + &platform_version.drive.grove_version, + )?) + } +} + +#[cfg(feature = "server")] +#[cfg(test)] +mod tests { + use super::*; + use crate::drive::Drive; + use crate::query::ContractLookupFn; + use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; + use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + use dpp::data_contract::document_type::random_document::CreateRandomDocument; + use dpp::data_contract::serialized_version::DataContractInSerializationFormat; + use dpp::document::DocumentV0Getters; + use dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0}; + use dpp::identity::Identity; + use dpp::prelude::DataContract; + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + use dpp::state_transition::proof_result::{ + StateTransitionProofOutcome, StateTransitionProofResult, + }; + use dpp::state_transition::StateTransition; + use dpp::tests::fixtures::get_dpns_data_contract_fixture; + use dpp::version::PlatformVersion; + use platform_version::TryIntoPlatformVersioned; + use std::sync::Arc; + + /// Helper: set up drive, insert a DPNS contract, return (drive, contract) + fn setup_drive_and_contract() -> (Drive, DataContract) { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let created_contract = + get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = created_contract.data_contract_owned(); + drive + .insert_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to insert contract"); + (drive, contract) + } + + /// Helper: set up drive and add an identity, return (drive, identity) + fn setup_drive_and_identity() -> (Drive, Identity) { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let identity = Identity::random_identity(3, Some(14), platform_version) + .expect("expected a platform identity"); + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + (drive, identity) + } + + // ----------------------------------------------------------------------- + // DataContractCreate + // ----------------------------------------------------------------------- + + #[test] + fn verify_data_contract_create_happy_path() { + let (drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + let contract_id = contract.id().to_buffer(); + + // Generate a proof for this contract + let proof = drive + .prove_contract(contract_id, None, platform_version) + .expect("expected to prove contract"); + + // Build the DataContractCreate state transition from the contract + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (root_hash, proof_result) = result.unwrap(); + assert_ne!(root_hash, [0u8; 32], "root hash should not be all zeros"); + match proof_result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedDataContract(verified_contract), + ) => { + assert_eq!( + verified_contract.id(), + contract.id(), + "verified contract id should match" + ); + } + other => panic!("expected VerifiedDataContract, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // DataContractUpdate + // ----------------------------------------------------------------------- + + #[test] + fn verify_data_contract_update_happy_path() { + let (drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + let contract_id = contract.id().to_buffer(); + + // For the update transition, we use the same contract (version hasn't changed + // in the fixture, but the proof verifies the contract is as expected). + let proof = drive + .prove_contract(contract_id, None, platform_version) + .expect("expected to prove contract"); + + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; + let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( + DataContractUpdateTransitionV0 { + identity_contract_nonce: 0, + data_contract: data_contract_serialized, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (_root_hash, proof_result) = result.unwrap(); + // The proven contract body equaling the update's target does not + // prove that THIS update executed (the version may predate the + // request), so the outcome is a snapshot, not execution evidence. + match proof_result { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedDataContract(verified_contract), + ) => { + assert_eq!(verified_contract.id(), contract.id()); + } + other => panic!( + "expected AffectedState(VerifiedDataContract), got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // IdentityCreate + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_create_happy_path() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + + let proof = drive + .prove_full_identity(identity_id, None, &platform_version.drive) + .expect("expected to prove full identity"); + + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: identity.id(), + public_keys: identity.public_keys().values().map(Into::into).collect(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (_root_hash, proof_result) = result.unwrap(); + match proof_result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedIdentity(verified_identity), + ) => { + assert_eq!(verified_identity.id(), identity.id()); + } + other => panic!("expected VerifiedIdentity, got {:?}", other), + } + } + + #[test] + fn verify_identity_create_rejects_unrelated_public_keys() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let proof = drive + .prove_full_identity(identity.id().to_buffer(), None, &platform_version.drive) + .expect("expected to prove full identity"); + + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: identity.id(), + public_keys: vec![], + ..Default::default() + }, + )); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + &|_id| Ok(None), + platform_version, + ); + + assert!(matches!( + result, + Err(Error::Proof(ProofError::IncorrectProof(_))) + )); + } + + // ----------------------------------------------------------------------- + // IdentityTopUp + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_top_up_returns_affected_state_snapshot() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + + let proof = drive + .prove_identity_balance_and_revision(identity_id, None, &platform_version.drive) + .expect("expected to prove identity balance and revision"); + + use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; + use dpp::state_transition::state_transitions::identity::identity_topup_transition::v0::IdentityTopUpTransitionV0; + let st = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0( + IdentityTopUpTransitionV0 { + identity_id: identity.id(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + // A top-up proof cannot be bound to the transition's execution, so + // the outcome must be tagged as an affected-state snapshot, never as + // execution evidence. + let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed"); + + match outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), + ) => { + assert_eq!(partial_identity.id, identity.id()); + assert!(partial_identity.balance.is_some()); + assert!(partial_identity.revision.is_some()); + } + other => panic!( + "expected AffectedState(VerifiedPartialIdentity), got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // IdentityCreditWithdrawal + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_credit_withdrawal_returns_affected_state_snapshot() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + + let proof = drive + .prove_identity_balance(identity_id, None, &platform_version.drive) + .expect("expected to prove identity balance"); + + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; + let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( + IdentityCreditWithdrawalTransitionV0 { + identity_id: identity.id(), + amount: 100, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed"); + + match outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), + ) => { + assert_eq!(partial_identity.id, identity.id()); + assert!(partial_identity.balance.is_some()); + } + other => panic!( + "expected AffectedState(VerifiedPartialIdentity), got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // IdentityUpdate + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_update_happy_path() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + + // The verify function for IdentityUpdate uses verify_identity_keys_by_identity_id + // with with_revision=true, which creates a merged query of keys + revision. + // We need a proof that contains both. Use the same path queries merged together. + use crate::drive::identity::key::fetch::IdentityKeysRequest; + let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); + let keys_path_query = key_request.into_path_query(); + let revision_path_query = Drive::identity_revision_query(&identity_id); + let merged = grovedb::PathQuery::merge( + vec![&keys_path_query, &revision_path_query], + &platform_version.drive.grove_version, + ) + .expect("expected to merge path queries"); + let proof = drive + .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) + .expect("expected to prove identity keys and revision"); + + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; + let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( + IdentityUpdateTransitionV0 { + identity_id: identity.id(), + revision: identity.revision(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (_root_hash, proof_result) = result.unwrap(); + match proof_result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), + ) => { + assert_eq!(partial_identity.id, identity.id()); + assert!( + !partial_identity.loaded_public_keys.is_empty(), + "loaded public keys should not be empty" + ); + } + other => panic!("expected VerifiedPartialIdentity, got {:?}", other), + } + } + + #[test] + fn verify_identity_update_rejects_unrelated_revision() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); + let keys_path_query = key_request.into_path_query(); + let revision_path_query = Drive::identity_revision_query(&identity_id); + let merged = grovedb::PathQuery::merge( + vec![&keys_path_query, &revision_path_query], + &platform_version.drive.grove_version, + ) + .expect("expected to merge path queries"); + let proof = drive + .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) + .expect("expected to prove identity keys and revision"); + + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; + let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( + IdentityUpdateTransitionV0 { + identity_id: identity.id(), + revision: identity.revision() + 1, + ..Default::default() + }, + )); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + &|_id| Ok(None), + platform_version, + ); + + assert!(matches!( + result, + Err(Error::Proof(ProofError::IncorrectProof(_))) + )); + } + + #[test] + fn verify_identity_update_rejects_active_key_as_disabled() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); + let keys_path_query = key_request.into_path_query(); + let revision_path_query = Drive::identity_revision_query(&identity_id); + let merged = grovedb::PathQuery::merge( + vec![&keys_path_query, &revision_path_query], + &platform_version.drive.grove_version, + ) + .expect("expected to merge path queries"); + let proof = drive + .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) + .expect("expected to prove identity keys and revision"); + let key_id = *identity + .public_keys() + .keys() + .next() + .expect("identity should have a public key"); + + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; + let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( + IdentityUpdateTransitionV0 { + identity_id: identity.id(), + revision: identity.revision(), + disable_public_keys: vec![key_id], + ..Default::default() + }, + )); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + &|_id| Ok(None), + platform_version, + ); + + assert!(matches!( + result, + Err(Error::Proof(ProofError::IncorrectProof(_))) + )); + } + + #[test] + fn verify_identity_update_accepts_key_disabled_before_proof_block() { + let (drive, identity) = setup_drive_and_identity(); + let platform_version = PlatformVersion::latest(); + let identity_id = identity.id().to_buffer(); + let key_id = *identity + .public_keys() + .keys() + .next() + .expect("identity should have a public key"); + + // Disable the key at time 50, then verify against a proof served at a + // later block: `disabled_at` never changes afterwards, so retried or + // later proofs must still verify. + let disabled_at: u64 = 50; + drive + .disable_identity_keys( + identity_id, + vec![key_id], + disabled_at, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to disable identity key"); + + let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); + let keys_path_query = key_request.into_path_query(); + let revision_path_query = Drive::identity_revision_query(&identity_id); + let merged = grovedb::PathQuery::merge( + vec![&keys_path_query, &revision_path_query], + &platform_version.drive.grove_version, + ) + .expect("expected to merge path queries"); + let proof = drive + .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) + .expect("expected to prove identity keys and revision"); + + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; + let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( + IdentityUpdateTransitionV0 { + identity_id: identity.id(), + revision: identity.revision(), + disable_public_keys: vec![key_id], + ..Default::default() + }, + )); + + let later_block = BlockInfo { + time_ms: disabled_at + 10_000, + ..Default::default() + }; + let (_root_hash, result) = Drive::verify_state_transition_was_executed_with_proof( + &st, + &later_block, + &proof, + &|_id| Ok(None), + platform_version, + ) + .expect("expected verification to accept a key disabled before the proof block"); + + match result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), + ) => { + assert_eq!(partial_identity.id, identity.id()); + } + other => panic!( + "expected ExecutionProved(VerifiedPartialIdentity), got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // IdentityCreditTransfer + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_credit_transfer_returns_affected_state_snapshot() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + let sender = Identity::random_identity(3, Some(14), platform_version) + .expect("expected a sender identity"); + let recipient = Identity::random_identity(3, Some(15), platform_version) + .expect("expected a recipient identity"); + + drive + .add_new_identity( + sender.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add sender identity"); + drive + .add_new_identity( + recipient.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add recipient identity"); + + let proof = drive + .prove_many_identity_balances( + &[sender.id().to_buffer(), recipient.id().to_buffer()], + None, + &platform_version.drive, + ) + .expect("expected to prove both balances"); + + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( + IdentityCreditTransferTransitionV0 { + identity_id: sender.id(), + recipient_id: recipient.id(), + amount: 50, + nonce: 1, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed"); + + match outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedBalanceTransfer( + sender_identity, + recipient_identity, + ), + ) => { + assert_eq!(sender_identity.id, sender.id()); + assert_eq!(recipient_identity.id, recipient.id()); + assert!(sender_identity.balance.is_some()); + assert!(recipient_identity.balance.is_some()); + } + other => panic!( + "expected AffectedState(VerifiedBalanceTransfer), got {:?}", + other + ), + } + } + + // ----------------------------------------------------------------------- + // Batch: empty transitions error + // ----------------------------------------------------------------------- + + #[test] + fn verify_batch_empty_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof, got: {:?}", + result + ); + let err = result.unwrap_err(); + match &err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("no transition"), + "expected error about no transition, got: {}", + msg + ); + } + other => panic!("expected InvalidTransition error, got: {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // Batch: too many transitions error + // ----------------------------------------------------------------------- + + #[test] + fn verify_batch_too_many_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base1 = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + let base2 = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 2, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![ + DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base: base1 }, + )), + DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base: base2 }, + )), + ], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof, got: {:?}", + result + ); + let err = result.unwrap_err(); + match &err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("does not support more than one document"), + "expected error about too many transitions, got: {}", + msg + ); + } + other => panic!("expected InvalidTransition error, got: {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // Batch: document delete happy path + // ----------------------------------------------------------------------- + + #[test] + fn verify_batch_document_delete_happy_path() { + let (drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + + let document_type = contract + .document_type_for_name("preorder") + .expect("expected preorder document type"); + + let document = document_type + .random_document(Some(99), platform_version) + .expect("expected a random document"); + let doc_id = document.id(); + + // Insert the document + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("expected to insert document"); + + // Delete the document + drive + .delete_document_for_contract( + doc_id, + &contract, + "preorder", + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("expected to delete document"); + + // Generate a proof for the now-absent document + use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + let single_query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "preorder".to_string(), + document_type_keeps_history: document_type.documents_keep_history(), + document_id: doc_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + }; + let path_query = single_query + .construct_path_query(platform_version) + .expect("expected to build path query"); + let proof = drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("expected to get proof"); + + // Build a document delete batch transition + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: doc_id, + identity_contract_nonce: 1, + document_type_name: "preorder".to_string(), + data_contract_id: contract.id(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))], + ..Default::default() + })); + + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (_root_hash, proof_result) = result.unwrap(); + match proof_result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedDocuments(docs), + ) => { + assert_eq!(docs.len(), 1, "expected exactly one document entry"); + let (returned_id, maybe_doc) = docs.into_iter().next().unwrap(); + assert_eq!(returned_id, doc_id); + assert!( + maybe_doc.is_none(), + "document should be None after deletion" + ); + } + other => panic!("expected VerifiedDocuments, got {:?}", other), + } + } + + /// An erase acts on a document that was already deleted, so the by-id + /// proof shows the same absence whether or not it ran. The classifier must + /// report that as affected state, while a delete over the very same proof + /// stays execution-proved: presence before and absence after do bind a + /// delete's execution. + #[test] + fn verify_batch_document_erase_is_affected_state_not_execution_proved() { + use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + use dpp::document::DocumentV0Setters; + use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::{ + DocumentEraseTransition, DocumentEraseTransitionV0, + }; + use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransitionV1, DocumentTransitionV1, + }; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::{ + DocumentDeleteTransition, DocumentDeleteTransitionV0, + }; + use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV2}; + use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract( + "tests/supporting_files/contract/family/family-contract-with-history.json", + false, + platform_version, + ) + .expect("expected the family history contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + None, + None, + platform_version, + ) + .expect("expected to apply the contract"); + let document_type = contract + .document_type_for_name("person") + .expect("expected the person type"); + let mut document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some([4u8; 32].into()), + document_type, + platform_version, + ) + .expect("expected a person"); + for revision in 1..=2u64 { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + platform_version, + None, + ) + .expect("expected to write a revision"); + } + let doc_id = document.id(); + drive + .delete_document_for_contract( + doc_id, + &contract, + "person", + BlockInfo::default_with_time(5_000), + true, + None, + platform_version, + None, + ) + .expect("expected the delete to succeed"); + + let path_query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".to_string(), + document_type_keeps_history: true, + document_id: doc_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + } + .construct_path_query(platform_version) + .expect("expected a by-id path query"); + let proof = drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("expected an absence proof"); + + let base = || { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: doc_id, + identity_contract_nonce: 1, + document_type_name: "person".to_string(), + data_contract_id: contract.id(), + }) + }; + let batch = |transition: DocumentTransitionV1| { + StateTransition::Batch(BatchTransition::V2(BatchTransitionV2 { + owner_id: Default::default(), + transitions: vec![BatchedTransitionV1::Document(transition)], + ..Default::default() + })) + }; + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + let verify = |state_transition: StateTransition| { + Drive::verify_state_transition_was_executed_with_proof( + &state_transition, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed") + .1 + }; + + let erase_outcome = verify(batch(DocumentTransitionV1::Erase( + DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: base() }), + ))); + match erase_outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedDocuments(documents), + ) => { + assert_eq!(documents.len(), 1); + let (id, found) = documents.into_iter().next().unwrap(); + assert_eq!(id, doc_id); + assert!(found.is_none(), "the document is absent by id"); + } + other => panic!("an erase must be classified as affected state, got {other:?}"), + } + + let delete_outcome = verify(batch(DocumentTransitionV1::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { base: base() }), + ))); + assert!( + matches!( + delete_outcome, + StateTransitionProofOutcome::ExecutionProved(_) + ), + "a delete over the same proof stays execution-proved, got {delete_outcome:?}" + ); + } + + // ----------------------------------------------------------------------- + // Batch: document create happy path + // ----------------------------------------------------------------------- + + #[test] + fn verify_batch_document_create_happy_path() { + let (drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + + let document_type = contract + .document_type_for_name("preorder") + .expect("expected preorder document type"); + + let document = document_type + .random_document(Some(99), platform_version) + .expect("expected a random document"); + let doc_id = document.id(); + let owner_id = document.owner_id(); + + let block_info = BlockInfo::default(); + + // Insert the document + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: Some(owner_id.to_buffer()), + }, + contract: &contract, + document_type, + }, + false, + block_info, + true, + None, + platform_version, + None, + ) + .expect("expected to insert document"); + + // Generate a proof for the existing document + use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + let single_query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "preorder".to_string(), + document_type_keeps_history: document_type.documents_keep_history(), + document_id: doc_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + }; + let path_query = single_query + .construct_path_query(platform_version) + .expect("expected to build path query"); + let proof = drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("expected to get proof"); + + // Build a document create batch transition + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: doc_id, + identity_contract_nonce: 1, + document_type_name: "preorder".to_string(), + data_contract_id: contract.id(), + }); + + // Build the create transition from document properties + let create_transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base, + entropy: [100u8; 32], + data: document.properties().clone(), + prefunded_voting_balance: None, + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id, + transitions: vec![DocumentTransition::Create(create_transition)], + ..Default::default() + })); + + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &block_info, + &proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + result.is_ok(), + "expected verification to succeed, got: {:?}", + result.err() + ); + let (_root_hash, proof_result) = result.unwrap(); + match proof_result { + StateTransitionProofOutcome::ExecutionProved( + StateTransitionProofResult::VerifiedDocuments(docs), + ) => { + assert_eq!(docs.len(), 1, "expected exactly one document entry"); + let (returned_id, maybe_doc) = docs.into_iter().next().unwrap(); + assert_eq!(returned_id, doc_id); + assert!(maybe_doc.is_some(), "document should exist after creation"); + } + other => panic!("expected VerifiedDocuments, got {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // Batch: unknown contract returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_batch_document_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))], + ..Default::default() + })); + + // Provider returns None for all contracts + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof, got: {:?}", + result + ); + let err = result.unwrap_err(); + match &err { + Error::Proof(ProofError::UnknownContract(msg)) => { + assert!( + msg.contains("unknown contract"), + "expected error about unknown contract, got: {}", + msg + ); + } + other => panic!("expected UnknownContract error, got: {:?}", other), + } + } + + // ----------------------------------------------------------------------- + // DataContractCreate: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_data_contract_create_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + // Build a minimal contract create transition + let created_contract = + get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = created_contract.data_contract(); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + // Empty proof should cause a GroveDB/proof error + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // IdentityCreate: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_create_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // IdentityTopUp: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_top_up_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; + use dpp::state_transition::state_transitions::identity::identity_topup_transition::v0::IdentityTopUpTransitionV0; + let st = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0( + IdentityTopUpTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // IdentityCreditWithdrawal: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_credit_withdrawal_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; + let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( + IdentityCreditWithdrawalTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + amount: 100, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // IdentityUpdate: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_update_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; + use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; + let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( + IdentityUpdateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + revision: 1, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // IdentityCreditTransfer: empty proof returns error + // ----------------------------------------------------------------------- + + #[test] + fn verify_identity_credit_transfer_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( + IdentityCreditTransferTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + recipient_id: dpp::prelude::Identifier::random(), + amount: 50, + nonce: 1, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // Additional coverage for rarely-executed error branches. + // + // The following tests exercise variants that were not previously + // covered by the inline suite, and in particular focus on error arms + // (unknown-contract, invalid-transition, malformed-input) that are + // otherwise hard to exercise through happy-path tests. + // ----------------------------------------------------------------------- + + // --- DataContractUpdate: empty proof returns Error::Proof or Error::GroveDB. + #[test] + fn verify_data_contract_update_empty_proof_returns_error() { + let (_drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; + let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( + DataContractUpdateTransitionV0 { + identity_contract_nonce: 1, + data_contract: data_contract_serialized, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + // Empty proof → must error (either Error::Proof or Error::GroveDB). + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", + result + ); + } + + // --- MasternodeVote: unknown contract returns a specific + // `ProofError::UnknownContract` because the provider returns None. + #[test] + fn verify_masternode_vote_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + + let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: dpp::prelude::Identifier::from([7u8; 32]), + document_type_name: "some_type".to_string(), + index_name: "idx".to_string(), + index_values: vec![], + }, + ), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + // Provider always returns None – triggers UnknownContract. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract error, got Ok"); + match err { + crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { + assert!( + msg.contains("unknown contract") && msg.contains("resource vote"), + "unexpected UnknownContract message: {msg}" + ); + } + other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), + } + } + + // --- ShieldedWithdrawal: empty proof + no nullifiers → error. + // + // The `first_nullifier` guard in the verifier is only reachable once + // `verify_shielded_nullifiers` returns Ok. With an empty proof, grove-db + // proof verification errors out first, so this test cannot reach the + // `InvalidTransition` guard specifically — we only assert that some + // Error::Proof / Error::GroveDB is returned on the failing path. + #[test] + fn verify_shielded_withdrawal_empty_proof_error() { + let platform_version = PlatformVersion::latest(); + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; + use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; + use dpp::withdrawal::Pooling; + + let st = StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( + ShieldedWithdrawalTransitionV0 { + actions: vec![], + unshielding_amount: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shielded withdrawal with empty proof, got: {:?}", + result + ); + } + + // --- ShieldedTransfer: empty proof leads to an error (Error::Proof + // or Error::GroveDB) through the verify_shielded_nullifiers path. + #[test] + fn verify_shielded_transfer_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; + use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; + + let st = StateTransition::ShieldedTransfer(ShieldedTransferTransition::V0( + ShieldedTransferTransitionV0 { + actions: vec![], + value_balance: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shielded transfer with empty proof, got: {:?}", + result + ); + } + + // --- Unshield: empty proof leads to an error. + #[test] + fn verify_unshield_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::unshield_transition::v0::UnshieldTransitionV0; + use dpp::state_transition::unshield_transition::UnshieldTransition; + + let st = StateTransition::Unshield(UnshieldTransition::V0(UnshieldTransitionV0 { + output_address: Default::default(), + actions: vec![], + unshielding_amount: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for unshield with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreateFromShieldedPool: empty proof returns error. + // + // Exercises the STRICT merged-query verify arm: an empty proof cannot satisfy the strict + // `verify_query` over the merged {nullifier-tree, identity} query, so the verifier must reject + // (rather than silently accepting). The positive prove→verify roundtrip lives in drive-abci's + // identity_create_from_shielded_pool tests (synthetic-action execution). + #[test] + fn verify_identity_create_from_shielded_pool_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::shielded::SerializedAction; + use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions; + use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; + use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; + + let actions = vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }]; + let identity_id = derive_identity_id_from_actions(&actions); + + let st = StateTransition::IdentityCreateFromShieldedPool( + IdentityCreateFromShieldedPoolTransition::V0( + IdentityCreateFromShieldedPoolTransitionV0 { + public_keys: vec![], + denomination: 10_000_000_000, + actions, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + send_to_address_on_creation_failure: PlatformAddress::P2pkh([0u8; 20]), + identity_id, + }, + ), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity create from shielded pool with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreditTransferToAddresses: empty proof returns error. + #[test] + fn verify_identity_credit_transfer_to_addresses_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; + + let st = StateTransition::IdentityCreditTransferToAddresses( + IdentityCreditTransferToAddressesTransition::V0( + IdentityCreditTransferToAddressesTransitionV0 { + identity_id: dpp::prelude::Identifier::from([3u8; 32]), + ..Default::default() + }, + ), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity credit transfer to addresses with empty proof, got: {:?}", + result + ); + } + + // ----------------------------------------------------------------------- + // Additional error-path coverage for state transitions whose error arms + // were not previously tested. These tests target proof decoding / early + // validation errors on state transition variants that were not covered + // by the existing test suite. + // ----------------------------------------------------------------------- + + // --- AddressFundsTransfer: empty proof returns error. + #[test] + fn verify_address_funds_transfer_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; + use dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (1u32, 1000u64)); + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([2u8; 20]), 500u64); + + let st = StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0( + AddressFundsTransferTransitionV0 { + inputs, + outputs, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + + // --- AddressCreditWithdrawal: empty proof returns error. + #[test] + fn verify_address_credit_withdrawal_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; + use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( + AddressCreditWithdrawalTransitionV0 { + inputs, + output: None, + fee_strategy: vec![], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + + // --- AddressCreditWithdrawal with change output: empty proof returns error. + #[test] + fn verify_address_credit_withdrawal_with_change_output_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; + use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( + AddressCreditWithdrawalTransitionV0 { + inputs, + output: Some((PlatformAddress::P2pkh([4u8; 20]), 1000u64)), + fee_strategy: vec![], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + user_fee_increase: 0, + input_witnesses: vec![], + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + + // --- AddressFundingFromAssetLock: empty proof returns error. + #[test] + fn verify_address_funding_from_asset_lock_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; + use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; + use std::collections::BTreeMap; + + let mut outputs = BTreeMap::new(); + outputs.insert(PlatformAddress::P2pkh([7u8; 20]), None); + + let st = StateTransition::AddressFundingFromAssetLock( + AddressFundingFromAssetLockTransition::V0(AddressFundingFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), + inputs: BTreeMap::new(), + outputs, + fee_strategy: vec![], + user_fee_increase: 0, + signature: BinaryData::new(vec![]), + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + + // --- Shield: empty proof returns error. + #[test] + fn verify_shield_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::shield_transition::v0::ShieldTransitionV0; + use dpp::state_transition::shield_transition::ShieldTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([8u8; 20]), (1u32, 1000u64)); + + let st = StateTransition::Shield(ShieldTransition::V0(ShieldTransitionV0 { + inputs, + actions: vec![], + amount: 500, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + + // --- ShieldFromAssetLock with invalid (no-output) Instant asset lock proof + // should return InvalidTransition("shield from asset lock has no outpoint"). + // Default InstantAssetLockProof has no AssetLockPayload, so out_point() returns None. + #[test] + fn verify_shield_from_asset_lock_missing_outpoint_returns_invalid_transition() { + let platform_version = PlatformVersion::latest(); + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + + let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), + actions: vec![], + value_balance: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + surplus_output: None, + signature: BinaryData::new(vec![]), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition error, got Ok"); + match err { + crate::error::Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("shield from asset lock has no outpoint"), + "unexpected InvalidTransition message: {msg}" + ); + } + other => panic!("expected Error::Proof(InvalidTransition), got {:?}", other), + } + } + + // --- ShieldFromAssetLock with Chain asset lock proof (has outpoint) and + // empty GroveDB proof: should return an error from grove-db verification. + #[test] + fn verify_shield_from_asset_lock_chain_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::dashcore::OutPoint; + use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; + use dpp::platform_value::BinaryData; + use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + + let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }), + actions: vec![], + value_balance: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + surplus_output: None, + signature: BinaryData::new(vec![]), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shield from asset lock (chain) with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreateFromAddresses: empty proof returns error. + #[test] + fn verify_identity_create_from_addresses_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; + use dpp::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([9u8; 20]), (1u32, 2000u64)); + + let st = StateTransition::IdentityCreateFromAddresses( + IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { + public_keys: vec![], + inputs, + output: None, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity create from addresses with empty proof, got: {:?}", + result + ); + } + + #[test] + fn verify_identity_create_from_addresses_binds_public_keys() { + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; + use dpp::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; + use dpp::state_transition::StateTransitionIdentityIdFromInputs; + use std::collections::BTreeMap; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([9u8; 20]), (1u32, 2000u64)); + let id_transition = + IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { + public_keys: vec![], + inputs: inputs.clone(), + output: None, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }); + let identity_id = id_transition + .identity_id_from_inputs() + .expect("expected identity id from inputs"); + + let mut identity = Identity::random_identity(3, Some(14), platform_version) + .expect("expected a platform identity"); + identity.set_id(identity_id); + drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to add identity"); + + let matching = StateTransition::IdentityCreateFromAddresses( + IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { + public_keys: identity.public_keys().values().map(Into::into).collect(), + inputs: inputs.clone(), + output: None, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + let proof = drive + .prove_state_transition(&matching, None, platform_version) + .expect("expected proof creation") + .into_data() + .expect("expected proof data"); + + let matching_result = Drive::verify_state_transition_was_executed_with_proof( + &matching, + &BlockInfo::default(), + &proof, + &|_id| Ok(None), + platform_version, + ); + assert!(matching_result.is_ok()); + + let mismatched = StateTransition::IdentityCreateFromAddresses( + IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { + public_keys: vec![], + inputs, + output: None, + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + let mismatched_result = Drive::verify_state_transition_was_executed_with_proof( + &mismatched, + &BlockInfo::default(), + &proof, + &|_id| Ok(None), + platform_version, + ); + assert!(matches!( + mismatched_result, + Err(Error::Proof(ProofError::IncorrectProof(_))) + )); + } + + // --- IdentityTopUpFromAddresses: empty proof returns error. + #[test] + fn verify_identity_top_up_from_addresses_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; + use dpp::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; + use std::collections::BTreeMap; + + let mut inputs = BTreeMap::new(); + inputs.insert(PlatformAddress::P2pkh([10u8; 20]), (1u32, 500u64)); + + let st = StateTransition::IdentityTopUpFromAddresses( + IdentityTopUpFromAddressesTransition::V0(IdentityTopUpFromAddressesTransitionV0 { + inputs, + output: None, + identity_id: dpp::prelude::Identifier::from([11u8; 32]), + fee_strategy: vec![], + user_fee_increase: 0, + input_witnesses: vec![], + }), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity top up from addresses with empty proof, got: {:?}", + result + ); + } + + // --- IdentityCreditWithdrawal V1 empty proof returns error. + #[test] + fn verify_identity_credit_withdrawal_v1_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::withdrawal::Pooling; + + let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V1( + IdentityCreditWithdrawalTransitionV1 { + identity_id: dpp::prelude::Identifier::random(), + amount: 100, + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: None, + nonce: 1, + user_fee_increase: 0, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity credit withdrawal v1 with empty proof, got: {:?}", + result + ); + } + + // --- MasternodeVote: empty proof (but with a known contract) returns + // an Error::Proof / Error::GroveDB because the proof can't be decoded. + #[test] + fn verify_masternode_vote_empty_proof_with_known_contract_returns_error() { + let (_drive, contract) = setup_drive_and_contract(); + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + + let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: contract.id(), + document_type_name: "preorder".to_string(), + index_name: "idx".to_string(), + index_values: vec![], + }, + ), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for masternode vote with empty proof, got: {:?}", + result + ); + } + + // --- ShieldedWithdrawal with no nullifiers and a known contract that + // triggers InvalidTransition("shielded withdrawal has no nullifiers") via + // the first_nullifier guard. + // + // We route past the grove-db verification by providing `actions: vec![]` + // (so nullifiers() is empty) and a valid empty proof for nullifiers. + // Because proof is empty, grove-db errors out first — we only check for + // some Error variant here (this is a defensive test that ensures the + // known-contract branch doesn't crash). + #[test] + fn verify_shielded_withdrawal_with_known_contract_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::identity::core_script::CoreScript; + use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; + use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; + use dpp::withdrawal::Pooling; + + let st = StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( + ShieldedWithdrawalTransitionV0 { + actions: vec![], + unshielding_amount: 0, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![]), + }, + )); + + // Load the real withdrawals contract so the known_contracts_provider + // returns Some. Empty proof will still fail, but we exercise the + // contract-lookup path. + use dpp::data_contracts::withdrawals_contract; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + let withdrawals = + load_system_data_contract(SystemDataContract::Withdrawals, platform_version) + .expect("expected withdrawals system contract"); + let withdrawals_arc = Arc::new(withdrawals); + let expected_id = withdrawals_contract::ID; + let known_contracts_provider_fn: &ContractLookupFn = &|id| { + if id == &expected_id { + Ok(Some(withdrawals_arc.clone())) + } else { + Ok(None) + } + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for shielded withdrawal (known contract, empty proof), got: {:?}", + result + ); + } + + // --- Batch V1 with Token Mint transition + unknown contract returns + // UnknownContract error (token transition branch). + /// The classifier is the single authority on which transition families' + /// proofs bind execution; this table pins the snapshot-only families so + /// a reclassification (accidental or deliberate) fails a test instead of + /// silently upgrading a snapshot into execution evidence. + #[test] + fn classifier_tags_snapshot_only_families_as_affected_state() { + use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; + use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; + use dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use dpp::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; + use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; + + let no_contracts: &ContractLookupFn = &|_id| Ok(None); + + let snapshot_only: Vec<(&str, StateTransition)> = vec![ + ( + "identity top up", + StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(Default::default())), + ), + ( + "identity credit withdrawal", + StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( + Default::default(), + )), + ), + ( + "identity credit transfer", + StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( + Default::default(), + )), + ), + ( + "identity credit transfer to addresses", + StateTransition::IdentityCreditTransferToAddresses( + IdentityCreditTransferToAddressesTransition::V0(Default::default()), + ), + ), + ( + "identity top up from addresses", + StateTransition::IdentityTopUpFromAddresses( + IdentityTopUpFromAddressesTransition::V0(Default::default()), + ), + ), + ( + "address funds transfer", + StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0( + Default::default(), + )), + ), + ( + "address funding from asset lock", + StateTransition::AddressFundingFromAssetLock( + AddressFundingFromAssetLockTransition::V0(Default::default()), + ), + ), + ( + "address credit withdrawal", + StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( + Default::default(), + )), + ), + ]; + + for (name, st) in snapshot_only { + let binds = Drive::state_transition_proof_binds_execution_v1(&st, no_contracts) + .expect("classifier should not error for contract-independent families"); + assert!( + !binds, + "{name}: proof must be classified as an affected-state snapshot" + ); + } + } + + #[test] + fn verify_no_history_token_empty_proof_returns_error() { + use dpp::data_contract::accessors::v1::DataContractV1Setters; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; + use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransition; + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransition; + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV1}; + + let platform_version = PlatformVersion::latest(); + let mut contract = + get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + let mut token_configuration = TokenConfigurationV0::default_most_restrictive(); + token_configuration.keeps_history = + TokenKeepsHistoryRulesV0::default_for_keeping_all_history(false).into(); + contract.set_tokens(BTreeMap::from([( + 0, + TokenConfiguration::V0(token_configuration), + )])); + let contract_id = contract.id(); + let token_id = contract.token_id(0).expect("expected token id"); + let owner_id = Identifier::from([9u8; 32]); + let recipient_id = Identifier::from([10u8; 32]); + + let base = || { + TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: contract_id, + token_id, + using_group_info: None, + }) + }; + let transitions = vec![ + TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: base(), + burn_amount: 42, + public_note: None, + })), + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: base(), + amount: 42, + issued_to_identity_id: Some(recipient_id), + public_note: None, + })), + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: base(), + amount: 42, + recipient_id, + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })), + ]; + + let contract = Arc::new(contract); + let known_contracts_provider_fn: &ContractLookupFn = &|id| { + if id == &contract_id { + Ok(Some(contract.clone())) + } else { + Ok(None) + } + }; + + for transition in transitions { + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + assert!( + matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), + "expected a proof error for an empty proof, got: {:?}", + result + ); + } + } + + // --- Batch V1 with Token Mint transition + unknown contract returns + // UnknownContract error (token transition branch). + #[test] + fn verify_batch_token_mint_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([44u8; 32]), + token_id: dpp::prelude::Identifier::from([45u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: token_base, + amount: 99, + issued_to_identity_id: Some(dpp::prelude::Identifier::from([77u8; 32])), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([2u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + match err { + crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { + assert!( + msg.contains("unknown contract") && msg.contains("token verification"), + "unexpected UnknownContract message: {msg}" + ); + } + other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), + } + } + + // --- Batch V1 with Token Transfer transition + unknown contract. + #[test] + fn verify_batch_token_transfer_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([58u8; 32]), + token_id: dpp::prelude::Identifier::from([59u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { + base: token_base, + amount: 100, + recipient_id: dpp::prelude::Identifier::from([88u8; 32]), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([3u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Freeze transition + unknown contract. + #[test] + fn verify_batch_token_freeze_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::TokenFreezeTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([71u8; 32]), + token_id: dpp::prelude::Identifier::from([72u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { + base: token_base, + identity_to_freeze_id: dpp::prelude::Identifier::from([91u8; 32]), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([4u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Unfreeze transition + unknown contract. + #[test] + fn verify_batch_token_unfreeze_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([81u8; 32]), + token_id: dpp::prelude::Identifier::from([82u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { + base: token_base, + frozen_identity_id: dpp::prelude::Identifier::from([92u8; 32]), + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([5u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token DestroyFrozenFunds transition + unknown contract. + #[test] + fn verify_batch_token_destroy_frozen_funds_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([101u8; 32]), + token_id: dpp::prelude::Identifier::from([102u8; 32]), + using_group_info: None, + }); + let token_transition = TokenTransition::DestroyFrozenFunds( + TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { + base: token_base, + frozen_identity_id: dpp::prelude::Identifier::from([103u8; 32]), + public_note: None, + }), + ); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([6u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with Token Claim transition + unknown contract. + #[test] + fn verify_batch_token_claim_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([111u8; 32]), + token_id: dpp::prelude::Identifier::from([112u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: token_base, + distribution_type: TokenDistributionType::PreProgrammed, + public_note: None, + })); + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([7u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Replace transition + unknown contract + // returns UnknownContract. + #[test] + fn verify_batch_document_replace_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransition; + use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Replace(DocumentReplaceTransition::V0( + DocumentReplaceTransitionV0 { + base, + revision: 2, + data: Default::default(), + }, + ))], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Transfer transition + unknown contract. + #[test] + fn verify_batch_document_transfer_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransition; + use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Transfer( + DocumentTransferTransition::V0(DocumentTransferTransitionV0 { + base, + revision: 1, + recipient_owner_id: dpp::prelude::Identifier::from([77u8; 32]), + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document UpdatePrice transition + unknown contract. + #[test] + fn verify_batch_document_update_price_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransition; + use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::UpdatePrice( + DocumentUpdatePriceTransition::V0(DocumentUpdatePriceTransitionV0 { + base, + revision: 1, + price: 500, + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single document Purchase transition + unknown contract. + #[test] + fn verify_batch_document_purchase_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransition; + use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "test".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Purchase( + DocumentPurchaseTransition::V0(DocumentPurchaseTransitionV0 { + base, + revision: 1, + price: 500, + }), + )], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch V1 with an empty transitions vec returns InvalidTransition + // (covers the V1 batch "no transition" arm). + #[test] + fn verify_batch_v1_empty_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions: vec![], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition, got Ok"); + match err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("no transition"), + "expected 'no transition' message, got: {msg}" + ); + } + other => panic!("expected InvalidTransition, got: {:?}", other), + } + } + + // --- Batch V1 with two transitions returns InvalidTransition (too-many arm). + #[test] + fn verify_batch_v1_too_many_transitions_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let make_delete = |nonce: u64| -> BatchedTransition { + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: nonce, + document_type_name: "x".to_string(), + data_contract_id: Default::default(), + }); + BatchedTransition::Document(DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))) + }; + + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions: vec![make_delete(1), make_delete(2)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected InvalidTransition, got Ok"); + match err { + Error::Proof(ProofError::InvalidTransition(msg)) => { + assert!( + msg.contains("does not support more than one document"), + "expected too-many-document message, got: {msg}" + ); + } + other => panic!("expected InvalidTransition, got: {:?}", other), + } + } + + // --- IdentityCreditTransferToAddresses with address recipients returns + // error for empty proof (exercises the recipient_addresses()-iteration arm). + #[test] + fn verify_identity_credit_transfer_to_addresses_with_recipients_empty_proof_errors() { + let platform_version = PlatformVersion::latest(); + use dpp::address_funds::PlatformAddress; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; + use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; + use std::collections::BTreeMap; + + let mut recipient_addresses = BTreeMap::new(); + recipient_addresses.insert(PlatformAddress::P2pkh([21u8; 20]), 100u64); + + let st = StateTransition::IdentityCreditTransferToAddresses( + IdentityCreditTransferToAddressesTransition::V0( + IdentityCreditTransferToAddressesTransitionV0 { + identity_id: dpp::prelude::Identifier::from([22u8; 32]), + recipient_addresses, + ..Default::default() + }, + ), + ); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for ICTTA with recipients empty proof, got: {:?}", + result + ); + } + + // --- Non-empty, malformed proof bytes → decode / verification error. + // + // Exercises the decoder path inside grove-db for a non-empty but + // garbage proof, which should error out (either Error::GroveDB or + // Error::Proof). + #[test] + fn verify_identity_create_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + // A short, non-empty garbage proof triggers the decoder error path. + let bad_proof: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &bad_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for garbage proof bytes, got: {:?}", + result + ); + } + + // --- DataContractCreate with a larger garbage proof returns an error. + #[test] + fn verify_data_contract_create_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + let created_contract = + get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); + let contract = created_contract.data_contract(); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + let garbage_proof: [u8; 32] = [0x5A; 32]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &garbage_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for data contract create with garbage proof, got: {:?}", + result + ); + } + + // --- IdentityCreditTransfer: garbage proof returns error. + #[test] + fn verify_identity_credit_transfer_garbage_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( + IdentityCreditTransferTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + recipient_id: dpp::prelude::Identifier::random(), + amount: 50, + nonce: 1, + ..Default::default() + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + let garbage_proof = vec![0xFFu8; 20]; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &garbage_proof, + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for identity credit transfer with garbage proof, got: {:?}", + result + ); + } + + // --- IdentityCreate: garbage proof with a contract provider that errors + // internally does not affect the Identity path (provider is not queried). + // This sanity-checks that an IdentityCreate with an erroring provider + // still fails on the proof (not on the provider call). + #[test] + fn verify_identity_create_erroring_provider_ignored_for_identity_flow() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::identity_create_transition::IdentityCreateTransition; + use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; + let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0 { + identity_id: dpp::prelude::Identifier::random(), + ..Default::default() + }, + )); + + // Provider returns an error, but the IdentityCreate branch never + // consults the provider. The error surface is the empty proof. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "unused provider error".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected proof/grovedb error (not provider error), got: {:?}", + result + ); + } + + // --- MasternodeVote: provider callback errors → error is propagated. + #[test] + fn verify_masternode_vote_provider_errors_is_propagated() { + let platform_version = PlatformVersion::latest(); + use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; + use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; + use dpp::voting::vote_polls::VotePoll; + use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; + use dpp::voting::votes::resource_vote::ResourceVote; + use dpp::voting::votes::Vote; + + let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( + MasternodeVoteTransitionV0 { + pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), + voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), + vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { + vote_poll: VotePoll::ContestedDocumentResourceVotePoll( + ContestedDocumentResourceVotePoll { + contract_id: dpp::prelude::Identifier::from([7u8; 32]), + document_type_name: "some_type".to_string(), + index_name: "idx".to_string(), + index_values: vec![], + }, + ), + resource_vote_choice: ResourceVoteChoice::Abstain, + })), + nonce: 1, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + // Provider returns Err — the error should be propagated as-is. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "synthetic provider failure".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected provider error, got Ok"); + match err { + Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { + assert_eq!(msg, "synthetic provider failure"); + } + other => panic!("expected CorruptedDriveState, got: {:?}", other), + } + } + + // --- Batch with document transition: provider error is propagated. + #[test] + fn verify_batch_document_provider_error_is_propagated() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; + use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "x".to_string(), + data_contract_id: Default::default(), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { base }, + ))], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| { + Err(Error::Drive( + crate::error::drive::DriveError::CorruptedDriveState( + "synthetic batch provider failure".to_string(), + ), + )) + }; + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected provider error, got Ok"); + match err { + Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { + assert_eq!(msg, "synthetic batch provider failure"); + } + other => panic!("expected CorruptedDriveState, got: {:?}", other), + } + } + + // --- DataContractCreate empty proof + keeps_history=true: exercises the + // happy-path arm up to proof verification but with an empty proof so it + // errors out. + #[test] + fn verify_data_contract_create_keeps_history_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + // Build a contract with keeps_history=true + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + let contract = DataContract::V1(DataContractV1 { + id: dpp::prelude::Identifier::from([99u8; 32]), + version: 1, + owner_id: Default::default(), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: true, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens: Default::default(), + keywords: Vec::new(), + description: None, + }); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; + let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( + DataContractCreateTransitionV0 { + data_contract: data_contract_serialized, + identity_nonce: 0, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for keeps_history contract create with empty proof, got: {:?}", + result + ); + } + + // --- DataContractUpdate keeps_history=true + empty proof: errors out. + #[test] + fn verify_data_contract_update_keeps_history_empty_proof_returns_error() { + let platform_version = PlatformVersion::latest(); + use dpp::data_contract::config::v0::DataContractConfigV0; + use dpp::data_contract::config::DataContractConfig; + use dpp::data_contract::v1::DataContractV1; + use dpp::prelude::DataContract; + let contract = DataContract::V1(DataContractV1 { + id: dpp::prelude::Identifier::from([98u8; 32]), + version: 2, + owner_id: Default::default(), + document_types: Default::default(), + config: DataContractConfig::V0(DataContractConfigV0 { + can_be_deleted: false, + readonly: false, + keeps_history: true, + documents_keep_history_contract_default: false, + documents_mutable_contract_default: false, + documents_can_be_deleted_contract_default: false, + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + }), + schema_defs: None, + created_at: None, + updated_at: None, + created_at_block_height: None, + updated_at_block_height: None, + created_at_epoch: None, + updated_at_epoch: None, + groups: Default::default(), + tokens: Default::default(), + keywords: Vec::new(), + description: None, + }); + let data_contract_serialized: DataContractInSerializationFormat = contract + .clone() + .try_into_platform_versioned(platform_version) + .expect("expected to serialize contract"); + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; + let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( + DataContractUpdateTransitionV0 { + identity_contract_nonce: 1, + data_contract: data_contract_serialized, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }, + )); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + assert!( + matches!( + result, + Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) + ), + "expected error for keeps_history contract update with empty proof, got: {:?}", + result + ); + } + + // --- Batch V0 with a Document Create transition in contested status + // + unknown contract returns UnknownContract. The contested_status arm + // is a distinct branch that other tests don't cover. + #[test] + fn verify_batch_document_contested_create_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; + use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV0; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Default::default(), + identity_contract_nonce: 1, + document_type_name: "t".to_string(), + data_contract_id: Default::default(), + }); + + let create_transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { + base, + entropy: [0u8; 32], + data: Default::default(), + // Contested (prefunded) create → separate branch in verify() + prefunded_voting_balance: Some(("dash".to_string(), 1000)), + }); + + let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![DocumentTransition::Create(create_transition)], + ..Default::default() + })); + + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract, got Ok"); + assert!( + matches!( + err, + crate::error::Error::Proof(ProofError::UnknownContract(_)) + ), + "expected Error::Proof(UnknownContract), got: {:?}", + err + ); + } + + // --- Batch with a single Token transition + unknown contract returns + // UnknownContract error (covers the token transition branch). + #[test] + fn verify_batch_token_transition_unknown_contract_returns_error() { + let platform_version = PlatformVersion::latest(); + + use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::BatchTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: dpp::prelude::Identifier::from([55u8; 32]), + token_id: dpp::prelude::Identifier::from([66u8; 32]), + using_group_info: None, + }); + let token_transition = + TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: token_base, + burn_amount: 42, + public_note: None, + })); + + // V1 supports BatchedTransition::Token. V0 only supports document + // transitions — use V1 to include a token transition. + let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: dpp::prelude::Identifier::from([9u8; 32]), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })); + + // Provider returns None — triggers UnknownContract for the token. + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); + + let result = Drive::verify_state_transition_was_executed_with_proof( + &st, + &BlockInfo::default(), + &[], + known_contracts_provider_fn, + platform_version, + ); + + let err = result.expect_err("expected UnknownContract error, got Ok"); + match err { + crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { + assert!( + msg.contains("unknown contract") && msg.contains("token verification"), + "unexpected UnknownContract message: {msg}" + ); + } + other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), + } + } +} diff --git a/packages/rs-drive/tests/drive_storage_ops_coverage.rs b/packages/rs-drive/tests/drive_storage_ops_coverage.rs index 6db6ef4c404..6403e728fec 100644 --- a/packages/rs-drive/tests/drive_storage_ops_coverage.rs +++ b/packages/rs-drive/tests/drive_storage_ops_coverage.rs @@ -870,7 +870,6 @@ mod document_operation_tests { fn document_operation_add_type_construction() { let id = dpp::prelude::Identifier::new([2u8; 32]); let _op = DocumentOperationType::DeleteDocument { - deleter_id: None, document_id: id, contract_info: drive::util::object_size_info::DataContractInfo::DataContractId( dpp::prelude::Identifier::new([3u8; 32]), diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs index 4768a0722ea..53dab036184 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -80,8 +80,8 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ }, batch_state_transition: FeatureVersionBounds { min_version: 0, - max_version: 1, - default_current_version: 1, + max_version: 2, + default_current_version: 2, }, document_base_state_transition: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs index 1bde330b4db..c628f24ee2e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs @@ -3,6 +3,7 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DPPStateTransitionVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs new file mode 100644 index 00000000000..a670bc78b8b --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs @@ -0,0 +1,21 @@ +use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_versions::{ + DPPStateTransitionVersions, DocumentTransitionVersions, + DocumentsBatchTransitionValidationVersions, DocumentsBatchTransitionVersions, +}; + +/// Protocol 14 selects the batch validator that sees every batch wire format, +/// including format 2, whose document shell carries the erase kind. The +/// generation selected by earlier protocol versions only sees the shell of +/// formats 0 and 1 and remains unchanged. +pub const STATE_TRANSITION_VERSIONS_V4: DPPStateTransitionVersions = DPPStateTransitionVersions { + documents: DocumentTransitionVersions { + documents_batch_transition: DocumentsBatchTransitionVersions { + validation: DocumentsBatchTransitionValidationVersions { + find_duplicates_by_id: 0, + validate_base_structure: 1, + }, + }, + }, + ..STATE_TRANSITION_VERSIONS_V3 +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs index 4e00a02840d..e45ea1c4e70 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs @@ -12,6 +12,8 @@ pub struct DriveAbciQueryVersions { pub proofs_query: FeatureVersion, pub document_query: FeatureVersionBounds, pub document_history: FeatureVersionBounds, + /// Internal processing generation for the document-history wire v0 query. + pub document_history_processing: FeatureVersion, /// Per-helper version slots for internal v1-document-query /// routing helpers. Separate from `document_query` (which /// versions the wire surface) because the helper output is diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs index 4462a3cf293..f632a1396e4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs @@ -28,6 +28,7 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V0: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, + document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs index 1ab0fc4fae2..14f307e58c7 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs @@ -30,6 +30,7 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V1: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, + document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs index 036a932a5f6..edd50cdc3f7 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs @@ -41,6 +41,7 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V3: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, + document_history_processing: 1, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 2, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 192f4aacc46..aa876081619 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -225,9 +225,8 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_structure_validation: FeatureVersion, - /// The erase kind (PV14+); 0 in every earlier version table, where the - /// kind cannot appear because the wire gate rejects it. - pub document_erase_transition_structure_validation: FeatureVersion, + /// The erase kind, active from protocol version 14. + pub document_erase_transition_structure_validation: OptionalFeatureVersion, pub document_replace_transition_structure_validation: FeatureVersion, pub document_transfer_transition_structure_validation: FeatureVersion, pub document_purchase_transition_structure_validation: FeatureVersion, @@ -238,14 +237,12 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_state_validation: FeatureVersion, - /// The erase kind (PV14+); 0 in every earlier version table, where the - /// kind cannot appear because the wire gate rejects it. - pub document_erase_transition_state_validation: FeatureVersion, + /// The erase kind, active from protocol version 14. + pub document_erase_transition_state_validation: OptionalFeatureVersion, /// Versions `fetch_keep_history_document_lifecycle`, the stateful read /// that classifies a keep-history document as active, deleted, erasing or - /// absent. 0 in every table before protocol version 14, where no caller - /// exists. - pub fetch_keep_history_document_lifecycle: FeatureVersion, + /// absent. The helper is active from protocol version 14. + pub fetch_keep_history_document_lifecycle: OptionalFeatureVersion, pub document_replace_transition_state_validation: FeatureVersion, pub document_transfer_transition_state_validation: FeatureVersion, pub document_purchase_transition_state_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 2b7cdec0a5b..a5faa5a3acb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -127,7 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -136,8 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = document_create_transition_state_validation: 0, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 7b923eee2e3..a831222eba9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -117,9 +117,10 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, - advanced_structure: 0, - state: 0, - revision: 0, + advanced_structure: 1, + state: 1, + // Generation 1 reads the nonces of a batch of any wire format. + revision: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition // fee accounting fixes. This single field gates multiple // related billing changes so they all activate together at @@ -143,7 +144,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // * T3 — DashPay data trigger recipient identity-balance // fetch cost (switched to `fetch_identity_balance_with_costs`). // * T4 — withdrawals data trigger `query_documents` cost. - transform_into_action: 1, + transform_into_action: 2, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit // a `BumpIdentityDataContractNonce` action so the user pays @@ -156,7 +157,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // their query_documents calls. v0 stays for PV11 chain // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). - fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + // Generation 2 keeps generation 1's billing and reads the + // transitions of any batch wire format. + fetch_documents_for_transitions_knowing_contract_and_document_type: 2, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { // PROTOCOL_VERSION_14: v2 adds DashPay `profile` @@ -183,7 +186,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = reject_data_trigger: 0, }, }, - is_allowed: 0, + // Generation 1 reads a batch of any wire format. + is_allowed: 1, document_create_transition_structure_validation: 1, // Keep-history types take part in the document lifecycle: a delete // removes the current pointer and its index references while the @@ -191,7 +195,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // original internal-error outcome. document_delete_transition_structure_validation: 1, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: Some(0), document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -203,8 +207,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // error rather than a second removal. document_delete_transition_state_validation: 1, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: Some(0), + fetch_keep_history_document_lifecycle: Some(0), document_replace_transition_state_validation: 1, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index ca2713f5ff9..46edb3178c1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -127,7 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -136,8 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index c32d2f2b13a..95ef1fcccda 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -127,7 +127,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -136,8 +136,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index f98581dc9ba..8f962d14a87 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -130,7 +130,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -139,8 +139,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index c8efc2c2d72..dcbf2db5503 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -131,7 +131,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -140,8 +140,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 5c5ce7aace1..33ead66ce64 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -134,7 +134,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -143,8 +143,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 88ab4148e91..c9befa9af48 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -128,7 +128,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -137,8 +137,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 1a143aae9e2..83a0b29efc2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -182,7 +182,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -191,8 +191,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 1167b71b49e..1de62df75b3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -178,7 +178,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = document_create_transition_structure_validation: 0, document_delete_transition_structure_validation: 0, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: 0, + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, @@ -187,8 +187,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = document_create_transition_state_validation: 1, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: 0, - fetch_keep_history_document_lifecycle: 0, + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 0, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, 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 799d211c1e7..82429ab676e 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 @@ -1,4 +1,4 @@ -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v2; @@ -40,9 +40,8 @@ pub struct DriveDocumentQueryMethodVersions { pub primary_key_path_query: FeatureVersion, /// Reads the lifecycle record of one keep-history document and, when the /// record says the document is deleted, its newest retained revision. - /// Present in every version table so the slot exists; unreachable before - /// protocol version 14, which introduces the lifecycle tree. - pub fetch_document_lifecycle: FeatureVersion, + /// Absent before protocol version 14, which introduces the lifecycle tree. + pub fetch_document_lifecycle: OptionalFeatureVersion, /// Mode-detection routing table for `SELECT COUNT` queries. /// Versioned because the routing table is consensus-relevant on /// the query surface — a future protocol version that changes @@ -162,12 +161,11 @@ pub struct DriveDocumentDeleteMethodVersions { pub delete_index_only_document_for_contract: FeatureVersion, /// Removes a bounded chunk of the retained revisions of a deleted /// keep-history document, dropping the history subtree and the lifecycle - /// record with the terminal chunk. Present in every version table so the - /// slot exists; unreachable before protocol version 14. - pub erase_document_for_contract_operations: FeatureVersion, + /// record with the terminal chunk. Absent before protocol version 14. + pub erase_document_for_contract_operations: OptionalFeatureVersion, /// Estimation layers for the erase chunk. Same dormancy as /// `erase_document_for_contract_operations`. - pub add_estimation_costs_for_erase_document: FeatureVersion, + pub add_estimation_costs_for_erase_document: OptionalFeatureVersion, } #[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 0606a70e8ea..63d9e9fa116 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 @@ -18,7 +18,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, - fetch_document_lifecycle: 0, + fetch_document_lifecycle: None, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -42,8 +42,8 @@ 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, - erase_document_for_contract_operations: 0, - add_estimation_costs_for_erase_document: 0, + erase_document_for_contract_operations: None, + add_estimation_costs_for_erase_document: None, }, 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 d329e1e8124..14e5a08796c 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 @@ -20,7 +20,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, - fetch_document_lifecycle: 0, + fetch_document_lifecycle: None, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -44,8 +44,8 @@ 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, - erase_document_for_contract_operations: 0, - add_estimation_costs_for_erase_document: 0, + erase_document_for_contract_operations: None, + add_estimation_costs_for_erase_document: None, }, 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 5424d148305..462b1adb210 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 @@ -30,7 +30,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, - fetch_document_lifecycle: 0, + fetch_document_lifecycle: None, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -59,8 +59,8 @@ 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, - erase_document_for_contract_operations: 0, - add_estimation_costs_for_erase_document: 0, + erase_document_for_contract_operations: None, + add_estimation_costs_for_erase_document: None, }, 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 ed8d022de35..1f856f683b0 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 @@ -84,7 +84,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = primary_key_path_query: 0, // The lifecycle tree is introduced with the delete and erase // lifecycle; the read exists only from protocol version 14. - fetch_document_lifecycle: 0, + fetch_document_lifecycle: Some(0), detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -99,9 +99,9 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = // pointers, not document items, so its layer sizes come from the // reference shape. add_estimation_costs_for_remove_document_to_primary_storage: 1, - delete_document_for_contract: 0, - delete_document_for_contract_id: 0, - delete_document_for_contract_apply_and_add_to_operations: 0, + delete_document_for_contract: 1, + delete_document_for_contract_id: 1, + delete_document_for_contract_apply_and_add_to_operations: 1, // v1 at protocol v14: the stateless delete of a keep-history // primary entry is sized as a reference. remove_document_from_primary_storage: 1, @@ -111,16 +111,16 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = remove_reference_for_index_level_for_contract_operations: 1, remove_indices_for_index_level_for_contract_operations: 2, remove_indices_for_top_index_level_for_contract_operations: 2, - delete_document_for_contract_id_with_named_type_operations: 0, - delete_document_for_contract_with_named_type_operations: 0, + delete_document_for_contract_id_with_named_type_operations: 1, + delete_document_for_contract_with_named_type_operations: 1, // v1 at protocol v14: keep-history documents lose their current // pointer and index references and gain a lifecycle record; the // non-history path is delegated to v0 unchanged. delete_document_for_contract_operations: 1, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, - erase_document_for_contract_operations: 0, - add_estimation_costs_for_erase_document: 0, + erase_document_for_contract_operations: Some(0), + add_estimation_costs_for_erase_document: Some(0), }, insert: DriveDocumentInsertMethodVersions { add_document: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index f07f67eaafd..0b8e86a7774 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -4,7 +4,7 @@ pub mod v3; pub mod v4; use crate::version::drive_versions::DriveDataContractOperationMethodVersions; -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; #[derive(Clone, Debug, Default)] pub struct DriveStateTransitionMethodVersions { @@ -32,9 +32,8 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub document_create_transition: FeatureVersion, pub document_delete_transition: FeatureVersion, pub document_index_only_delete_transition: FeatureVersion, - /// The erase kind (protocol version 14+); 0 in every table, where the kind - /// either cannot appear or has only one conversion. - pub document_erase_transition: FeatureVersion, + /// The erase kind, absent before protocol version 14. + pub document_erase_transition: OptionalFeatureVersion, pub document_purchase_transition: FeatureVersion, pub document_replace_transition: FeatureVersion, pub document_transfer_transition: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs index 69021c67d9b..da4b5aef71f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs @@ -20,7 +20,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V1: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, - document_erase_transition: 0, + document_erase_transition: None, document_purchase_transition: 0, document_replace_transition: 0, document_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs index 8922224eb44..83d23cf2080 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs @@ -21,7 +21,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V2: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, - document_erase_transition: 0, + document_erase_transition: None, document_purchase_transition: 0, document_replace_transition: 0, document_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs index 74b1c4a361b..12fc5663a3c 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs @@ -21,7 +21,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3: DriveStateTransitionMethodV document_create_transition: 0, document_delete_transition: 0, document_index_only_delete_transition: 0, - document_erase_transition: 0, + document_erase_transition: None, // PROTOCOL_VERSION_13: v1 rewrites a transferred or purchased // DPNS domain document's `records.identity` to the new owner // so the username resolves to the buyer. v0 stays for diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index f02b720ee86..10f7ce05c4a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -19,9 +19,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV data_contract_create_transition: 0, data_contract_update_transition: 0, document_create_transition: 0, - document_delete_transition: 0, + document_delete_transition: 1, document_index_only_delete_transition: 0, - document_erase_transition: 0, + document_erase_transition: Some(0), // PROTOCOL_VERSION_13: v1 rewrites a transferred or purchased // DPNS domain document's `records.identity` to the new owner // so the username resolves to the buyer. v0 stays for @@ -33,7 +33,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV token_burn_transition: 0, token_mint_transition: 0, token_transfer_transition: 0, - documents_batch_transition: 0, + // Generation 1 consumes batch action format 1, whose items + // may include the erase of a keep-history document. + documents_batch_transition: 1, identity_create_transition: 0, identity_create_from_addresses_transition: 0, identity_credit_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs index 013b30fda61..34de2e862db 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs @@ -1,5 +1,8 @@ use super::v2::DRIVE_VERIFY_METHOD_VERSIONS_V2; -use super::{DriveVerifyDocumentMethodVersions, DriveVerifyMethodVersions}; +use super::{ + DriveVerifyDocumentMethodVersions, DriveVerifyMethodVersions, + DriveVerifyStateTransitionMethodVersions, +}; /// Verification of composite document history and its independent metadata proof, /// and the cursor lookup of a proved page in the protocol 15 keep-history layout. @@ -9,5 +12,8 @@ pub const DRIVE_VERIFY_METHOD_VERSIONS_V3: DriveVerifyMethodVersions = DriveVeri verify_start_at_document_in_proof: 1, ..DRIVE_VERIFY_METHOD_VERSIONS_V2.document }, + state_transition: DriveVerifyStateTransitionMethodVersions { + verify_state_transition_was_executed_with_proof: 1, + }, ..DRIVE_VERIFY_METHOD_VERSIONS_V2 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index fdb7aa5aaca..9da17e7e4e8 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -63,7 +63,9 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { prove: DriveProveMethodVersions { prove_elements: 0, prove_multiple_state_transition_results: 0, - prove_state_transition: 0, + // Generation 1 proves a batch of any wire format, including + // format 2, whose document shell carries the erase kind. + prove_state_transition: 1, }, balances: DriveBalancesMethodVersions { add_to_system_credits: 0, 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 44a45107bc1..2efa8fa033b 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -178,6 +178,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_version: 0, default_current_version: 0, }, + document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index a321869ce94..273962ac0df 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -9,7 +9,7 @@ use crate::version::dpp_versions::dpp_method_versions::v3::DPP_METHOD_VERSIONS_V use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; @@ -298,7 +298,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, - state_transitions: STATE_TRANSITION_VERSIONS_V3, + state_transitions: STATE_TRANSITION_VERSIONS_V4, contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords; validate_structure_interval v1 rejects a zero epoch interval document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties identity_versions: IDENTITY_VERSIONS_V1, diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index 53184d151f2..736c490eca8 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -69,6 +69,56 @@ fn should_preserve_released_keep_history_validation_versions() { 0, "the keep-history delete branch must not be selected at protocol {protocol}" ); + assert_eq!( + version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version, + 1, + "batch wire versions at protocol {protocol}" + ); + assert_eq!( + version + .dpp + .state_transitions + .documents + .documents_batch_transition + .validation + .validate_base_structure, + 0, + "batch structure validation at protocol {protocol}" + ); + assert_eq!( + version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .document_delete_transition, + 0, + "delete action conversion at protocol {protocol}" + ); + assert_eq!( + version + .drive + .methods + .verify + .state_transition + .verify_state_transition_was_executed_with_proof, + 0, + "state transition proof verification at protocol {protocol}" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action, + 1, + "batch action transform at protocol {protocol}" + ); } } @@ -115,6 +165,85 @@ fn should_activate_keep_history_validation_at_protocol_14() { 1, "protocol 14 selects the keep-history delete branch" ); + assert_eq!( + version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version, + 2, + "protocol 14 admits BatchTransitionV2" + ); + assert_eq!( + version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + 2, + "protocol 14 constructs BatchTransitionV2" + ); + assert_eq!( + version + .dpp + .state_transitions + .documents + .documents_batch_transition + .validation + .validate_base_structure, + 1, + "protocol 14 selects structure validation that admits erase only in V2" + ); + assert_eq!( + version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .document_delete_transition, + 1, + "protocol 14 selects the lifecycle-specific delete operation" + ); + assert_eq!( + version + .drive + .methods + .verify + .state_transition + .verify_state_transition_was_executed_with_proof, + 1, + "protocol 14 selects erase-aware proof verification" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action, + 2, + "protocol 14 selects the erase-aware transformer" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .advanced_structure, + 1, + "protocol 14 selects erase-aware advanced validation" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .state, + 1, + "protocol 14 selects erase-aware state validation" + ); let bounds = version .dpp @@ -145,6 +274,7 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { let Ok(version) = PlatformVersion::get(protocol) else { continue; }; + let expected_erase_version = (protocol >= 14).then_some(0); assert_eq!( version .drive @@ -152,7 +282,7 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { .document .query .fetch_document_lifecycle, - 0, + expected_erase_version, "lifecycle read at protocol {protocol}" ); assert_eq!( @@ -162,9 +292,29 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { .document .delete .erase_document_for_contract_operations, - 0, + expected_erase_version, "erase operation at protocol {protocol}" ); + assert_eq!( + version + .drive + .methods + .document + .delete + .add_estimation_costs_for_erase_document, + expected_erase_version, + "erase estimation at protocol {protocol}" + ); + assert_eq!( + version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .document_erase_transition, + expected_erase_version, + "erase action conversion at protocol {protocol}" + ); assert_eq!( version .drive_abci @@ -172,7 +322,7 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { .state_transitions .batch_state_transition .document_erase_transition_structure_validation, - 0, + expected_erase_version, "erase structure validation at protocol {protocol}" ); assert_eq!( @@ -182,8 +332,18 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { .state_transitions .batch_state_transition .document_erase_transition_state_validation, - 0, + expected_erase_version, "erase state validation at protocol {protocol}" ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_keep_history_document_lifecycle, + expected_erase_version, + "lifecycle state read at protocol {protocol}" + ); } } diff --git a/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs b/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs index 2460dfaf975..2e128df29db 100644 --- a/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs +++ b/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs @@ -21,9 +21,9 @@ use std::borrow::Cow; use std::slice; use dpp::serialization::PlatformDeserializableUntrusted; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRefV1; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::state_transition::batch_transition::token_direct_purchase_transition::v0::v0_methods::TokenDirectPurchaseTransitionV0Methods; @@ -172,7 +172,7 @@ fn project_parsed_token_direct_purchase( batch: &BatchTransition, batch_description: &str, ) -> Result { - let transitions_len = batch.transitions_len(); + let transitions_len = batch.transitions_len_v1(); if transitions_len != 1 { return Err(PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, @@ -184,8 +184,8 @@ fn project_parsed_token_direct_purchase( )); } - let Some(BatchedTransitionRef::Token(TokenTransition::DirectPurchase(purchase))) = - batch.first_transition() + let Some(BatchedTransitionRefV1::Token(TokenTransition::DirectPurchase(purchase))) = + batch.first_transition_v1() else { return Err(PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index 6dafdfa83ef..4dbc6130810 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -8,16 +8,14 @@ use dpp::document::{Document, INITIAL_REVISION}; use dpp::identity::signer::Signer; use dpp::identity::IdentityPublicKey; use dpp::prelude::UserFeeIncrease; -use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; use dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; use dpp::version::PlatformVersion; use dpp::ProtocolError; -use drive::drive::document::history::{ - DocumentHistoryLifecycle, DocumentHistorySelector, DocumentHistoryState, -}; +use drive::query::document_history_drive_query::{DocumentHistoryFilter, DocumentHistoryLifecycle}; use std::sync::Arc; /// A builder to configure and broadcast document erase transitions. @@ -143,8 +141,8 @@ impl DocumentEraseTransitionBuilder { identity_contract_nonce, user_fee_increase, signer, - platform_version, creation_options, + platform_version, ) .await?; @@ -187,10 +185,10 @@ impl DocumentEraseTransitionBuilder { let batch_feature_version = creation_options .and_then(|options| options.batch_feature_version) .unwrap_or(serialization.batch_state_transition.default_current_version); - if !matches!(batch_feature_version, 0 | 1) { + if batch_feature_version != 2 { return Err(Error::Protocol(ProtocolError::UnknownVersionMismatch { method: "DocumentEraseTransitionBuilder::sign".to_string(), - known_versions: vec![0, 1], + known_versions: vec![2], received: batch_feature_version, })); } @@ -204,6 +202,20 @@ impl DocumentEraseTransitionBuilder { received: method_feature_version, })); } + let base_feature_version = creation_options + .and_then(|options| options.base_feature_version) + .unwrap_or( + serialization + .document_base_state_transition + .default_current_version, + ); + if !matches!(base_feature_version, 0 | 1) { + return Err(Error::Protocol(ProtocolError::UnknownVersionMismatch { + method: "DocumentEraseTransitionBuilder::sign".to_string(), + known_versions: vec![0, 1], + received: base_feature_version, + })); + } Ok(()) } } @@ -252,6 +264,12 @@ fn erase_observation(result: StateTransitionProofResult) -> Result, +) -> Option { + history.and_then(|history| history.lifecycle) +} + impl Sdk { /// Erases a chunk of the retained revisions of an already deleted document. /// @@ -290,13 +308,15 @@ impl Sdk { /// Deliberately named apart from the erase and delete calls: the answer /// describes committed state at the moment of the read, not the outcome of /// any particular transition. Another erase, or a re-create of the same id, - /// may land between a transition and this read. + /// may land between a transition and this read. `None` means the queried + /// storage/proof generation does not authenticate lifecycle metadata; it + /// must not be interpreted as an authenticated absent state. pub async fn document_current_lifecycle( &self, data_contract_id: Identifier, document_type_name: String, document_id: Identifier, - ) -> Result { + ) -> Result, Error> { use dash_platform_queries::documents::document_history_query::DocumentHistoryQuery; use drive_proof_verifier::types::DocumentHistory; @@ -308,25 +328,20 @@ impl Sdk { document_id, // The oldest retained revision, if any: the page is not the // point, the metadata alongside it is. - selector: DocumentHistorySelector::StartAtTime(0), + filter: DocumentHistoryFilter::StartAtTime(0), limit: Some(1), }, ) .await?; - Ok(history - .and_then(|history| history.lifecycle) - .unwrap_or(DocumentHistoryLifecycle { - state: DocumentHistoryState::Absent, - remaining_revisions: 0, - times: Default::default(), - })) + Ok(lifecycle_from_history(history)) } } #[cfg(test)] mod tests { use super::*; + use drive::query::document_history_drive_query::DocumentHistoryState; use std::collections::BTreeMap; /// The erase reads its observation out of exactly the result the @@ -353,6 +368,32 @@ mod tests { .expect_err("only a document result can describe an erase"); } + #[test] + fn should_not_turn_unavailable_lifecycle_into_absent() { + use drive_proof_verifier::types::DocumentHistory; + + assert_eq!(lifecycle_from_history(None), None); + assert_eq!( + lifecycle_from_history(Some(DocumentHistory { + entries: vec![], + lifecycle: None, + })), + None + ); + let absent = DocumentHistoryLifecycle { + state: DocumentHistoryState::Absent, + remaining_revisions: 0, + times: Default::default(), + }; + assert_eq!( + lifecycle_from_history(Some(DocumentHistory { + entries: vec![], + lifecycle: Some(absent.clone()), + })), + Some(absent) + ); + } + #[cfg(feature = "mocks")] fn builder_with(settings: Option) -> DocumentEraseTransitionBuilder { let contract = dpp::tests::fixtures::get_dashpay_contract_fixture( @@ -379,7 +420,7 @@ mod tests { #[test] fn should_sign_with_the_settings_fee_increase_unless_set_explicitly() { let options = StateTransitionCreationOptions { - batch_feature_version: Some(1), + batch_feature_version: Some(2), ..Default::default() }; let settings = PutSettings { @@ -411,9 +452,17 @@ mod tests { assert!(error .to_string() .contains("erase transitions do not exist at this platform version")); - let current = PlatformVersion::get(14).unwrap(); + let current = PlatformVersion::latest(); DocumentEraseTransitionBuilder::check_erase_is_constructible(None, current) .expect("protocol 14 constructs erases"); + for unsupported in [0, 1] { + let options = StateTransitionCreationOptions { + batch_feature_version: Some(unsupported), + ..Default::default() + }; + DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&options), current) + .expect_err("a pre-erase batch generation is refused before nonce reservation"); + } let unknown_batch = StateTransitionCreationOptions { batch_feature_version: Some(9), ..Default::default() @@ -424,7 +473,16 @@ mod tests { method_feature_version: Some(1), ..Default::default() }; - DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&unknown_method), current) - .expect_err("an unknown erase method version is refused before any nonce is reserved"); + DocumentEraseTransitionBuilder::check_erase_is_constructible( + Some(&unknown_method), + current, + ) + .expect_err("an unknown erase method version is refused before any nonce is reserved"); + let unknown_base = StateTransitionCreationOptions { + base_feature_version: Some(9), + ..Default::default() + }; + DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&unknown_base), current) + .expect_err("an unknown erase base version is refused before any nonce is reserved"); } } diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs index 1a5acf13908..89e0e5df571 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs @@ -68,7 +68,6 @@ impl DocumentTransitionWasm { DocumentTransition::Transfer(_) => JsValue::null(), DocumentTransition::UpdatePrice(_) => JsValue::null(), DocumentTransition::Purchase(_) => JsValue::null(), - DocumentTransition::Erase(_) => JsValue::null(), DocumentTransition::IndexOnlyDelete(index_only_delete) => { let json_value = index_only_delete.data().to_json_value().unwrap(); json_value @@ -119,7 +118,6 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(update_price) => Some(update_price.price()), DocumentTransition::Purchase(purchase) => Some(purchase.price()), DocumentTransition::IndexOnlyDelete(_) => None, - DocumentTransition::Erase(_) => None, } } @@ -133,7 +131,6 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, - DocumentTransition::Erase(_) => None, } } diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index cce5e70e488..2a0b45775ae 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -21,9 +21,11 @@ use dpp::state_transition::StateTransition::{ IdentityCreditWithdrawal, IdentityUpdate, MasternodeVote, ShieldFromIdentity, }; use dpp::state_transition::batch_transition::BatchTransition; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionV1, +}; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; @@ -399,6 +401,14 @@ impl StateTransitionWasm { Some(token_batch.identity_contract_nonce()) } }, + BatchTransition::V2(v2) => match v2.transitions.first()? { + BatchedTransitionV1::Document(doc_batch) => { + Some(doc_batch.identity_contract_nonce()) + } + BatchedTransitionV1::Token(token_batch) => { + Some(token_batch.identity_contract_nonce()) + } + }, }, StateTransition::IdentityCreate(_) => None, IdentityTopUp(_) => None, @@ -531,6 +541,11 @@ impl StateTransitionWasm { BatchTransition::V1(v1) } + BatchTransition::V2(mut v2) => { + v2.owner_id = owner_id; + + BatchTransition::V2(v2) + } }; self.0 = Batch(batch); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs index a1ee2efe8f2..803ace4dab0 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs @@ -11,10 +11,14 @@ use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; use dpp::platform_value::string_encoding::{decode, encode}; use dpp::prelude::UserFeeIncrease; use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionV1, +}; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; -use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV1}; +use dpp::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV1, BatchTransitionV2, +}; use dpp::state_transition::{ StateTransition, StateTransitionIdentitySigned, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, @@ -76,7 +80,7 @@ impl From for BatchTransition { fn convert_array_to_vec_batched( batched_transitions: &js_sys::Array, -) -> WasmDppResult> { +) -> WasmDppResult> { let mut transitions = Vec::with_capacity(batched_transitions.length() as usize); for batched_transition in batched_transitions.iter() { @@ -84,12 +88,25 @@ fn convert_array_to_vec_batched( .to_wasm::("BatchedTransition")? .clone(); - transitions.push(BatchedTransition::from(transition)); + transitions.push(BatchedTransitionV1::from(transition)); } Ok(transitions) } +/// The same transitions in the shell of batch formats 0 and 1, when every one +/// of them fits there; an erase does not. +fn downcast_batched_transitions( + transitions: Vec, +) -> Result, Vec> { + transitions + .iter() + .cloned() + .map(BatchedTransition::try_from) + .collect::, _>>() + .map_err(|_| transitions) +} + #[wasm_bindgen(js_class = BatchTransition)] impl BatchTransitionWasm { #[wasm_bindgen(js_name = "fromBatchedTransitions")] @@ -99,22 +116,34 @@ impl BatchTransitionWasm { #[wasm_bindgen(js_name = "userFeeIncrease")] user_fee_increase: UserFeeIncrease, ) -> WasmDppResult { let transitions = convert_array_to_vec_batched(batched_transitions)?; + let owner_id = owner_id.try_into()?; - Ok(BatchTransitionWasm(BatchTransition::V1( - BatchTransitionV1 { - owner_id: owner_id.try_into()?, + // Batch format 1 carries every transition but an erase; an erase needs + // format 2, which only protocol version 14 and later accept. + let batch = match downcast_batched_transitions(transitions) { + Ok(transitions) => BatchTransition::V1(BatchTransitionV1 { + owner_id, + transitions, + user_fee_increase, + signature_public_key_id: 0u32, + signature: BinaryData::default(), + }), + Err(transitions) => BatchTransition::V2(BatchTransitionV2 { + owner_id, transitions, user_fee_increase, signature_public_key_id: 0u32, signature: BinaryData::default(), - }, - ))) + }), + }; + + Ok(BatchTransitionWasm(batch)) } #[wasm_bindgen(getter = "transitions")] pub fn batched_transitions(&self) -> Vec { self.0 - .transitions_iter() + .transitions_iter_v1() .map(|transition| BatchedTransitionWasm::from(transition.to_owned_transition())) .collect() } @@ -126,7 +155,20 @@ impl BatchTransitionWasm { ) -> WasmDppResult<()> { let transitions = convert_array_to_vec_batched(batched_transitions)?; - self.0.set_transitions(transitions); + match (&mut self.0, downcast_batched_transitions(transitions)) { + (BatchTransition::V2(batch), Ok(transitions)) => { + batch.transitions = transitions.into_iter().map(Into::into).collect(); + } + (BatchTransition::V2(batch), Err(transitions)) => { + batch.transitions = transitions; + } + (batch, Ok(transitions)) => batch.set_transitions(transitions), + (_, Err(_)) => { + return Err(WasmDppError::invalid_argument( + "an erase transition needs batch transition format 2", + )); + } + } Ok(()) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs index 3fbbf9b8065..d7c70037e76 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs @@ -6,13 +6,13 @@ use crate::state_transitions::batch::document_transition::DocumentTransitionWasm use crate::state_transitions::batch::token_transition::TokenTransitionWasm; use crate::utils::get_class_type; use dpp::prelude::Identifier; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; -use dpp::state_transition::batch_transition::batched_transition::document_transition::{ - DocumentTransition, DocumentTransitionV0Methods, -}; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ TokenTransition, TokenTransitionV0Methods, }; +use dpp::state_transition::batch_transition::batched_transition::{ + BatchedTransition, BatchedTransitionV1, DocumentTransitionV1, +}; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; @@ -34,17 +34,26 @@ impl_from_for_extern_type!( TokenTransitionWasm ); +/// Wraps the batched transition shell of batch format 2, which knows every +/// kind the older batch formats carry plus the erase kind, so one class +/// mirrors a batched transition of any batch format. #[derive(Debug, Clone, PartialEq)] #[wasm_bindgen(js_name = "BatchedTransition")] -pub struct BatchedTransitionWasm(BatchedTransition); +pub struct BatchedTransitionWasm(BatchedTransitionV1); + +impl From for BatchedTransitionWasm { + fn from(v: BatchedTransitionV1) -> Self { + BatchedTransitionWasm(v) + } +} impl From for BatchedTransitionWasm { fn from(v: BatchedTransition) -> Self { - BatchedTransitionWasm(v) + BatchedTransitionWasm(v.into()) } } -impl From for BatchedTransition { +impl From for BatchedTransitionV1 { fn from(v: BatchedTransitionWasm) -> Self { v.0 } @@ -62,11 +71,11 @@ impl BatchedTransitionWasm { } match get_class_type(&js_transition)?.as_str() { - "TokenTransition" => Ok(BatchedTransitionWasm::from(BatchedTransition::from( + "TokenTransition" => Ok(BatchedTransitionWasm(BatchedTransitionV1::Token( TokenTransition::from(TokenTransitionWasm::try_from(&js_transition)?), ))), - "DocumentTransition" => Ok(BatchedTransitionWasm(BatchedTransition::Document( - DocumentTransition::from(DocumentTransitionWasm::try_from(&js_transition)?), + "DocumentTransition" => Ok(BatchedTransitionWasm(BatchedTransitionV1::Document( + DocumentTransitionV1::from(DocumentTransitionWasm::try_from(&js_transition)?), ))), _ => Err(WasmDppError::invalid_argument("Invalid transition type")), } @@ -75,10 +84,10 @@ impl BatchedTransitionWasm { #[wasm_bindgen(js_name = "toTransition")] pub fn to_transition(&self) -> BatchedTransitionLikeJs { match &self.0 { - BatchedTransition::Document(document_transition) => { + BatchedTransitionV1::Document(document_transition) => { DocumentTransitionWasm::from(document_transition.clone()).into() } - BatchedTransition::Token(token_transition) => { + BatchedTransitionV1::Token(token_transition) => { TokenTransitionWasm::from(token_transition.clone()).into() } } @@ -87,10 +96,10 @@ impl BatchedTransitionWasm { #[wasm_bindgen(getter = "dataContractId")] pub fn data_contract_id(&self) -> IdentifierWasm { match self.0.clone() { - BatchedTransition::Document(document_transition) => { + BatchedTransitionV1::Document(document_transition) => { document_transition.data_contract_id().into() } - BatchedTransition::Token(token_transition) => { + BatchedTransitionV1::Token(token_transition) => { token_transition.data_contract_id().into() } } @@ -104,15 +113,15 @@ impl BatchedTransitionWasm { let contract_id: Identifier = contract_id.try_into()?; self.0 = match self.0.clone() { - BatchedTransition::Document(mut document_transition) => { + BatchedTransitionV1::Document(mut document_transition) => { document_transition.set_data_contract_id(contract_id); - BatchedTransition::Document(document_transition) + BatchedTransitionV1::Document(document_transition) } - BatchedTransition::Token(mut token_transition) => { + BatchedTransitionV1::Token(mut token_transition) => { token_transition.set_data_contract_id(contract_id); - BatchedTransition::Token(token_transition) + BatchedTransitionV1::Token(token_transition) } }; diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs index 1e5cce18f0b..03864de991e 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs @@ -13,23 +13,33 @@ use dpp::prelude::{IdentityNonce, Revision}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::batched_transition::document_transition_action_type::{ DocumentTransitionActionType, DocumentTransitionActionTypeGetter, }; use crate::utils::try_to_u64; use wasm_bindgen::prelude::wasm_bindgen; +/// Wraps the document transition shell of batch format 2, which knows every +/// kind the older batch formats carry plus the erase kind, so one class +/// mirrors a document transition of any batch format. #[derive(Clone)] #[wasm_bindgen(js_name = "DocumentTransition")] -pub struct DocumentTransitionWasm(DocumentTransition); +pub struct DocumentTransitionWasm(DocumentTransitionV1); + +impl From for DocumentTransitionWasm { + fn from(transition: DocumentTransitionV1) -> Self { + DocumentTransitionWasm(transition) + } +} impl From for DocumentTransitionWasm { fn from(transition: DocumentTransition) -> Self { - DocumentTransitionWasm(transition) + DocumentTransitionWasm(transition.into()) } } -impl From for DocumentTransition { +impl From for DocumentTransitionV1 { fn from(transition: DocumentTransitionWasm) -> Self { transition.0 } @@ -90,7 +100,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "createTransition")] pub fn create_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::Create(create) => Ok(DocumentCreateTransitionWasm::from(create)), + DocumentTransitionV1::Create(create) => Ok(DocumentCreateTransitionWasm::from(create)), _ => Err(WasmDppError::invalid_argument( "Document transition is not a create transition", )), @@ -100,7 +110,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "replaceTransition")] pub fn replace_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::Replace(replace) => { + DocumentTransitionV1::Replace(replace) => { Ok(DocumentReplaceTransitionWasm::from(replace)) } _ => Err(WasmDppError::invalid_argument( @@ -112,7 +122,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "deleteTransition")] pub fn delete_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::Delete(delete) => Ok(DocumentDeleteTransitionWasm::from(delete)), + DocumentTransitionV1::Delete(delete) => Ok(DocumentDeleteTransitionWasm::from(delete)), _ => Err(WasmDppError::invalid_argument( "Document transition is not a delete transition", )), @@ -122,7 +132,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "purchaseTransition")] pub fn purchase_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::Purchase(purchase) => { + DocumentTransitionV1::Purchase(purchase) => { Ok(DocumentPurchaseTransitionWasm::from(purchase)) } _ => Err(WasmDppError::invalid_argument( @@ -134,7 +144,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "transferTransition")] pub fn transfer_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::Transfer(transfer) => { + DocumentTransitionV1::Transfer(transfer) => { Ok(DocumentTransferTransitionWasm::from(transfer)) } _ => Err(WasmDppError::invalid_argument( @@ -146,7 +156,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "updatePriceTransition")] pub fn update_price_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransition::UpdatePrice(update_price) => { + DocumentTransitionV1::UpdatePrice(update_price) => { Ok(DocumentUpdatePriceTransitionWasm::from(update_price)) } _ => Err(WasmDppError::invalid_argument( diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs index 7a79a1e7e94..d03c05ff096 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs @@ -12,7 +12,7 @@ use crate::utils::{ try_vec_to_fixed_bytes, ToSerdeJSONExt, }; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; use dpp::state_transition::batch_transition::DocumentCreateTransition; @@ -149,7 +149,7 @@ impl DocumentCreateTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs index 22935a38988..c80c72a7e3c 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs @@ -7,7 +7,7 @@ use crate::state_transitions::batch::generators::generate_delete_transition; use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::DocumentDeleteTransition; use wasm_bindgen::prelude::wasm_bindgen; @@ -74,7 +74,7 @@ impl DocumentDeleteTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs index 6fa0745c34b..7c520145e59 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs @@ -9,7 +9,7 @@ use crate::utils::{try_from_options, try_from_options_optional, try_from_options use dpp::fee::Credits; use dpp::prelude::{IdentityNonce, Revision}; use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::batched_transition::DocumentPurchaseTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use wasm_bindgen::prelude::wasm_bindgen; @@ -114,7 +114,7 @@ impl DocumentPurchaseTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs index ec9d4ace0a0..c55dd7e04db 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs @@ -8,7 +8,7 @@ use crate::state_transitions::batch::generators::generate_replace_transition; use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64, ToSerdeJSONExt}; use dpp::prelude::{IdentityNonce, Revision}; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; use dpp::state_transition::batch_transition::DocumentReplaceTransition; @@ -115,7 +115,7 @@ impl DocumentReplaceTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs index a80a4ea85dd..79cb6a0dc20 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs @@ -9,7 +9,7 @@ use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::prelude::IdentityNonce; use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::batched_transition::DocumentTransferTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use wasm_bindgen::prelude::wasm_bindgen; @@ -101,7 +101,7 @@ impl DocumentTransferTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs index e60dd1350f8..6051961bdd4 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs @@ -8,7 +8,7 @@ use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::fee::Credits; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::DocumentUpdatePriceTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; @@ -94,7 +94,7 @@ impl DocumentUpdatePriceTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransition::from(self.0.clone()); + let rs_transition = DocumentTransitionV1::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 1b817755b86..fbd104993ea 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -193,7 +193,8 @@ export interface DocumentHistoryQuery { } export interface DocumentHistoryResult { entries: { timeMs: bigint; revision: bigint; document: Document }[]; - lifecycle: { + /** Missing when the legacy storage generation cannot authenticate lifecycle metadata. */ + lifecycle?: { /** * ACTIVE while the document is visible to ordinary reads, DELETED once it * has been deleted and its revisions are retained, ERASING once an @@ -1672,7 +1673,8 @@ mod tests { mod history_wasm_tests { use super::*; use drive_proof_verifier::types::{ - DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryState, + DocumentHistoryEntry, DocumentHistoryLifecycle, DocumentHistoryLifecycleTimes, + DocumentHistoryState, }; use js_sys::{Array, Reflect}; use wasm_bindgen::JsCast; @@ -1742,8 +1744,6 @@ mod history_wasm_tests { /// survive a JavaScript number. #[wasm_bindgen_test] fn should_report_the_erasing_state_and_its_times_exactly_in_javascript() { - use drive::drive::document::history::DocumentHistoryLifecycleTimes; - let started_at = (1u64 << 53) + 3; let history = DocumentHistory { entries: vec![], @@ -1758,7 +1758,9 @@ mod history_wasm_tests { }, }), }; - let result = document_history_to_js(history, [1; 32].into(), "note").unwrap(); + let result = JsValue::from( + DocumentHistoryResultWasm::from_history(history, [1; 32].into(), "note").unwrap(), + ); let lifecycle = Reflect::get(&result, &"lifecycle".into()).unwrap(); assert_eq!( Reflect::get(&lifecycle, &"state".into()).unwrap(), @@ -1789,6 +1791,7 @@ mod history_wasm_tests { lifecycle: Some(DocumentHistoryLifecycle { state: DocumentHistoryState::Active, remaining_revisions: (1u64 << 53) + 1, + times: Default::default(), }), }; let response = ProofMetadataResponseWasm::from_sdk_parts( diff --git a/packages/wasm-sdk/src/state_transitions/broadcast.rs b/packages/wasm-sdk/src/state_transitions/broadcast.rs index b2b2ca30d9e..1ab01abc636 100644 --- a/packages/wasm-sdk/src/state_transitions/broadcast.rs +++ b/packages/wasm-sdk/src/state_transitions/broadcast.rs @@ -7,7 +7,7 @@ use crate::error::WasmSdkError; use crate::sdk::WasmSdk; use crate::settings::{parse_put_settings, PutSettingsJs}; use dash_sdk::dpp::platform_value::Identifier; -use dash_sdk::dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dash_sdk::dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; use dash_sdk::dpp::state_transition::StateTransition; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; @@ -22,7 +22,7 @@ use wasm_dpp2::StateTransitionWasm; fn referenced_contract_ids(state_transition: &StateTransition) -> BTreeSet { match state_transition { StateTransition::Batch(batch_transition) => batch_transition - .transitions_iter() + .transitions_iter_v1() .map(|transition| transition.data_contract_id()) .collect(), _ => BTreeSet::new(), diff --git a/packages/wasm-sdk/src/state_transitions/document.rs b/packages/wasm-sdk/src/state_transitions/document.rs index d601d579da2..4755e0aa319 100644 --- a/packages/wasm-sdk/src/state_transitions/document.rs +++ b/packages/wasm-sdk/src/state_transitions/document.rs @@ -577,7 +577,10 @@ impl WasmSdk { /// it. Read the document's history to see how much is left. /// /// @param options - Erase options including the document identifiers, identity key, and signer - /// @returns Promise that resolves when the erase has been accepted + /// @returns Promise that resolves after an affected-state proof authenticates + /// that the document is absent by id. This does not prove that this erase + /// executed or was accepted; query document history to observe the lifecycle + /// and remaining revisions. #[wasm_bindgen(js_name = "documentErase")] pub async fn document_erase( &self, From e52b0c23b493b17b8e3df870bf96b643596d7713 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 18 Sep 2026 21:25:06 +0700 Subject: [PATCH 34/48] refactor(platform)!: move the document lifecycle to protocol 15 generations Protocol 14 ships with Platform 4.2, so the keep-history storage branch moved its activation to protocol 15 and every table and leaf module that protocol 14 selects is released. The lifecycle follows: each of its selections now lives in a protocol 15 constant, and each module it had amended that protocol 14 now owns became a new generation that only protocol 15 reaches. Version tables: the protocol 14 constants are restored to the storage branch plus dormant slots (`None` / `0`); the lifecycle selections move into `STATE_TRANSITION_SERIALIZATION_VERSIONS_V4` (batch formats up to 2, erase kind), `STATE_TRANSITION_VERSIONS_V4` (basic structure v1), `DPP_VALIDATION_VERSIONS_V6` (document type update v2), `CONTRACT_VERSIONS_V7` (parser generation 4, meta-schema v4), `SYSTEM_LIMITS_V5` (erase chunk), `DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5`, `DRIVE_ABCI_VALIDATION_VERSIONS_V11` and `DRIVE_ABCI_QUERY_VERSIONS_V4`, and into the storage branch's unreleased `DRIVE_DOCUMENT_METHOD_VERSIONS_V5`, `DRIVE_VERIFY_METHOD_VERSIONS_V3` and `DRIVE_VERSION_V10`. `PLATFORM_V15` lists the four changes it carries. Batch format 2 is active from 15. New generations for the modules protocol 14 owns: delete advanced structure v2 (admits a keep-history delete, refuses one on a contested type), create state v3 (refuses the id of a deleted or erasing document), contract parser generation 4 with `DocumentTypeV3` carrying `documents_can_be_erased` and `DocumentTypeV3Getters`, and document type update validation v2 (`canBeErased` is immutable). Generation 3 keeps refusing a deletable keep-history type; generation 4 reuses its ranked-index checks through widened visibility. Released generations differ from the storage branch only by exhaustiveness arms on the new enum variants. The format-aware document fetch has its own optional slot (`fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type`, `None` through 14, `Some(0)` at 15) instead of a generation 2 of the legacy helper that its dispatcher could never select. The legacy-signature delete dispatchers refuse a keep-history type at generation 1 instead of recording a fabricated block; only the lifecycle entry point carries the authoring block and deleter. Transformer v1's failure helper drops a parameter it never read. Tests: pins move to 15 (wire range, activation, released loops over 12 to 14 with per-version expectations, the delete harness distinguishing internal error, paid rejection and deletion). New: batch wire format ranges and an in-version decode of a format 2 batch (refused at 14, decoded at latest, empty through the released view); the released structure generation failing closed on a format 2 batch; format 1 batch action unit tests (order, credit sums with an erase alongside, security level for an erase, a token and a master-level type, agreement with format 0 on nonce bumps); the new slot in the one-implementation sweep. The storage-level delete and erase block appended to the 14 to 15 fork test is dropped: a keep-history type registered at 14 can never be deletable, and `should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion` already deletes and erases a migrated history at the storage layer. Co-Authored-By: Claude Opus 5 (1M context) --- .../document/v3/document-meta.json | 4 - .../document/v4/document-meta.json | 979 ++++++++++++++++++ .../document_type/accessors/mod.rs | 175 +++- .../document_type/accessors/v2/mod.rs | 5 - .../document_type/accessors/v3/mod.rs | 8 + .../try_from_schema/common/mod.rs | 12 +- .../class_methods/try_from_schema/mod.rs | 16 +- .../try_from_schema/v3/keep_history_tests.rs | 416 +++----- .../class_methods/try_from_schema/v3/mod.rs | 45 +- .../v3/ranked_prefix_overlap.rs | 2 +- .../try_from_schema/v4/keep_history_tests.rs | 495 +++++++++ .../class_methods/try_from_schema/v4/mod.rs | 170 +++ .../document_type/methods/mod.rs | 21 + .../methods/validate_update/common/mod.rs | 22 - .../methods/validate_update/mod.rs | 4 +- .../methods/validate_update/v1/mod.rs | 22 +- .../methods/validate_update/v2/mod.rs | 86 ++ .../methods/versioned_methods.rs | 2 + .../src/data_contract/document_type/mod.rs | 15 +- .../src/data_contract/document_type/v1/mod.rs | 3 +- .../document_type/v2/accessors.rs | 4 - .../src/data_contract/document_type/v2/mod.rs | 11 - .../document_type/v3/accessors.rs | 274 +++++ .../src/data_contract/document_type/v3/mod.rs | 350 +++++++ .../document_type/v3/random_document_type.rs | 40 + .../methods/validate_update/v0/mod.rs | 1 + packages/rs-dpp/src/state_transition/mod.rs | 114 +- .../batch_transition/accessors/mod.rs | 9 +- .../from_document.rs | 2 +- .../validate_basic_structure/v1/mod.rs | 37 + .../src/validation/meta_validators/mod.rs | 59 ++ .../document_create_transition_action/mod.rs | 13 +- .../state_v2/mod.rs | 38 - .../state_v3/mod.rs | 99 ++ .../advanced_structure_v1/mod.rs | 33 +- .../advanced_structure_v2/mod.rs | 84 ++ .../document_delete_transition_action/mod.rs | 5 +- .../advanced_structure_v0/mod.rs | 4 +- .../batch/advanced_structure/v1/mod.rs | 2 +- .../batch/state/v0/fetch_documents.rs | 54 +- .../state_transitions/batch/state/v2/mod.rs | 2 +- .../batch/state/validation_v1/mod.rs | 33 +- .../batch/tests/document/deletion.rs | 84 +- .../batch/tests/document/erase.rs | 14 +- .../tests/document/lifecycle_contracts.rs | 12 +- .../batch/transformer/v1/mod.rs | 22 +- .../src/query/document_history/mod.rs | 13 +- .../src/query/document_history/v1/mod.rs | 2 +- .../src/query/document_history/v1/tests.rs | 2 +- .../test_cases/upgrade_fork_tests.rs | 71 -- .../mod.rs | 19 + .../rs-drive/src/drive/document/delete/mod.rs | 12 +- .../src/drive/document/lifecycle/mod.rs | 2 +- .../src/drive/document/lifecycle/tests.rs | 22 +- .../src/drive/document/migration/tests.rs | 12 +- .../batch/batch_transition_v1.rs | 2 +- .../state_transition_action/batch/v1/mod.rs | 3 + .../state_transition_action/batch/v1/tests.rs | 316 ++++++ .../src/util/batch/drive_op_batch/document.rs | 7 +- .../dpp_versions/dpp_contract_versions/mod.rs | 1 + .../dpp_versions/dpp_contract_versions/v7.rs | 36 + .../mod.rs | 3 +- .../v1.rs | 2 +- .../v2.rs | 2 +- .../v3.rs | 31 +- .../v4.rs | 30 + .../dpp_state_transition_versions/v4.rs | 2 +- .../dpp_validation_versions/mod.rs | 1 + .../dpp_validation_versions/v6.rs | 18 + .../drive_abci_query_versions/mod.rs | 1 + .../drive_abci_query_versions/v3.rs | 2 +- .../drive_abci_query_versions/v4.rs | 13 + .../drive_abci_validation_versions/mod.rs | 15 +- .../drive_abci_validation_versions/v1.rs | 2 + .../drive_abci_validation_versions/v10.rs | 35 +- .../drive_abci_validation_versions/v11.rs | 53 + .../drive_abci_validation_versions/v2.rs | 2 + .../drive_abci_validation_versions/v3.rs | 2 + .../drive_abci_validation_versions/v4.rs | 2 + .../drive_abci_validation_versions/v5.rs | 2 + .../drive_abci_validation_versions/v6.rs | 2 + .../drive_abci_validation_versions/v7.rs | 2 + .../drive_abci_validation_versions/v8.rs | 2 + .../drive_abci_validation_versions/v9.rs | 2 + .../drive_document_method_versions/mod.rs | 4 +- .../drive_document_method_versions/v4.rs | 32 +- .../drive_document_method_versions/v5.rs | 20 +- .../mod.rs | 3 +- .../v4.rs | 8 +- .../v5.rs | 25 + .../drive_verify_method_versions/v3.rs | 3 +- .../src/version/drive_versions/v10.rs | 12 +- .../src/version/drive_versions/v9.rs | 4 +- .../src/version/system_limits/mod.rs | 1 + .../src/version/system_limits/v4.rs | 2 +- .../src/version/system_limits/v5.rs | 11 + .../rs-platform-version/src/version/v14.rs | 4 +- .../rs-platform-version/src/version/v15.rs | 55 +- .../tests/keep_history_delete_versions.rs | 78 +- packages/rs-sdk/src/mock/requests.rs | 2 +- .../platform/documents/transitions/erase.rs | 8 +- .../batch/batch_transition.rs | 2 +- 102 files changed, 4132 insertions(+), 790 deletions(-) create mode 100644 packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json create mode 100644 packages/rs-dpp/src/data_contract/document_type/accessors/v3/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v3/accessors.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v3/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/document_type/v3/random_document_type.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v3/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v2/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/v1/tests.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs create mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs create mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs create mode 100644 packages/rs-platform-version/src/version/system_limits/v5.rs diff --git a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json index d3cdbd4a265..51e6b34c648 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json @@ -767,10 +767,6 @@ "type": "boolean", "description": "True if the documents can be deleted, default is true" }, - "canBeErased": { - "type": "boolean", - "description": "True if a deleted document may have its retained revisions purged by an erase transition; requires documentsKeepHistory and canBeDeleted, default is false, immutable on contract update" - }, "transferable": { "type": "integer", "enum": [ diff --git a/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json new file mode 100644 index 00000000000..d3cdbd4a265 --- /dev/null +++ b/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json @@ -0,0 +1,979 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", + "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V14 SHIPS — FROZEN AFTER. This v3 document meta-schema activates with protocol v14 (CONTRACT_VERSIONS_V6). It is v2 plus the ranked index keywords (rankedCountable, rankedSummable, rankedAverageable), the refersTo reference keyword on identifier properties, the requiredSince property keyword (the contract version a property is required from), and the timeRange index transform, and admits every v14+ contract written to disk. v2 stays in place for protocol v13, where those keys still fail an index entry's `additionalProperties: false`. Once the release carrying protocol v14 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v4+). The $id above deliberately still names the v1 path: v1, v2 and v3 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", + "type": "object", + "$defs": { + "documentProperties": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9-_]{1,64}$": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + } + }, + "propertyNames": { + "pattern": "^[a-zA-Z0-9-_]{1,64}$" + }, + "minProperties": 1, + "maxProperties": 100 + }, + "documentSchemaArray": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + } + }, + "documentSchema": { + "type": "object", + "properties": { + "$id": { + "type": "string", + "pattern": "^#", + "minLength": 1 + }, + "$ref": { + "type": "string", + "pattern": "^#", + "minLength": 1 + }, + "$comment": { + "$ref": "https://json-schema.org/draft/2020-12/meta/core#/properties/$comment" + }, + "description": { + "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data#/properties/description" + }, + "examples": { + "$ref": "https://json-schema.org/draft/2020-12/meta/meta-data#/properties/examples" + }, + "multipleOf": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/multipleOf" + }, + "maximum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxLength" + }, + "minLength": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minLength" + }, + "pattern": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/pattern" + }, + "maxItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxItems" + }, + "minItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minItems" + }, + "uniqueItems": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/uniqueItems" + }, + "refersTo": { + "type": "object", + "properties": { + "type": { + "enum": [ + "identity", + "contract", + "token", + "permanentDocument", + "identityPublicKey" + ] + }, + "contractId": { + "description": "The id of the data contract the referenced document lives in, as a base58 string or a 32-byte array; when absent the reference targets the declaring contract itself", + "oneOf": [ + { + "type": "string", + "minLength": 32, + "maxLength": 44, + "pattern": "^[123456789A-HJ-NP-Za-km-z]{32,44}$" + }, + { + "type": "array", + "minItems": 32, + "maxItems": 32, + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + } + } + ] + }, + "documentType": { + "description": "The name of the referenced document type; it must forbid deletion (canBeDeleted: false)", + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-zA-Z0-9-_]{1,64}$" + }, + "keyIdProperty": { + "description": "The property of the same document type whose value carries the referenced key id; the reference property's value carries the identity id", + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-zA-Z0-9-_]{1,64}(\\.[a-zA-Z0-9-_]{1,64})*$" + }, + "propertyAgreement": { + "description": "permanentDocument references only: each { referring property: referenced property } pair must hold as an equality between the referring document's value and the referenced document's value, enforced by consensus at document write time; both properties must exist and share one property type, validated at contract registration", + "type": "object", + "minProperties": 1, + "maxProperties": 10, + "propertyNames": { + "pattern": "^[a-zA-Z0-9-_]{1,64}(\\.[a-zA-Z0-9-_]{1,64})*$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[a-zA-Z0-9-_]{1,64}(\\.[a-zA-Z0-9-_]{1,64})*$" + } + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { "type": { "const": "permanentDocument" } }, + "required": ["type"] + }, + "then": { + "required": ["type", "documentType"] + }, + "else": { + "properties": { + "contractId": false, + "documentType": false + } + } + }, + { + "if": { + "properties": { "type": { "const": "identityPublicKey" } }, + "required": ["type"] + }, + "then": { + "required": ["type", "keyIdProperty"] + }, + "else": { + "properties": { + "keyIdProperty": false + } + } + } + ] + }, + "contains": { + "$ref": "https://json-schema.org/draft/2020-12/meta/applicator#/properties/contains" + }, + "maxProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/maxProperties" + }, + "minProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/minProperties" + }, + "required": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/required" + }, + "additionalProperties": { + "type": "boolean", + "const": false + }, + "properties": { + "$ref": "#/$defs/documentProperties" + }, + "dependentRequired": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "$ref": "https://json-schema.org/draft/2020-12/meta/validation#/properties/type" + }, + "format": { + "$ref": "https://json-schema.org/draft/2020-12/meta/format-annotation#/properties/format" + }, + "contentMediaType": { + "$ref": "https://json-schema.org/draft/2020-12/meta/content#/properties/contentMediaType" + }, + "byteArray": { + "type": "boolean", + "const": true + }, + "position": { + "type": "integer", + "minimum": 0 + }, + "requiredSince": { + "type": "integer", + "minimum": 1, + "maximum": 4294967295 + } + }, + "dependentSchemas": { + "byteArray": { + "description": "should be used only with array type", + "properties": { + "type": { + "type": "string", + "const": "array" + } + } + }, + "contentMediaType": { + "if": { + "properties": { + "contentMediaType": { + "const": "application/x.dash.dpp.identifier" + } + } + }, + "then": { + "properties": { + "byteArray": { + "const": true + }, + "minItems": { + "const": 32 + }, + "maxItems": { + "const": 32 + } + }, + "required": [ + "byteArray", + "minItems", + "maxItems" + ] + } + }, + "pattern": { + "description": "prevent slow pattern matching of large strings", + "properties": { + "maxLength": { + "type": "integer", + "minimum": 0, + "maximum": 50000 + } + }, + "required": [ + "maxLength" + ] + }, + "refersTo": { + "description": "refersTo is only allowed on identifier properties", + "properties": { + "type": { + "const": "array" + }, + "byteArray": { + "const": true + }, + "contentMediaType": { + "const": "application/x.dash.dpp.identifier" + }, + "minItems": { + "const": 32 + }, + "maxItems": { + "const": 32 + } + }, + "required": [ + "type", + "byteArray", + "contentMediaType", + "minItems", + "maxItems" + ] + }, + "format": { + "description": "prevent slow format validation of large strings", + "properties": { + "maxLength": { + "type": "integer", + "minimum": 0, + "maximum": 50000 + } + }, + "required": [ + "maxLength" + ] + } + }, + "allOf": [ + { + "$comment": "require index for object properties", + "if": { + "properties": { + "type": { + "const": "object" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "properties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "position": true + }, + "required": [ + "position" + ] + } + } + } + } + }, + { + "$comment": "allow only byte arrays", + "if": { + "properties": { + "type": { + "const": "array" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "byteArray": true + }, + "required": [ + "byteArray" + ] + } + }, + { + "$comment": "all object properties must be defined", + "if": { + "properties": { + "type": { + "const": "object" + } + }, + "not": { + "properties": { + "$ref": true + }, + "required": [ + "$ref" + ] + } + }, + "then": { + "properties": { + "properties": { + "$ref": "#/$defs/documentProperties" + }, + "additionalProperties": { + "$ref": "#/$defs/documentSchema/properties/additionalProperties" + } + }, + "required": [ + "properties", + "additionalProperties" + ] + } + } + ] + }, + "documentActionTokenCost": { + "type": "object", + "properties": { + "contractId": { + "type": "array", + "contentMediaType": "application/x.dash.dpp.identifier", + "byteArray": true, + "minItems": 32, + "maxItems": 32 + }, + "tokenPosition": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "amount": { + "type": "integer", + "minimum": 1, + "maximum": 281474976710655 + }, + "effect": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "0 - TransferTokenToContractOwner (default), 1 - Burn" + }, + "gasFeesPaidBy": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "0 - DocumentOwner (default), 1 - ContractOwner, 2 - PreferContractOwner" + } + }, + "required": [ + "tokenPosition", + "amount" + ], + "additionalProperties": false + } + }, + "properties": { + "type": { + "type": "string", + "const": "object" + }, + "$schema": { + "type": "string", + "const": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json" + }, + "$defs": { + "$ref": "#/$defs/documentProperties" + }, + "indices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "properties": { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "maxLength": 256 + }, + "additionalProperties": { + "type": "string", + "enum": [ + "asc" + ] + }, + "minProperties": 1, + "maxProperties": 1 + }, + "minItems": 1, + "maxItems": 10 + }, + "unique": { + "type": "boolean" + }, + "nullSearchable": { + "type": "boolean" + }, + "contested": { + "type": "object", + "properties": { + "fieldMatches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "regexPattern": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "additionalProperties": false, + "required": [ + "field", + "regexPattern" + ] + }, + "minItems": 1 + }, + "resolution": { + "type": "integer", + "enum": [ + 0 + ], + "description": "Resolution. 0 - Masternode Vote" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "resolution" + ], + "additionalProperties": false + }, + "countable": { + "oneOf": [ + { + "type": "boolean", + "description": "Legacy form. true == \"countable\", false == \"notCountable\". Kept for back-compat with contracts written before the enum form was introduced." + }, + { + "type": "string", + "enum": ["notCountable", "countable", "countableAllowingOffset"], + "description": "\"countable\" — index uses a CountTree (O(1) totals). \"countableAllowingOffset\" — index uses a ProvableCountTree (totals + future O(log n) range / offset queries). \"notCountable\" — plain NormalTree (no count fast path)." + } + ], + "description": "Whether and how the index supports count fast paths. Adds extra storage cost for non-default values." + }, + "rangeCountable": { + "type": "boolean", + "description": "When true, the property-name level becomes a ProvableCountTree and value trees become CountTrees so range-count queries on the indexed property are O(log n). Requires `countable` to be \"countable\" or \"countableAllowingOffset\"." + }, + "summable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Name of an integer document property whose values are aggregated into a sum at the index. When set, the index's value trees become SumTrees and each per-document index reference is a ReferenceWithSumItem contributing the named property's value to ancestor sum-bearing trees. The property must exist on the document type, be in `required`, and have an integer type." + }, + "rangeSummable": { + "type": "boolean", + "description": "When true, the property-name level becomes a ProvableSumTree (or ProvableCountProvableSumTree when paired with `rangeCountable: true`) so range-sum queries on the indexed property are O(log n) via the `AggregateSumOnRange` proof primitive. Requires `summable` to be set." + }, + "averageable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Syntactic sugar: `averageable: \"\"` is shorthand for `countable: \"countable\"` + `summable: \"\"`. Enables average queries (which return `(count, sum)` pairs the client divides) without forcing authors to think in terms of count + sum. Same on-disk layout as setting both underlying flags. If you set both `averageable` and `summable`, they must name the same property." + }, + "rangeAverageable": { + "type": "boolean", + "description": "Syntactic sugar: `rangeAverageable: true` is shorthand for `rangeCountable: true` + `rangeSummable: true`. Requires `averageable` to be set." + }, + "rankedCountable": { + "oneOf": [ + { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's document count, so \"top / bottom K groups by count\" queries are O(log n + k) with proofs." + }, + { + "type": "object", + "properties": { + "at": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 10, + "uniqueItems": true + } + ], + "description": "Name of the index property (or array of properties) whose levels carry Count rankings. Each must be one of the index's properties (an index has at most 10, hence maxItems); naming the last property is equivalent to the boolean form. A non-terminal property places a ranking at that prefix level: its values are ranked by whole-subtree document count (e.g. on [hashtag, postId], at: \"hashtag\" ranks hashtags by total count across all their posts), and every level from the shallowest ranked one down to the terminal is laid out count-bearing so each write's delta propagates up through the chain. ANY subset of levels may be named — e.g. [\"hashtag\", \"postId\"] declares both rankings on one index, and a fully ranked index ranks every level." + } + }, + "required": ["at"], + "additionalProperties": false, + "description": "Level-addressed form: places Count rankings at the named properties' levels. Cannot be combined with rankedSummable or rankedAverageable when a non-terminal level is named, and no other index of the document type may share a non-terminal ranked level or any level below it." + } + ], + "description": "Count ranking axis. Requires `rangeCountable: true`. Independent of the Sum and Avg axes (`rankedSummable` / `rankedAverageable`), which stay terminal-level booleans." + }, + "rankedSummable": { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's sum of the `summable` property, so \"top / bottom K groups by sum\" queries are O(log n + k) with proofs. Requires `rangeSummable: true`. Adds the Sum ranking axis only." + }, + "rankedAverageable": { + "type": "boolean", + "description": "When true, the index's terminal property-name tree also carries an ordered secondary tree keyed by each group's average (count + sum pair) of the `averageable` property, so \"top / bottom K groups by average\" queries are O(log n + k) with proofs. Requires `rangeAverageable: true` (which itself implies `rangeCountable` + `rangeSummable`). Adds the Avg ranking axis only — it does NOT imply `rankedCountable` or `rankedSummable`; each ranking axis costs its own secondary tree and is opted into separately." + }, + "timeRange": { + "type": "object", + "properties": { + "on": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Name of the timestamp index property to bucket. Must be this index's first property and name one of the system timestamps ($createdAt, $updatedAt or $transferredAt). A timeRange index may be unique only when range equals step (non-overlapping windows) and `on` is $createdAt." + }, + "range": { + "type": "integer", + "minimum": 1, + "description": "Length of each time range window, in seconds. Must be an exact multiple of `step`." + }, + "step": { + "type": "integer", + "minimum": 1, + "description": "Interval between successive range starts, in seconds. When `range` > `step` the ranges overlap and a document is indexed under `range / step` bucket-start values, bounded by a protocol-versioned cap (24 at protocol version 14)." + }, + "phase": { + "type": "integer", + "minimum": 0, + "description": "Grid alignment phase, in seconds. Range starts are `phase + k * step`; must be strictly less than `step` (a larger value would be a redundant spelling of `phase % step`) and strictly less than one year (31536000 — a phase further out could sit past current block time on a huge step, leaving valid timestamps before the grid's first bucket). A pure alignment offset — it moves where window boundaries fall (e.g. daily windows cut at 06:00 UTC instead of midnight) and never excludes any real timestamp. Defaults to 0." + }, + "ttl": { + "type": "integer", + "minimum": 1, + "description": "Time to live, in seconds: entries under this index exist for at most this long past their bucket's start, plus a bounded drainage lag — every write into the index continues draining the oldest expired bucket under a per-write operation budget, and expired windows are not queryable (a byStart selection past the horizon is rejected), so every queryable window is complete. Must be at least `range` (a window still able to receive consensus-timestamped writes can never expire) and at most a protocol-versioned cap (604800 — one week — at protocol version 14). Indexes bucketing one field with the same grid share its storage level and must declare the same ttl. Bytes written under a TTL'd index bill to processing at an ephemeral-bytes rate instead of to storage, carry no storage flags, and refund nothing on removal. Omitted means entries live forever. Available from protocol version 14." + } + }, + "required": ["on", "range", "step"], + "additionalProperties": false, + "description": "Buckets the first index property's timestamp into fixed-length, regularly-spaced (possibly overlapping) time ranges. The window parameters (`range`, `step`, `phase`) are declared in seconds, since a bucket is selected from block time and the target block interval is five seconds; the stored key is the range start as a u64 millisecond timestamp, so it stays directly comparable to the source timestamp it buckets. Enables trending/leaderboard queries within the newest/oldest active range. A system-timestamp source must be listed in the document type's required fields. Several indexes may bucket the same timestamp with different grids — each grid gets its own index subtree, keyed by the property name qualified with the grid parameters. Available from protocol version 14." + }, + "terminal": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Only on indexOnly document types: names the property whose value is this index entry's member key — the docId-analog terminal key under the index's storage marker, stored as an Item instead of a Reference because there is no primary-storage row. Either \"$ownerId\" (the default when omitted) or an identifier property carrying a refersTo declaration (identity, contract, token, or permanentDocument). Must not repeat one of the index's listed properties. Available from protocol version 14." + }, + "preallocated": { + "type": "boolean", + "description": "Only on indexOnly document types whose index path is fully determined by a same-contract permanentDocument refersTo declaration: every index property must be either the referring property itself (its value is the referenced document's $id) or a key of that declaration's propertyAgreement (consensus-equal to a referenced-document property). When true, creating a referenced document also creates this index's dynamic trees for entries referencing it — paid by the referenced document's creator — and deleting the last entry keeps them, so every entry insert costs the same as the first. Available from protocol version 14." + }, + "skipIfAbsent": { + "type": "boolean", + "description": "Only on indexOnly document types: when true, a document that omits this index's first property writes no entry into this index (and a delete recomputes the same skip), so the index holds only documents carrying the property. The first property is the skip trigger: it must be a top-level schema property NOT listed in `required` (making it the only way an indexOnly property may be optional), and every index involving an optional property must be skipIfAbsent with that property first. Every other property that is not a skip trigger must still appear in at least one non-skipIfAbsent index, and at least one $createdAt-free index must remain non-skipIfAbsent (the executed-transition proof index). An absent trigger is distinct from an empty value: absence skips the index, while any present value — empty included — indexes normally. Available from protocol version 14." + } + }, + "required": [ + "properties", + "name" + ], + "dependentRequired": { + "rangeCountable": ["countable"], + "rangeSummable": ["summable"], + "rangeAverageable": ["averageable"] + }, + "$comment": "The ranked prerequisites are value-sensitive, unlike the range* rows above: `dependentRequired` fires on key *presence*, so listing them there would make an explicit `\"rankedCountable\": false` — a written-out opt-out, which the structural parser accepts as such — demand a `rangeCountable` the index does not need. The range* rows keep presence semantics because that is what they shipped with in v2 and changing them would move historical validation results.", + "allOf": [ + { + "if": { + "properties": { + "rankedCountable": { + "anyOf": [{ "const": true }, { "type": "object" }] + } + }, + "required": ["rankedCountable"] + }, + "then": { "required": ["rangeCountable"] } + }, + { + "if": { + "properties": { "rankedSummable": { "const": true } }, + "required": ["rankedSummable"] + }, + "then": { "required": ["rangeSummable"] } + }, + { + "if": { + "properties": { "rankedAverageable": { "const": true } }, + "required": ["rankedAverageable"] + }, + "then": { "required": ["rangeAverageable"] } + } + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 10 + }, + "signatureSecurityLevelRequirement": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "description": "Public key security level. 1 - Critical, 2 - High, 3 - Medium. If none specified, High level is used" + }, + "documentsKeepHistory": { + "type": "boolean", + "description": "True if the documents keep all their history, default is false" + }, + "keepsTransferHistory": { + "type": "boolean", + "description": "True if transfers of these documents are recorded in the document history system contract, default is false" + }, + "keepsPurchaseHistory": { + "type": "boolean", + "description": "True if purchases of these documents are recorded in the document history system contract, default is false" + }, + "keepsPricingHistory": { + "type": "boolean", + "description": "True if price updates on these documents are recorded in the document history system contract, default is false" + }, + "documentsMutable": { + "type": "boolean", + "description": "True if the documents are mutable, default is true" + }, + "canBeDeleted": { + "type": "boolean", + "description": "True if the documents can be deleted, default is true" + }, + "canBeErased": { + "type": "boolean", + "description": "True if a deleted document may have its retained revisions purged by an erase transition; requires documentsKeepHistory and canBeDeleted, default is false, immutable on contract update" + }, + "transferable": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "Transferable without a marketplace sell. 0 - Never, 1 - Always" + }, + "tradeMode": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "Built in marketplace system. 0 - None, 1 - Direct purchase (The user can buy the item without the need for an approval)" + }, + "creationRestrictionMode": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Restrictions of document creation. 0 - No restrictions, 1 - Owner only, 2 - No creation (System Only)" + }, + "requiresIdentityEncryptionBoundedKey": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." + }, + "requiresIdentityDecryptionBoundedKey": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "description": "Key requirements. 0 - Unique Non Replaceable, 1 - Multiple, 2 - Multiple with reference to latest key." + }, + "documentsCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a CountTree enabling O(1) total document count queries." + }, + "rangeCountable": { + "type": "boolean", + "description": "When true, the primary key tree uses a ProvableCountTree enabling range countable. Implies documentsCountable." + }, + "documentsSummable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Name of an integer document property aggregated into the primary-key SumTree (one sum per document type). Stores documents as `ItemWithSumItem` so the primary-key tree's root sum is the total of the named property across all docs of this type. Property must exist on the document type, be in `required`, and have an integer type. Composes with `documentsKeepHistory: true` — keep-history doctypes get a `SumTree` per-document subtree with a `ReferenceWithSumItem` on the `0`-key carrying the current version's value, so the doctype-level root aggregate reflects current versions only (historical versions don't double-count)." + }, + "rangeSummable": { + "type": "boolean", + "description": "When true, the primary key tree uses a ProvableSumTree (or ProvableCountProvableSumTree paired with rangeCountable: true) enabling O(log n) range-sum queries over the primary axis. Requires `documentsSummable` to be set. Rarely useful — range-sum on the primary key with no where-clause filter is unusual; most callers want per-index `rangeSummable` instead. Set this only when you need a provable global range-sum tree." + }, + "documentsAverageable": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Syntactic sugar: `documentsAverageable: \"\"` is shorthand for `documentsCountable: true` + `documentsSummable: \"\"`. Enables doctype-wide average queries (returns `(count, sum)` the client divides) without authors having to compose the count + sum flags. Same on-disk layout. If you set both `documentsAverageable` and `documentsSummable`, they must name the same property. Composes with `documentsKeepHistory: true` via the per-doc SumTree + ReferenceWithSumItem layout described under `documentsSummable`." + }, + "rangeAverageable": { + "type": "boolean", + "description": "Syntactic sugar: `rangeAverageable: true` is shorthand for `rangeCountable: true` + `rangeSummable: true`. Requires `documentsAverageable` to be set. Same caveat as `rangeSummable` — rarely useful on the primary key; per-index `rangeAverageable` is what most callers want." + }, + "indexOnly": { + "type": "boolean", + "description": "When true, documents of this type are never written to primary storage: the index entries are the rows, each terminating in an Item keyed by the index's `terminal` property instead of a Reference keyed by the document id. Only what is in the indexes exists and is recoverable. Requires: every property required and appearing in at least one index (except a `skipIfAbsent` index's optional first property), $ownerId in at least one index (as a property or terminal), documentsMutable: false, no transfers/trading/history/transient properties, and no doctype-level aggregate keywords (use the index-level count flags). Available from protocol version 14." + }, + "tokenCost": { + "type": "object", + "properties": { + "create": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "replace": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "delete": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "transfer": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "update_price": { + "$ref": "#/$defs/documentActionTokenCost" + }, + "purchase": { + "$ref": "#/$defs/documentActionTokenCost" + } + }, + "additionalProperties": false + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/documentSchema" + } + ], + "unevaluatedProperties": false + }, + "properties": { + "$id": true, + "$ownerId": true, + "$revision": true, + "$createdAt": true, + "$updatedAt": true, + "$transferredAt": true, + "$createdAtBlockHeight": true, + "$updatedAtBlockHeight": true, + "$transferredAtBlockHeight": true, + "$createdAtCoreBlockHeight": true, + "$updatedAtCoreBlockHeight": true, + "$transferredAtCoreBlockHeight": true + }, + "propertyNames": { + "oneOf": [ + { + "type": "string", + "pattern": "^[a-zA-Z0-9-_]{1,64}$" + }, + { + "type": "string", + "enum": [ + "$id", + "$ownerId", + "$revision", + "$createdAt", + "$updatedAt", + "$transferredAt", + "$createdAtBlockHeight", + "$updatedAtBlockHeight", + "$transferredAtBlockHeight", + "$createdAtCoreBlockHeight", + "$updatedAtCoreBlockHeight", + "$transferredAtCoreBlockHeight" + ] + } + ] + }, + "minProperties": 1, + "maxProperties": 100 + }, + "transient": { + "type": "array", + "items": { + "type": "string" + } + }, + "additionalProperties": { + "type": "boolean", + "const": false + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "$comment": { + "type": "string" + }, + "description": { + "type": "string" + }, + "minProperties": { + "type": "integer", + "minimum": 0 + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + }, + "required": [ + "$schema", + "type", + "properties", + "additionalProperties" + ], + "dependentRequired": { + "rangeSummable": ["documentsSummable"], + "rangeAverageable": ["documentsAverageable"] + }, + "additionalProperties": false +} diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs index 86710502e76..5816ce36801 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/mod.rs @@ -1,6 +1,7 @@ mod v0; mod v1; mod v2; +mod v3; use crate::data_contract::document_type::index::Index; use crate::data_contract::document_type::index_level::IndexLevel; @@ -23,6 +24,7 @@ use std::collections::{BTreeMap, BTreeSet}; pub use v0::*; pub use v1::*; pub use v2::*; +pub use v3::*; impl DocumentTypeV0MutGetters for DocumentType { fn schema_mut(&mut self) -> &mut Value { @@ -30,6 +32,7 @@ impl DocumentTypeV0MutGetters for DocumentType { DocumentType::V0(v0) => v0.schema_mut(), DocumentType::V1(v1) => v1.schema_mut(), DocumentType::V2(v2) => v2.schema_mut(), + DocumentType::V3(v3) => v3.schema_mut(), } } } @@ -40,6 +43,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.name(), DocumentType::V1(v1) => v1.name(), DocumentType::V2(v2) => v2.name(), + DocumentType::V3(v3) => v3.name(), } } @@ -48,6 +52,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.schema(), DocumentType::V1(v1) => v1.schema(), DocumentType::V2(v2) => v2.schema(), + DocumentType::V3(v3) => v3.schema(), } } @@ -56,6 +61,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.schema_owned(), DocumentType::V1(v1) => v1.schema_owned(), DocumentType::V2(v2) => v2.schema_owned(), + DocumentType::V3(v3) => v3.schema_owned(), } } @@ -64,6 +70,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.indexes(), DocumentType::V1(v1) => v1.indexes(), DocumentType::V2(v2) => v2.indexes(), + DocumentType::V3(v3) => v3.indexes(), } } @@ -72,6 +79,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.find_contested_index(), DocumentType::V1(v1) => v1.find_contested_index(), DocumentType::V2(v2) => v2.find_contested_index(), + DocumentType::V3(v3) => v3.find_contested_index(), } } @@ -80,6 +88,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.index_structure(), DocumentType::V1(v1) => v1.index_structure(), DocumentType::V2(v2) => v2.index_structure(), + DocumentType::V3(v3) => v3.index_structure(), } } @@ -88,6 +97,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.flattened_properties(), DocumentType::V1(v1) => v1.flattened_properties(), DocumentType::V2(v2) => v2.flattened_properties(), + DocumentType::V3(v3) => v3.flattened_properties(), } } @@ -96,6 +106,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.properties(), DocumentType::V1(v1) => v1.properties(), DocumentType::V2(v2) => v2.properties(), + DocumentType::V3(v3) => v3.properties(), } } @@ -104,6 +115,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.identifier_paths(), DocumentType::V1(v1) => v1.identifier_paths(), DocumentType::V2(v2) => v2.identifier_paths(), + DocumentType::V3(v3) => v3.identifier_paths(), } } @@ -112,6 +124,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.binary_paths(), DocumentType::V1(v1) => v1.binary_paths(), DocumentType::V2(v2) => v2.binary_paths(), + DocumentType::V3(v3) => v3.binary_paths(), } } @@ -120,6 +133,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.required_fields(), DocumentType::V1(v1) => v1.required_fields(), DocumentType::V2(v2) => v2.required_fields(), + DocumentType::V3(v3) => v3.required_fields(), } } @@ -128,6 +142,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.transient_fields(), DocumentType::V1(v1) => v1.transient_fields(), DocumentType::V2(v2) => v2.transient_fields(), + DocumentType::V3(v3) => v3.transient_fields(), } } @@ -136,6 +151,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_keep_history(), DocumentType::V1(v1) => v1.documents_keep_history(), DocumentType::V2(v2) => v2.documents_keep_history(), + DocumentType::V3(v3) => v3.documents_keep_history(), } } @@ -144,6 +160,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_keep_transfer_history(), DocumentType::V1(v1) => v1.documents_keep_transfer_history(), DocumentType::V2(v2) => v2.documents_keep_transfer_history(), + DocumentType::V3(v3) => v3.documents_keep_transfer_history(), } } @@ -152,6 +169,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_keep_purchase_history(), DocumentType::V1(v1) => v1.documents_keep_purchase_history(), DocumentType::V2(v2) => v2.documents_keep_purchase_history(), + DocumentType::V3(v3) => v3.documents_keep_purchase_history(), } } @@ -160,6 +178,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_keep_pricing_history(), DocumentType::V1(v1) => v1.documents_keep_pricing_history(), DocumentType::V2(v2) => v2.documents_keep_pricing_history(), + DocumentType::V3(v3) => v3.documents_keep_pricing_history(), } } @@ -168,6 +187,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_mutable(), DocumentType::V1(v1) => v1.documents_mutable(), DocumentType::V2(v2) => v2.documents_mutable(), + DocumentType::V3(v3) => v3.documents_mutable(), } } @@ -176,6 +196,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_can_be_deleted(), DocumentType::V1(v1) => v1.documents_can_be_deleted(), DocumentType::V2(v2) => v2.documents_can_be_deleted(), + DocumentType::V3(v3) => v3.documents_can_be_deleted(), } } @@ -184,6 +205,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.documents_transferable(), DocumentType::V1(v1) => v1.documents_transferable(), DocumentType::V2(v2) => v2.documents_transferable(), + DocumentType::V3(v3) => v3.documents_transferable(), } } @@ -192,6 +214,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.trade_mode(), DocumentType::V1(v1) => v1.trade_mode(), DocumentType::V2(v2) => v2.trade_mode(), + DocumentType::V3(v3) => v3.trade_mode(), } } @@ -200,6 +223,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.creation_restriction_mode(), DocumentType::V1(v1) => v1.creation_restriction_mode(), DocumentType::V2(v2) => v2.creation_restriction_mode(), + DocumentType::V3(v3) => v3.creation_restriction_mode(), } } @@ -208,6 +232,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.data_contract_id(), DocumentType::V1(v1) => v1.data_contract_id(), DocumentType::V2(v2) => v2.data_contract_id(), + DocumentType::V3(v3) => v3.data_contract_id(), } } @@ -216,6 +241,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentType::V1(v1) => v1.requires_identity_encryption_bounded_key(), DocumentType::V2(v2) => v2.requires_identity_encryption_bounded_key(), + DocumentType::V3(v3) => v3.requires_identity_encryption_bounded_key(), } } @@ -224,6 +250,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentType::V1(v1) => v1.requires_identity_decryption_bounded_key(), DocumentType::V2(v2) => v2.requires_identity_decryption_bounded_key(), + DocumentType::V3(v3) => v3.requires_identity_decryption_bounded_key(), } } @@ -232,6 +259,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.security_level_requirement(), DocumentType::V1(v1) => v1.security_level_requirement(), DocumentType::V2(v2) => v2.security_level_requirement(), + DocumentType::V3(v3) => v3.security_level_requirement(), } } @@ -241,6 +269,7 @@ impl DocumentTypeV0Getters for DocumentType { DocumentType::V0(v0) => v0.json_schema_validator_ref(), DocumentType::V1(v1) => v1.json_schema_validator_ref(), DocumentType::V2(v2) => v2.json_schema_validator_ref(), + DocumentType::V3(v3) => v3.json_schema_validator_ref(), } } } @@ -251,6 +280,7 @@ impl DocumentTypeV0Setters for DocumentType { DocumentType::V0(v0) => v0.set_data_contract_id(data_contract_id), DocumentType::V1(v1) => v1.set_data_contract_id(data_contract_id), DocumentType::V2(v2) => v2.set_data_contract_id(data_contract_id), + DocumentType::V3(v3) => v3.set_data_contract_id(data_contract_id), } } } @@ -261,6 +291,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_creation_token_cost(cost), DocumentType::V2(v2) => v2.set_document_creation_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_creation_token_cost(cost), } } @@ -269,6 +300,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_replacement_token_cost(cost), DocumentType::V2(v2) => v2.set_document_replacement_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_replacement_token_cost(cost), } } @@ -277,6 +309,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_deletion_token_cost(cost), DocumentType::V2(v2) => v2.set_document_deletion_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_deletion_token_cost(cost), } } @@ -285,6 +318,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_transfer_token_cost(cost), DocumentType::V2(v2) => v2.set_document_transfer_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_transfer_token_cost(cost), } } @@ -293,6 +327,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_price_update_token_cost(cost), DocumentType::V2(v2) => v2.set_document_price_update_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_price_update_token_cost(cost), } } @@ -301,6 +336,7 @@ impl DocumentTypeV1Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(v1) => v1.set_document_purchase_token_cost(cost), DocumentType::V2(v2) => v2.set_document_purchase_token_cost(cost), + DocumentType::V3(v3) => v3.set_document_purchase_token_cost(cost), } } } @@ -311,6 +347,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.name(), DocumentTypeRef::V1(v1) => v1.name(), DocumentTypeRef::V2(v2) => v2.name(), + DocumentTypeRef::V3(v3) => v3.name(), } } @@ -319,6 +356,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.schema(), DocumentTypeRef::V1(v1) => v1.schema(), DocumentTypeRef::V2(v2) => v2.schema(), + DocumentTypeRef::V3(v3) => v3.schema(), } } @@ -327,6 +365,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.clone().schema_owned(), DocumentTypeRef::V1(v1) => v1.clone().schema_owned(), DocumentTypeRef::V2(v2) => v2.clone().schema_owned(), + DocumentTypeRef::V3(v3) => v3.clone().schema_owned(), } } @@ -335,6 +374,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.indexes(), DocumentTypeRef::V1(v1) => v1.indexes(), DocumentTypeRef::V2(v2) => v2.indexes(), + DocumentTypeRef::V3(v3) => v3.indexes(), } } @@ -343,6 +383,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.find_contested_index(), DocumentTypeRef::V1(v1) => v1.find_contested_index(), DocumentTypeRef::V2(v2) => v2.find_contested_index(), + DocumentTypeRef::V3(v3) => v3.find_contested_index(), } } @@ -351,6 +392,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.index_structure(), DocumentTypeRef::V1(v1) => v1.index_structure(), DocumentTypeRef::V2(v2) => v2.index_structure(), + DocumentTypeRef::V3(v3) => v3.index_structure(), } } @@ -359,6 +401,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.flattened_properties(), DocumentTypeRef::V1(v1) => v1.flattened_properties(), DocumentTypeRef::V2(v2) => v2.flattened_properties(), + DocumentTypeRef::V3(v3) => v3.flattened_properties(), } } @@ -367,6 +410,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.properties(), DocumentTypeRef::V1(v1) => v1.properties(), DocumentTypeRef::V2(v2) => v2.properties(), + DocumentTypeRef::V3(v3) => v3.properties(), } } @@ -375,6 +419,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.identifier_paths(), DocumentTypeRef::V1(v1) => v1.identifier_paths(), DocumentTypeRef::V2(v2) => v2.identifier_paths(), + DocumentTypeRef::V3(v3) => v3.identifier_paths(), } } @@ -383,6 +428,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.binary_paths(), DocumentTypeRef::V1(v1) => v1.binary_paths(), DocumentTypeRef::V2(v2) => v2.binary_paths(), + DocumentTypeRef::V3(v3) => v3.binary_paths(), } } @@ -391,6 +437,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.required_fields(), DocumentTypeRef::V1(v1) => v1.required_fields(), DocumentTypeRef::V2(v2) => v2.required_fields(), + DocumentTypeRef::V3(v3) => v3.required_fields(), } } @@ -399,6 +446,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.transient_fields(), DocumentTypeRef::V1(v1) => v1.transient_fields(), DocumentTypeRef::V2(v2) => v2.transient_fields(), + DocumentTypeRef::V3(v3) => v3.transient_fields(), } } @@ -407,6 +455,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_keep_history(), DocumentTypeRef::V1(v1) => v1.documents_keep_history(), DocumentTypeRef::V2(v2) => v2.documents_keep_history(), + DocumentTypeRef::V3(v3) => v3.documents_keep_history(), } } @@ -415,6 +464,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_keep_transfer_history(), DocumentTypeRef::V1(v1) => v1.documents_keep_transfer_history(), DocumentTypeRef::V2(v2) => v2.documents_keep_transfer_history(), + DocumentTypeRef::V3(v3) => v3.documents_keep_transfer_history(), } } @@ -423,6 +473,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_keep_purchase_history(), DocumentTypeRef::V1(v1) => v1.documents_keep_purchase_history(), DocumentTypeRef::V2(v2) => v2.documents_keep_purchase_history(), + DocumentTypeRef::V3(v3) => v3.documents_keep_purchase_history(), } } @@ -431,6 +482,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_keep_pricing_history(), DocumentTypeRef::V1(v1) => v1.documents_keep_pricing_history(), DocumentTypeRef::V2(v2) => v2.documents_keep_pricing_history(), + DocumentTypeRef::V3(v3) => v3.documents_keep_pricing_history(), } } @@ -439,6 +491,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_mutable(), DocumentTypeRef::V1(v1) => v1.documents_mutable(), DocumentTypeRef::V2(v2) => v2.documents_mutable(), + DocumentTypeRef::V3(v3) => v3.documents_mutable(), } } @@ -447,6 +500,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_can_be_deleted(), DocumentTypeRef::V1(v1) => v1.documents_can_be_deleted(), DocumentTypeRef::V2(v2) => v2.documents_can_be_deleted(), + DocumentTypeRef::V3(v3) => v3.documents_can_be_deleted(), } } @@ -455,6 +509,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.documents_transferable(), DocumentTypeRef::V1(v1) => v1.documents_transferable(), DocumentTypeRef::V2(v2) => v2.documents_transferable(), + DocumentTypeRef::V3(v3) => v3.documents_transferable(), } } @@ -463,6 +518,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.trade_mode(), DocumentTypeRef::V1(v1) => v1.trade_mode(), DocumentTypeRef::V2(v2) => v2.trade_mode(), + DocumentTypeRef::V3(v3) => v3.trade_mode(), } } @@ -471,6 +527,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.creation_restriction_mode(), DocumentTypeRef::V1(v1) => v1.creation_restriction_mode(), DocumentTypeRef::V2(v2) => v2.creation_restriction_mode(), + DocumentTypeRef::V3(v3) => v3.creation_restriction_mode(), } } @@ -479,6 +536,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.data_contract_id(), DocumentTypeRef::V1(v1) => v1.data_contract_id(), DocumentTypeRef::V2(v2) => v2.data_contract_id(), + DocumentTypeRef::V3(v3) => v3.data_contract_id(), } } @@ -487,6 +545,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentTypeRef::V1(v1) => v1.requires_identity_encryption_bounded_key(), DocumentTypeRef::V2(v2) => v2.requires_identity_encryption_bounded_key(), + DocumentTypeRef::V3(v3) => v3.requires_identity_encryption_bounded_key(), } } @@ -495,6 +554,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentTypeRef::V1(v1) => v1.requires_identity_decryption_bounded_key(), DocumentTypeRef::V2(v2) => v2.requires_identity_decryption_bounded_key(), + DocumentTypeRef::V3(v3) => v3.requires_identity_decryption_bounded_key(), } } @@ -503,6 +563,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.security_level_requirement(), DocumentTypeRef::V1(v1) => v1.security_level_requirement(), DocumentTypeRef::V2(v2) => v2.security_level_requirement(), + DocumentTypeRef::V3(v3) => v3.security_level_requirement(), } } @@ -512,6 +573,7 @@ impl DocumentTypeV0Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => v0.json_schema_validator_ref(), DocumentTypeRef::V1(v1) => v1.json_schema_validator_ref(), DocumentTypeRef::V2(v2) => v2.json_schema_validator_ref(), + DocumentTypeRef::V3(v3) => v3.json_schema_validator_ref(), } } } @@ -521,6 +583,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.name(), DocumentTypeMutRef::V1(v1) => v1.name(), DocumentTypeMutRef::V2(v2) => v2.name(), + DocumentTypeMutRef::V3(v3) => v3.name(), } } @@ -529,6 +592,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.schema(), DocumentTypeMutRef::V1(v1) => v1.schema(), DocumentTypeMutRef::V2(v2) => v2.schema(), + DocumentTypeMutRef::V3(v3) => v3.schema(), } } @@ -537,6 +601,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.clone().schema_owned(), DocumentTypeMutRef::V1(v1) => v1.clone().schema_owned(), DocumentTypeMutRef::V2(v2) => v2.clone().schema_owned(), + DocumentTypeMutRef::V3(v3) => v3.clone().schema_owned(), } } @@ -545,6 +610,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.indexes(), DocumentTypeMutRef::V1(v1) => v1.indexes(), DocumentTypeMutRef::V2(v2) => v2.indexes(), + DocumentTypeMutRef::V3(v3) => v3.indexes(), } } @@ -553,6 +619,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.find_contested_index(), DocumentTypeMutRef::V1(v1) => v1.find_contested_index(), DocumentTypeMutRef::V2(v2) => v2.find_contested_index(), + DocumentTypeMutRef::V3(v3) => v3.find_contested_index(), } } @@ -561,6 +628,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.index_structure(), DocumentTypeMutRef::V1(v1) => v1.index_structure(), DocumentTypeMutRef::V2(v2) => v2.index_structure(), + DocumentTypeMutRef::V3(v3) => v3.index_structure(), } } @@ -569,6 +637,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.flattened_properties(), DocumentTypeMutRef::V1(v1) => v1.flattened_properties(), DocumentTypeMutRef::V2(v2) => v2.flattened_properties(), + DocumentTypeMutRef::V3(v3) => v3.flattened_properties(), } } @@ -577,6 +646,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.properties(), DocumentTypeMutRef::V1(v1) => v1.properties(), DocumentTypeMutRef::V2(v2) => v2.properties(), + DocumentTypeMutRef::V3(v3) => v3.properties(), } } @@ -585,6 +655,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.identifier_paths(), DocumentTypeMutRef::V1(v1) => v1.identifier_paths(), DocumentTypeMutRef::V2(v2) => v2.identifier_paths(), + DocumentTypeMutRef::V3(v3) => v3.identifier_paths(), } } @@ -593,6 +664,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.binary_paths(), DocumentTypeMutRef::V1(v1) => v1.binary_paths(), DocumentTypeMutRef::V2(v2) => v2.binary_paths(), + DocumentTypeMutRef::V3(v3) => v3.binary_paths(), } } @@ -601,6 +673,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.required_fields(), DocumentTypeMutRef::V1(v1) => v1.required_fields(), DocumentTypeMutRef::V2(v2) => v2.required_fields(), + DocumentTypeMutRef::V3(v3) => v3.required_fields(), } } @@ -609,6 +682,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.transient_fields(), DocumentTypeMutRef::V1(v1) => v1.transient_fields(), DocumentTypeMutRef::V2(v2) => v2.transient_fields(), + DocumentTypeMutRef::V3(v3) => v3.transient_fields(), } } @@ -617,6 +691,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_keep_history(), DocumentTypeMutRef::V1(v1) => v1.documents_keep_history(), DocumentTypeMutRef::V2(v2) => v2.documents_keep_history(), + DocumentTypeMutRef::V3(v3) => v3.documents_keep_history(), } } @@ -625,6 +700,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_keep_transfer_history(), DocumentTypeMutRef::V1(v1) => v1.documents_keep_transfer_history(), DocumentTypeMutRef::V2(v2) => v2.documents_keep_transfer_history(), + DocumentTypeMutRef::V3(v3) => v3.documents_keep_transfer_history(), } } @@ -633,6 +709,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_keep_purchase_history(), DocumentTypeMutRef::V1(v1) => v1.documents_keep_purchase_history(), DocumentTypeMutRef::V2(v2) => v2.documents_keep_purchase_history(), + DocumentTypeMutRef::V3(v3) => v3.documents_keep_purchase_history(), } } @@ -641,6 +718,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_keep_pricing_history(), DocumentTypeMutRef::V1(v1) => v1.documents_keep_pricing_history(), DocumentTypeMutRef::V2(v2) => v2.documents_keep_pricing_history(), + DocumentTypeMutRef::V3(v3) => v3.documents_keep_pricing_history(), } } @@ -649,6 +727,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_mutable(), DocumentTypeMutRef::V1(v1) => v1.documents_mutable(), DocumentTypeMutRef::V2(v2) => v2.documents_mutable(), + DocumentTypeMutRef::V3(v3) => v3.documents_mutable(), } } @@ -657,6 +736,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_can_be_deleted(), DocumentTypeMutRef::V1(v1) => v1.documents_can_be_deleted(), DocumentTypeMutRef::V2(v2) => v2.documents_can_be_deleted(), + DocumentTypeMutRef::V3(v3) => v3.documents_can_be_deleted(), } } @@ -665,6 +745,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.documents_transferable(), DocumentTypeMutRef::V1(v1) => v1.documents_transferable(), DocumentTypeMutRef::V2(v2) => v2.documents_transferable(), + DocumentTypeMutRef::V3(v3) => v3.documents_transferable(), } } @@ -673,6 +754,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.trade_mode(), DocumentTypeMutRef::V1(v1) => v1.trade_mode(), DocumentTypeMutRef::V2(v2) => v2.trade_mode(), + DocumentTypeMutRef::V3(v3) => v3.trade_mode(), } } @@ -681,6 +763,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.creation_restriction_mode(), DocumentTypeMutRef::V1(v1) => v1.creation_restriction_mode(), DocumentTypeMutRef::V2(v2) => v2.creation_restriction_mode(), + DocumentTypeMutRef::V3(v3) => v3.creation_restriction_mode(), } } @@ -689,6 +772,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.data_contract_id(), DocumentTypeMutRef::V1(v1) => v1.data_contract_id(), DocumentTypeMutRef::V2(v2) => v2.data_contract_id(), + DocumentTypeMutRef::V3(v3) => v3.data_contract_id(), } } @@ -697,6 +781,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.requires_identity_encryption_bounded_key(), DocumentTypeMutRef::V1(v1) => v1.requires_identity_encryption_bounded_key(), DocumentTypeMutRef::V2(v2) => v2.requires_identity_encryption_bounded_key(), + DocumentTypeMutRef::V3(v3) => v3.requires_identity_encryption_bounded_key(), } } @@ -705,6 +790,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.requires_identity_decryption_bounded_key(), DocumentTypeMutRef::V1(v1) => v1.requires_identity_decryption_bounded_key(), DocumentTypeMutRef::V2(v2) => v2.requires_identity_decryption_bounded_key(), + DocumentTypeMutRef::V3(v3) => v3.requires_identity_decryption_bounded_key(), } } @@ -713,6 +799,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.security_level_requirement(), DocumentTypeMutRef::V1(v1) => v1.security_level_requirement(), DocumentTypeMutRef::V2(v2) => v2.security_level_requirement(), + DocumentTypeMutRef::V3(v3) => v3.security_level_requirement(), } } @@ -722,6 +809,7 @@ impl DocumentTypeV0Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.json_schema_validator_ref(), DocumentTypeMutRef::V1(v1) => v1.json_schema_validator_ref(), DocumentTypeMutRef::V2(v2) => v2.json_schema_validator_ref(), + DocumentTypeMutRef::V3(v3) => v3.json_schema_validator_ref(), } } } @@ -732,6 +820,7 @@ impl DocumentTypeV0Setters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(v0) => v0.set_data_contract_id(data_contract_id), DocumentTypeMutRef::V1(v1) => v1.set_data_contract_id(data_contract_id), DocumentTypeMutRef::V2(v2) => v2.set_data_contract_id(data_contract_id), + DocumentTypeMutRef::V3(v3) => v3.set_data_contract_id(data_contract_id), } } } @@ -742,6 +831,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_creation_token_cost(), DocumentType::V2(v2) => v2.document_creation_token_cost(), + DocumentType::V3(v3) => v3.document_creation_token_cost(), } } @@ -750,6 +840,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_replacement_token_cost(), DocumentType::V2(v2) => v2.document_replacement_token_cost(), + DocumentType::V3(v3) => v3.document_replacement_token_cost(), } } @@ -758,6 +849,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_deletion_token_cost(), DocumentType::V2(v2) => v2.document_deletion_token_cost(), + DocumentType::V3(v3) => v3.document_deletion_token_cost(), } } @@ -766,6 +858,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_transfer_token_cost(), DocumentType::V2(v2) => v2.document_transfer_token_cost(), + DocumentType::V3(v3) => v3.document_transfer_token_cost(), } } @@ -774,6 +867,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_update_price_token_cost(), DocumentType::V2(v2) => v2.document_update_price_token_cost(), + DocumentType::V3(v3) => v3.document_update_price_token_cost(), } } @@ -782,6 +876,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(v1) => v1.document_purchase_token_cost(), DocumentType::V2(v2) => v2.document_purchase_token_cost(), + DocumentType::V3(v3) => v3.document_purchase_token_cost(), } } @@ -790,6 +885,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => vec![], DocumentType::V1(v1) => v1.all_document_token_costs(), DocumentType::V2(v2) => v2.all_document_token_costs(), + DocumentType::V3(v3) => v3.all_document_token_costs(), } } @@ -800,6 +896,7 @@ impl DocumentTypeV1Getters for DocumentType { DocumentType::V0(_) => BTreeMap::new(), DocumentType::V1(v1) => v1.all_external_token_costs_contract_tokens(), DocumentType::V2(v2) => v2.all_external_token_costs_contract_tokens(), + DocumentType::V3(v3) => v3.all_external_token_costs_contract_tokens(), } } } @@ -810,6 +907,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_creation_token_cost(), DocumentTypeRef::V2(v2) => v2.document_creation_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_creation_token_cost(), } } @@ -818,6 +916,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_replacement_token_cost(), DocumentTypeRef::V2(v2) => v2.document_replacement_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_replacement_token_cost(), } } @@ -826,6 +925,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_deletion_token_cost(), DocumentTypeRef::V2(v2) => v2.document_deletion_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_deletion_token_cost(), } } @@ -834,6 +934,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_transfer_token_cost(), DocumentTypeRef::V2(v2) => v2.document_transfer_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_transfer_token_cost(), } } @@ -842,6 +943,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_update_price_token_cost(), DocumentTypeRef::V2(v2) => v2.document_update_price_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_update_price_token_cost(), } } @@ -850,6 +952,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(v1) => v1.document_purchase_token_cost(), DocumentTypeRef::V2(v2) => v2.document_purchase_token_cost(), + DocumentTypeRef::V3(v3) => v3.document_purchase_token_cost(), } } @@ -858,6 +961,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => vec![], DocumentTypeRef::V1(v1) => v1.all_document_token_costs(), DocumentTypeRef::V2(v2) => v2.all_document_token_costs(), + DocumentTypeRef::V3(v3) => v3.all_document_token_costs(), } } @@ -868,6 +972,7 @@ impl DocumentTypeV1Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => BTreeMap::new(), DocumentTypeRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), DocumentTypeRef::V2(v2) => v2.all_external_token_costs_contract_tokens(), + DocumentTypeRef::V3(v3) => v3.all_external_token_costs_contract_tokens(), } } } @@ -878,6 +983,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_creation_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_creation_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_creation_token_cost(), } } @@ -886,6 +992,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_replacement_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_replacement_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_replacement_token_cost(), } } @@ -894,6 +1001,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_deletion_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_deletion_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_deletion_token_cost(), } } @@ -902,6 +1010,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_transfer_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_transfer_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_transfer_token_cost(), } } @@ -910,6 +1019,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_update_price_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_update_price_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_update_price_token_cost(), } } @@ -918,6 +1028,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(v1) => v1.document_purchase_token_cost(), DocumentTypeMutRef::V2(v2) => v2.document_purchase_token_cost(), + DocumentTypeMutRef::V3(v3) => v3.document_purchase_token_cost(), } } @@ -926,6 +1037,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => vec![], DocumentTypeMutRef::V1(v1) => v1.all_document_token_costs(), DocumentTypeMutRef::V2(v2) => v2.all_document_token_costs(), + DocumentTypeMutRef::V3(v3) => v3.all_document_token_costs(), } } @@ -936,6 +1048,7 @@ impl DocumentTypeV1Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => BTreeMap::new(), DocumentTypeMutRef::V1(v1) => v1.all_external_token_costs_contract_tokens(), DocumentTypeMutRef::V2(v2) => v2.all_external_token_costs_contract_tokens(), + DocumentTypeMutRef::V3(v3) => v3.all_external_token_costs_contract_tokens(), } } } @@ -946,6 +1059,7 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V0(_) => false, DocumentType::V1(_) => false, DocumentType::V2(v2) => v2.documents_countable(), + DocumentType::V3(v3) => v3.documents_countable(), } } @@ -954,6 +1068,7 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V0(_) => false, DocumentType::V1(_) => false, DocumentType::V2(v2) => v2.range_countable(), + DocumentType::V3(v3) => v3.range_countable(), } } @@ -962,6 +1077,7 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V0(_) => None, DocumentType::V1(_) => None, DocumentType::V2(v2) => v2.documents_summable(), + DocumentType::V3(v3) => v3.documents_summable(), } } @@ -970,6 +1086,7 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V0(_) => false, DocumentType::V1(_) => false, DocumentType::V2(v2) => v2.range_summable(), + DocumentType::V3(v3) => v3.range_summable(), } } @@ -978,14 +1095,7 @@ impl DocumentTypeV2Getters for DocumentType { DocumentType::V0(_) => false, DocumentType::V1(_) => false, DocumentType::V2(v2) => v2.index_only(), - } - } - - fn documents_can_be_erased(&self) -> bool { - match self { - DocumentType::V0(_) => false, - DocumentType::V1(_) => false, - DocumentType::V2(v2) => v2.documents_can_be_erased(), + DocumentType::V3(v3) => v3.index_only(), } } } @@ -996,6 +1106,7 @@ impl DocumentTypeV2Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(_) => { /* no-op */ } DocumentType::V2(v2) => v2.set_documents_countable(countable), + DocumentType::V3(v3) => v3.set_documents_countable(countable), } } @@ -1004,6 +1115,7 @@ impl DocumentTypeV2Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(_) => { /* no-op */ } DocumentType::V2(v2) => v2.set_range_countable(range_countable), + DocumentType::V3(v3) => v3.set_range_countable(range_countable), } } @@ -1012,6 +1124,7 @@ impl DocumentTypeV2Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(_) => { /* no-op */ } DocumentType::V2(v2) => v2.set_documents_summable(property), + DocumentType::V3(v3) => v3.set_documents_summable(property), } } @@ -1020,6 +1133,7 @@ impl DocumentTypeV2Setters for DocumentType { DocumentType::V0(_) => { /* no-op */ } DocumentType::V1(_) => { /* no-op */ } DocumentType::V2(v2) => v2.set_range_summable(range_summable), + DocumentType::V3(v3) => v3.set_range_summable(range_summable), } } } @@ -1030,6 +1144,7 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => false, DocumentTypeRef::V1(_) => false, DocumentTypeRef::V2(v2) => v2.documents_countable(), + DocumentTypeRef::V3(v3) => v3.documents_countable(), } } @@ -1038,6 +1153,7 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => false, DocumentTypeRef::V1(_) => false, DocumentTypeRef::V2(v2) => v2.range_countable(), + DocumentTypeRef::V3(v3) => v3.range_countable(), } } @@ -1046,6 +1162,7 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => None, DocumentTypeRef::V1(_) => None, DocumentTypeRef::V2(v2) => v2.documents_summable(), + DocumentTypeRef::V3(v3) => v3.documents_summable(), } } @@ -1054,6 +1171,7 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => false, DocumentTypeRef::V1(_) => false, DocumentTypeRef::V2(v2) => v2.range_summable(), + DocumentTypeRef::V3(v3) => v3.range_summable(), } } @@ -1062,14 +1180,7 @@ impl DocumentTypeV2Getters for DocumentTypeRef<'_> { DocumentTypeRef::V0(_) => false, DocumentTypeRef::V1(_) => false, DocumentTypeRef::V2(v2) => v2.index_only(), - } - } - - fn documents_can_be_erased(&self) -> bool { - match self { - DocumentTypeRef::V0(_) => false, - DocumentTypeRef::V1(_) => false, - DocumentTypeRef::V2(v2) => v2.documents_can_be_erased(), + DocumentTypeRef::V3(v3) => v3.index_only(), } } } @@ -1080,6 +1191,7 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => false, DocumentTypeMutRef::V1(_) => false, DocumentTypeMutRef::V2(v2) => v2.documents_countable(), + DocumentTypeMutRef::V3(v3) => v3.documents_countable(), } } @@ -1088,6 +1200,7 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => false, DocumentTypeMutRef::V1(_) => false, DocumentTypeMutRef::V2(v2) => v2.range_countable(), + DocumentTypeMutRef::V3(v3) => v3.range_countable(), } } @@ -1096,6 +1209,7 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => None, DocumentTypeMutRef::V1(_) => None, DocumentTypeMutRef::V2(v2) => v2.documents_summable(), + DocumentTypeMutRef::V3(v3) => v3.documents_summable(), } } @@ -1104,6 +1218,7 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => false, DocumentTypeMutRef::V1(_) => false, DocumentTypeMutRef::V2(v2) => v2.range_summable(), + DocumentTypeMutRef::V3(v3) => v3.range_summable(), } } @@ -1112,14 +1227,40 @@ impl DocumentTypeV2Getters for DocumentTypeMutRef<'_> { DocumentTypeMutRef::V0(_) => false, DocumentTypeMutRef::V1(_) => false, DocumentTypeMutRef::V2(v2) => v2.index_only(), + DocumentTypeMutRef::V3(v3) => v3.index_only(), } } +} + +impl DocumentTypeV3Getters for DocumentType { + fn documents_can_be_erased(&self) -> bool { + match self { + DocumentType::V0(_) => false, + DocumentType::V1(_) => false, + DocumentType::V2(_) => false, + DocumentType::V3(v3) => v3.documents_can_be_erased(), + } + } +} + +impl DocumentTypeV3Getters for DocumentTypeRef<'_> { + fn documents_can_be_erased(&self) -> bool { + match self { + DocumentTypeRef::V0(_) => false, + DocumentTypeRef::V1(_) => false, + DocumentTypeRef::V2(_) => false, + DocumentTypeRef::V3(v3) => v3.documents_can_be_erased(), + } + } +} +impl DocumentTypeV3Getters for DocumentTypeMutRef<'_> { fn documents_can_be_erased(&self) -> bool { match self { DocumentTypeMutRef::V0(_) => false, DocumentTypeMutRef::V1(_) => false, - DocumentTypeMutRef::V2(v2) => v2.documents_can_be_erased(), + DocumentTypeMutRef::V2(_) => false, + DocumentTypeMutRef::V3(v3) => v3.documents_can_be_erased(), } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs index 0f4a952d4a5..68017106e45 100644 --- a/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/v2/mod.rs @@ -30,11 +30,6 @@ pub trait DocumentTypeV2Getters { /// each terminating in an `Item` keyed by the index's `terminal` /// property. Only what is in the indexes exists and is recoverable. fn index_only(&self) -> bool; - - /// Returns whether a deleted document of this type may have its retained - /// revisions purged by an erase transition. Always false for document - /// types parsed by an earlier grammar, which has no such keyword. - fn documents_can_be_erased(&self) -> bool; } /// Trait providing setters for DocumentTypeV2-specific fields. diff --git a/packages/rs-dpp/src/data_contract/document_type/accessors/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/accessors/v3/mod.rs new file mode 100644 index 00000000000..803e4b1f9c3 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/accessors/v3/mod.rs @@ -0,0 +1,8 @@ +/// Getters introduced with document type version 3: the keep-history document +/// lifecycle keywords of meta-schema v4. +pub trait DocumentTypeV3Getters { + /// Returns whether a deleted document of this type may have its retained + /// revisions purged by an erase transition. Always false for a document + /// type parsed by an earlier grammar, which has no such keyword. + fn documents_can_be_erased(&self) -> bool; +} diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 821d753cebc..49e4f2b7fca 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -36,6 +36,7 @@ use crate::data_contract::document_type::token_costs::v0::TokenCostsV0; use crate::data_contract::document_type::token_costs::TokenCosts; use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::v3::DocumentTypeV3; use crate::data_contract::document_type::{property_names, DocumentType}; use crate::data_contract::errors::DataContractError; use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; @@ -89,7 +90,7 @@ use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValid #[cfg(feature = "validation")] use crate::validation::meta_validators::{ DOCUMENT_META_SCHEMA_V0, DOCUMENT_META_SCHEMA_V1, DOCUMENT_META_SCHEMA_V2, - DOCUMENT_META_SCHEMA_V3, + DOCUMENT_META_SCHEMA_V3, DOCUMENT_META_SCHEMA_V4, }; #[cfg(feature = "validation")] use jsonschema::JSONSchema; @@ -290,10 +291,11 @@ pub(super) fn select_document_meta_schema( 1 => &*DOCUMENT_META_SCHEMA_V1, 2 => &*DOCUMENT_META_SCHEMA_V2, 3 => &*DOCUMENT_META_SCHEMA_V3, + 4 => &*DOCUMENT_META_SCHEMA_V4, version => { return Err(ProtocolError::UnknownVersionMismatch { method: method_name.to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2, 3, 4], received: version, }) } @@ -1946,7 +1948,7 @@ pub(super) fn apply_doctype_aggregates( /// /// Runs before the core parse for the same reason as /// [`parse_index_only_keyword`]: the core takes `schema` by value. Only the -/// generation-3 driver calls this; earlier generations have no such keyword +/// generation-4 driver calls this; earlier generations have no such keyword /// and their meta-schemas reject it under `full_validation`. pub(super) fn parse_can_be_erased_keyword(schema: &Value) -> Result { let schema_map_opt = schema.to_map().ok(); @@ -1976,7 +1978,7 @@ pub(super) fn parse_can_be_erased_keyword(schema: &Value) -> Result Result<(), ProtocolError> { @@ -2023,7 +2025,7 @@ pub(super) fn apply_can_be_erased( /// needs changes to the contested machinery itself, so until then the two are /// kept apart at contract registration. pub(super) fn reject_contested_keep_history( - document_type: &DocumentTypeV2, + document_type: &DocumentTypeV3, name: &str, ) -> Result<(), ProtocolError> { if !document_type.documents_keep_history { diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index 5c006d220d9..1f00b199937 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -22,6 +22,7 @@ mod v0; mod v1; mod v2; mod v3; +mod v4; const NOT_ALLOWED_SYSTEM_PROPERTIES: [&str; 1] = ["$id"]; @@ -104,9 +105,22 @@ impl DocumentType { validation_operations, platform_version, ), + 4 => DocumentType::try_from_schema_v4( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + validation_operations, + platform_version, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "try_from_schema".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2, 3, 4], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs index 65f664341ee..f0ceb8f5dcc 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/keep_history_tests.rs @@ -1,11 +1,6 @@ -//! The keep-history document lifecycle as generation 3 admits it: a -//! keep-history type may allow deletion, may additionally allow erasure, and -//! may not carry a contested index. +//! Regression tests for the `documentsKeepHistory` + `canBeDeleted` +//! cross-flag rule added in `try_from_schema` v3 (protocol version 14). use super::*; -use crate::data_contract::document_type::accessors::{ - DocumentTypeV0Getters, DocumentTypeV2Getters, -}; -use crate::data_contract::errors::DataContractError; use platform_value::platform_value; /// Parses through the public dispatcher at the given protocol version so @@ -55,258 +50,159 @@ fn keep_history_deletable_schema() -> Value { }) } -/// The two settings describe different things and no longer contradict each -/// other: history decides what is retained, deletion decides what ordinary -/// reads can still see. A delete on such a type removes the document from -/// every ordinary read and leaves its revisions readable. +/// `documentsKeepHistory: true` + `canBeDeleted: true` is +/// self-contradictory: rs-drive unconditionally refuses to delete +/// a document whose type keeps history +/// (`InvalidDeletionOfDocumentThatKeepsHistory`), so `canBeDeleted: +/// true` advertises a capability the storage layer will always +/// reject. The parser must reject the combination at contract +/// creation time so an SDK user gets a clean validation error +/// instead of the delete failing as an internal error at execution. +/// +/// With the `validation` feature enabled the rejection must surface +/// as `ProtocolError::ConsensusError` (not bare +/// `ProtocolError::DataContractError`) — drive-abci's +/// `transform_into_action_v0` only turns the consensus variant into +/// a clean invalid (paid) transition with a bump action; the +/// data-contract-error variant propagates as an internal execution +/// error in validator mode. #[test] -fn should_accept_a_keep_history_type_that_allows_deletion() { - let document_type = parse(keep_history_deletable_schema()) - .expect("a keep-history type may allow deletion from protocol version 14"); - assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); +fn doctype_keep_history_with_can_be_deleted_rejected() { + let result = parse(keep_history_deletable_schema()); assert!( - !document_type.documents_can_be_erased(), - "erasure is not implied by deletion; it has to be asked for" + result.is_err(), + "documentsKeepHistory: true + canBeDeleted: true must be rejected" + ); + let err = result.unwrap_err(); + let msg = format!("{:?}", err); + assert!( + msg.contains("documentsKeepHistory") && msg.contains("canBeDeleted"), + "error must reference both documentsKeepHistory and canBeDeleted; got {msg}" + ); + #[cfg(feature = "validation")] + assert!( + matches!(err, ProtocolError::ConsensusError(_)), + "with `validation` feature the rejection must be ProtocolError::ConsensusError so \ + drive-abci's transform_into_action turns it into an invalid (paid) transition \ + with a bump action rather than propagating as an internal execution error; got \ + {err:?}" ); } -/// The contract config defaults `canBeDeleted` to true, so a keep-history type -/// that says nothing about deletion is deletable — the same value it carried -/// before protocol 14, now with a delete that works. -#[test] -fn should_accept_a_keep_history_type_that_omits_the_delete_flag() { - let schema = platform_value!({ - "type": "object", - "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "additionalProperties": false, - "documentsKeepHistory": true, - }); - let document_type = parse(schema).expect("the omitted flag defaults to deletable"); - assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); -} - -/// A keep-history type that withholds deletion is still valid: its documents -/// are append-only and can never leave ordinary reads. +/// Omitting `canBeDeleted` exercises the contract-config default boundary: +/// the latest config defaults it to `true`, so a keep-history document type +/// remains contradictory and must be rejected during full validation. #[test] -fn should_accept_a_keep_history_type_that_withholds_deletion() { +fn doctype_keep_history_with_can_be_deleted_omitted_rejected() { let schema = platform_value!({ "type": "object", "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, + "label": { + "type": "string", + "maxLength": 50, + "position": 0, + }, }, "additionalProperties": false, "documentsKeepHistory": true, - "canBeDeleted": false, }); - let document_type = parse(schema).expect("an append-only keep-history type is valid"); - assert!(document_type.documents_keep_history()); - assert!(!document_type.documents_can_be_deleted()); -} - -/// The released parsers never saw the rule and must keep parsing the same -/// schemas the same way, so a historical block replays identically. -#[test] -fn should_accept_a_keep_history_deletable_type_at_released_protocol_versions() { - for protocol in [12, 13] { - let document_type = parse_at_version(keep_history_deletable_schema(), protocol, true) - .unwrap_or_else(|error| { - panic!("protocol {protocol} must still accept the schema: {error:?}") - }); - assert!(document_type.documents_keep_history()); - assert!(document_type.documents_can_be_deleted()); - } -} - -fn erasable_schema(keep_history: bool, can_be_deleted: bool, can_be_erased: bool) -> Value { - platform_value!({ - "type": "object", - "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "additionalProperties": false, - "documentsKeepHistory": keep_history, - "canBeDeleted": can_be_deleted, - "canBeErased": can_be_erased, - }) -} - -#[test] -fn should_accept_an_erasable_keep_history_deletable_type() { - let document_type = - parse(erasable_schema(true, true, true)).expect("the admitted combination must parse"); - assert!(document_type.documents_can_be_erased()); -} - -/// Erasure purges retained revisions of a document that has already been -/// deleted, so a type with no history has nothing to purge and a type whose -/// documents can never be deleted can never reach the state erasure acts on. -/// Neither is silently ignored. -#[test] -fn should_reject_erasure_without_history_or_without_deletion() { - for (keep_history, can_be_deleted) in [(false, true), (true, false), (false, false)] { - let error = parse(erasable_schema(keep_history, can_be_deleted, true)).unwrap_err(); - let message = format!("{error:?}"); - assert!( - message.contains("canBeErased"), - "the error must name the flag the author has to change; got {message}" - ); - assert_contract_structure_consensus_error(&error); - } -} - -/// A signed contract carrying a refused combination must be a paid rejection -/// with a nonce bump, and only the consensus variant becomes one: the bare -/// data-contract variant escapes the transformation as an internal execution -/// error, which costs the submitter nothing and reports nothing useful. -#[cfg(feature = "validation")] -fn assert_contract_structure_consensus_error(error: &ProtocolError) { - use crate::consensus::basic::BasicError; - use crate::consensus::ConsensusError; - - let ProtocolError::ConsensusError(consensus) = error else { - panic!("expected a consensus error, got {error:?}"); - }; + let result = parse(schema); assert!( - matches!( - consensus.as_ref(), - ConsensusError::BasicError(BasicError::ContractError( - DataContractError::InvalidContractStructure(_) - )) - ), - "expected an invalid-contract-structure basic error, got {consensus:?}" + result.is_err(), + "omitted canBeDeleted must default to true and conflict with documentsKeepHistory" ); -} - -#[cfg(not(feature = "validation"))] -fn assert_contract_structure_consensus_error(error: &ProtocolError) { + let msg = format!("{:?}", result.unwrap_err()); assert!( - matches!( - error, - ProtocolError::DataContractError(DataContractError::InvalidContractStructure(_)) - ), - "without the validation feature the structural variant is all there is, got {error:?}" + msg.contains("documentsKeepHistory") && msg.contains("canBeDeleted"), + "error must reference both documentsKeepHistory and defaulted canBeDeleted; got {msg}" ); } -/// Erasure defaults to off: a type that says nothing about it cannot have its -/// revisions purged. -#[test] -fn should_default_erasure_to_off() { - let document_type = parse(keep_history_deletable_schema()).expect("parses"); - assert!(!document_type.documents_can_be_erased()); -} - -/// The keyword does not exist in the earlier meta-schemas, so a contract that -/// declares it is refused there rather than silently parsing without it. +/// `documentsKeepHistory: true` + `canBeDeleted: true` is rejected +/// ONLY when `full_validation: true`. With `full_validation: false` +/// (the restore / migration / cache-warmup path) the same schema must +/// parse cleanly so already-deployed contradictory contracts continue +/// to load at v14+ — the drive-abci delete-transition guard turns +/// their deletes into clean invalid (paid) transitions instead of +/// rejecting them as internal errors at the contract-load layer. #[test] -fn should_reject_the_erasure_keyword_at_released_protocol_versions() { - for protocol in [12, 13] { - assert!( - parse_at_version(erasable_schema(true, true, true), protocol, true).is_err(), - "protocol {protocol} has no canBeErased keyword" - ); - } +fn doctype_keep_history_with_can_be_deleted_accepted_without_full_validation() { + let document_type = parse_at_version(keep_history_deletable_schema(), 14, false).expect( + "documentsKeepHistory: true + canBeDeleted: true must be accepted when \ + full_validation: false so already-deployed contradictory contracts continue to load", + ); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); } -/// A contested resource is awarded outside transition validation, at an id -/// derived from the winner rather than from the contested values, so that award -/// can land on an id whose retained history already exists. The two are kept -/// apart at registration until the contested machinery can handle it. +/// Protocol version 12 routes to `try_from_schema` v2, which has no +/// cross-flag rule — the combination must stay accepted there even under +/// full validation, because v12 is released and consensus-frozen: +/// contracts accepted at v12 must replay identically. #[test] -fn should_reject_a_keep_history_type_that_carries_a_contested_index() { - let schema = platform_value!({ - "type": "object", - "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "indices": [ - { - "name": "byLabel", - "properties": [{"label": "asc"}], - "unique": true, - "contested": { - "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], - "resolution": 0, - }, - }, - ], - "required": ["label"], - "additionalProperties": false, - "documentsMutable": false, - "documentsKeepHistory": true, - "canBeDeleted": false, - }); - let error = parse(schema).expect_err("a contested keep-history type must be refused"); - let message = format!("{error:?}"); - assert!( - message.contains("contested"), - "the error must say which index is the problem; got {message}" +fn doctype_keep_history_with_can_be_deleted_accepted_at_protocol_version_12() { + let document_type = parse_at_version(keep_history_deletable_schema(), 12, true).expect( + "documentsKeepHistory: true + canBeDeleted: true must stay accepted at protocol \ + version 12 (consensus-frozen v2 parser) for replay compatibility", ); - assert_contract_structure_consensus_error(&error); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); } -/// The refusal is a registration rule. A contract that carries the combination -/// was registered under a protocol that allowed it, and every stored contract -/// is loaded through the structural parse, so that parse must keep reading it -/// or the activation that migrates its history could not even start. +/// Guard against an over-broad fix: `documentsKeepHistory: true` + +/// `canBeDeleted: false` is consistent (the doctype is append-only) +/// and must continue to parse cleanly. The sibling omitted-key regression +/// covers the distinct default-`true` boundary and therefore expects +/// rejection rather than acceptance. #[test] -fn should_still_load_a_legacy_contested_keep_history_type_without_validation() { +fn doctype_keep_history_with_can_be_deleted_false_accepted() { let schema = platform_value!({ "type": "object", "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "indices": [ - { - "name": "byLabel", - "properties": [{"label": "asc"}], - "unique": true, - "contested": { - "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], - "resolution": 0, - }, + "label": { + "type": "string", + "maxLength": 50, + "position": 0, }, - ], - "required": ["label"], + }, "additionalProperties": false, - "documentsMutable": false, "documentsKeepHistory": true, "canBeDeleted": false, }); - let document_type = parse_at_version(schema, 14, false) - .expect("a stored contract with this combination must still load"); + let document_type = parse(schema) + .expect("documentsKeepHistory: true + canBeDeleted: false is consistent and must parse"); assert!(document_type.documents_keep_history()); + assert!(!document_type.documents_can_be_deleted()); } -/// The same index without history is unaffected: the refusal is about the -/// combination, not about contested indexes. +/// Symmetric guard: `canBeDeleted: true` on a non-keep-history +/// doctype must continue to parse cleanly. Catches a predicate that +/// triggers on `canBeDeleted: true` alone instead of the AND. #[test] -fn should_accept_a_contested_index_without_history() { +fn doctype_can_be_deleted_without_keep_history_accepted() { let schema = platform_value!({ "type": "object", "properties": { - "label": {"type": "string", "maxLength": 50, "position": 0}, - }, - "indices": [ - { - "name": "byLabel", - "properties": [{"label": "asc"}], - "unique": true, - "contested": { - "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], - "resolution": 0, - }, + "label": { + "type": "string", + "maxLength": 50, + "position": 0, }, - ], - "required": ["label"], + }, "additionalProperties": false, - "documentsMutable": false, + "canBeDeleted": true, }); - parse(schema).expect("a contested index alone is fine"); + let document_type = + parse(schema).expect("canBeDeleted: true without documentsKeepHistory must parse cleanly"); + assert!(!document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); +} +#[test] +fn should_accept_contradictory_keep_history_schema_at_protocol_13() { + let document_type = parse_at_version(keep_history_deletable_schema(), 13, true) + .expect("released protocol 13 must still accept the schema"); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); } fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { @@ -321,30 +217,46 @@ fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { }) } -/// A keep-history type may withdraw deletion, and only in that direction. #[test] -fn should_allow_a_keep_history_type_to_withdraw_deletion_at_protocol_14() { - let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); - let new = parse_at_version(repair_schema(true, false), 14, true).unwrap(); - let result = old - .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) - .expect("the update must reach a consensus result"); - assert!(result.is_valid(), "rejected: {:?}", result.errors); +fn should_repair_legacy_keep_history_delete_flag_at_protocol_14() { + let legacy_schemas = [ + repair_schema(true, true), + platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + }), + ]; + for schema in legacy_schemas { + let old = parse_at_version(schema, 13, true).unwrap(); + let repaired = parse_at_version(repair_schema(true, false), 14, true).unwrap(); + let result = old + .as_ref() + .validate_update(repaired.as_ref(), 2, PlatformVersion::get(14).unwrap()) + .expect("repair must reach a consensus result"); + assert!(result.is_valid(), "repair rejected: {:?}", result.errors); + } } #[test] -fn should_preserve_the_immutable_delete_flag_through_protocol_13() { +fn should_preserve_legacy_keep_history_repair_rejection_through_protocol_13() { for protocol in [12, 13] { let old = parse_at_version(repair_schema(true, true), protocol, true).unwrap(); - let new = parse_at_version(repair_schema(true, false), protocol, true).unwrap(); + let repaired = parse_at_version(repair_schema(true, false), protocol, true).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(protocol).unwrap()) + .validate_update( + repaired.as_ref(), + 2, + PlatformVersion::get(protocol).unwrap(), + ) .unwrap(); assert!( !result.is_valid(), - "protocol {protocol} must still refuse the change" + "protocol {protocol} must still reject repair" ); } } @@ -373,48 +285,8 @@ fn should_reject_other_delete_and_history_flag_changes_at_protocol_14() { } } -/// Erasability is immutable in both directions. Widening it hands an -/// irreversible operation to a type registered without it; narrowing it after a -/// first chunk has removed revisions strands a partially erased document. -#[test] -fn should_reject_every_change_to_the_erasure_flag() { - for (before, after) in [(false, true), (true, false)] { - let old = parse_at_version(erasable_schema(true, true, before), 14, false).unwrap(); - let new = parse_at_version(erasable_schema(true, true, after), 14, false).unwrap(); - let result = old - .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) - .unwrap(); - assert!( - !result.is_valid(), - "unexpectedly accepted canBeErased {before} -> {after}" - ); - } -} - -/// An erasable type can never stop being deletable, whichever way the update is -/// spelled: erasure applies to deleted documents only, and since erasability -/// itself cannot be withdrawn, such a type could never reach a state erasure -/// acts on again. Keeping the flag makes the type unparseable; dropping it -/// changes an immutable flag. -#[test] -fn should_refuse_to_withdraw_deletion_from_an_erasable_type() { - assert!( - parse_at_version(erasable_schema(true, false, true), 14, false).is_err(), - "an erasable type that forbids deletion is not a valid type at all" - ); - - let old = parse_at_version(erasable_schema(true, true, true), 14, false).unwrap(); - let new = parse_at_version(repair_schema(true, false), 14, false).unwrap(); - let result = old - .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(14).unwrap()) - .unwrap(); - assert!(!result.is_valid(), "an erasable type kept its delete flag"); -} - #[test] -fn should_reject_incompatible_properties_during_a_delete_flag_withdrawal() { +fn should_reject_incompatible_properties_during_keep_history_repair() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); let new = parse_at_version( platform_value!({ @@ -436,12 +308,12 @@ fn should_reject_incompatible_properties_during_a_delete_flag_withdrawal() { .unwrap(); assert!( !result.is_valid(), - "the withdrawal must not bypass schema compatibility" + "repair must not bypass schema compatibility" ); } #[test] -fn should_reject_mutability_change_during_a_delete_flag_withdrawal() { +fn should_reject_mutability_change_during_keep_history_repair() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); let mut schema = repair_schema(true, false); schema.set_value("documentsMutable", false.into()).unwrap(); @@ -452,12 +324,12 @@ fn should_reject_mutability_change_during_a_delete_flag_withdrawal() { .unwrap(); assert!( !result.is_valid(), - "the withdrawal must not bypass other configuration checks" + "repair must not bypass other configuration checks" ); } #[test] -fn should_still_validate_a_property_named_can_be_deleted_during_a_withdrawal() { +fn should_still_validate_property_named_can_be_deleted_during_keep_history_repair() { let mut old_schema = repair_schema(true, true); old_schema .set_value( diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs index b84d9937f43..8be6995123a 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/mod.rs @@ -15,7 +15,8 @@ //! index-key length ceilings, and the constants they are derived from. use crate::data_contract::config::DataContractConfig; - +use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; +use crate::data_contract::document_type::class_methods::consensus_or_protocol_data_contract_error; // Only the ranked key-length rule below names `Index`, and it is validation-only. #[cfg(feature = "validation")] use crate::data_contract::document_type::index::Index; @@ -24,7 +25,7 @@ use crate::data_contract::document_type::index::IndexGrammarAdmissions; use crate::data_contract::document_type::property::DocumentPropertyType; use crate::data_contract::document_type::v2::DocumentTypeV2; use crate::data_contract::document_type::DocumentType; - +use crate::data_contract::errors::DataContractError; use crate::data_contract::{TokenConfiguration, TokenContractPosition}; use crate::validation::operations::ProtocolValidationOperation; use crate::version::PlatformVersion; @@ -37,7 +38,7 @@ use crate::consensus::basic::data_contract::InvalidIndexedPropertyConstraintErro use super::common; -mod ranked_prefix_overlap; +pub(in crate::data_contract::document_type::class_methods::try_from_schema) mod ranked_prefix_overlap; use ranked_prefix_overlap::validate_no_ranked_prefix_overlap; /// grovedb's ceiling on the key of an entry stored directly under an @@ -214,10 +215,10 @@ fn validate_ranked_index_property_key_length( /// exactly equivalent there — this alias exists only so the call site can pass /// the check unconditionally. #[cfg(feature = "validation")] -const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = +pub(in crate::data_contract::document_type::class_methods::try_from_schema) const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = validate_ranked_index_property_key_length; #[cfg(not(feature = "validation"))] -const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = +pub(in crate::data_contract::document_type::class_methods::try_from_schema) const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = common::no_ranked_index_key_length_check; /// Parses a document type schema through the generation-3 grammar: the @@ -226,13 +227,9 @@ const RANKED_INDEX_KEY_LENGTH_CHECK: common::RankedIndexKeyLengthCheck = /// /// This parser is only reachable from protocol version 14+ (via /// CONTRACT_VERSIONS_V6). -/// -/// Generation 3 admits `documentsKeepHistory: true` together with -/// `canBeDeleted: true`: a delete on such a type removes the document from -/// ordinary reads while its retained revisions stay readable. It also admits -/// the `canBeErased` keyword, which additionally allows a deleted document's -/// revisions to be purged, and it refuses a keep-history type that carries a -/// contested index. +/// Full validation rejects keep-history document types that allow deletion. +/// Stored contracts bypass this check so legacy contradictory schemas remain +/// readable and can be repaired by setting `canBeDeleted: false` on update. #[allow(clippy::too_many_arguments)] fn try_from_schema_generation_3( data_contract_id: Identifier, @@ -251,7 +248,6 @@ fn try_from_schema_generation_3( // consumes `schema`. let aggregates = common::parse_doctype_aggregate_keywords(&schema, name)?; let index_only = common::parse_index_only_keyword(&schema)?; - let can_be_erased = common::parse_can_be_erased_keyword(&schema)?; let v1 = common::parse_document_type_core( data_contract_id, @@ -313,15 +309,20 @@ fn try_from_schema_generation_3( // aggregate flags (they describe the primary-key tree, which an // indexOnly type does not have), so it has to see them already applied. common::apply_index_only(&mut v2, index_only, name)?; - // Reads `canBeDeleted` off the parsed result rather than the raw schema, so - // it sees the value resolved against the contract config default (`true` - // when the key is omitted). - common::apply_can_be_erased(&mut v2, can_be_erased, name)?; - // A registration-time rule only: a contract that already carries this - // combination was registered under an earlier protocol, and the structural - // parse that loads stored contracts must keep reading it. - if full_validation { - common::reject_contested_keep_history(&v2, name)?; + + // The flags are read from the parsed result (not the raw schema) so + // the check sees `canBeDeleted` resolved against the contract config + // default (`true` when the key is omitted). + if full_validation && v2.documents_keep_history() && v2.documents_can_be_deleted() { + return Err(consensus_or_protocol_data_contract_error( + DataContractError::InvalidContractStructure(format!( + "document type \"{}\" sets both `documentsKeepHistory: true` and \ + `canBeDeleted: true`, but the storage layer unconditionally refuses to \ + delete a document whose type keeps history. Set `canBeDeleted` to false or \ + disable `documentsKeepHistory`.", + name, + )), + )); } Ok(v2) diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/ranked_prefix_overlap.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/ranked_prefix_overlap.rs index e27fd0c6c7c..23cfa2b77be 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/ranked_prefix_overlap.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/ranked_prefix_overlap.rs @@ -65,7 +65,7 @@ fn shares_leading_levels(ranked: &Index, other: &Index, depth: usize) -> bool { }) } -pub(super) fn validate_no_ranked_prefix_overlap( +pub(in crate::data_contract::document_type::class_methods::try_from_schema) fn validate_no_ranked_prefix_overlap( indices: &BTreeMap, ) -> Result<(), ProtocolError> { for ranked in indices.values() { diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs new file mode 100644 index 00000000000..fc65f129100 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs @@ -0,0 +1,495 @@ +//! The keep-history document lifecycle as generation 4 admits it: a +//! keep-history type may allow deletion, may additionally allow erasure, and +//! may not carry a contested index. +use super::*; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV3Getters, +}; +use crate::data_contract::errors::DataContractError; +use platform_value::platform_value; + +/// Parses through the public dispatcher at the given protocol version so +/// the test exercises the same `try_from_schema` version routing consensus +/// code uses (generation 4 at protocol version 15, 3 at 14, 2 at 12 and 13). +fn parse_at_version( + schema: Value, + protocol_version: u32, + full_validation: bool, +) -> Result { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected platform version"); + let config = DataContractConfig::default_for_version(platform_version) + .expect("default config available"); + DocumentType::try_from_schema( + Identifier::new([1; 32]), + 1, + config.version(), + "test_doc", + schema, + None, + &BTreeMap::new(), + &config, + full_validation, + &mut vec![], + platform_version, + ) +} + +fn parse(schema: Value) -> Result { + parse_at_version(schema, 15, true) +} + +fn keep_history_deletable_schema() -> Value { + platform_value!({ + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 50, + "position": 0, + }, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + "canBeDeleted": true, + }) +} + +/// The two settings describe different things and no longer contradict each +/// other: history decides what is retained, deletion decides what ordinary +/// reads can still see. A delete on such a type removes the document from +/// every ordinary read and leaves its revisions readable. +#[test] +fn should_accept_a_keep_history_type_that_allows_deletion() { + let document_type = parse(keep_history_deletable_schema()) + .expect("a keep-history type may allow deletion from protocol version 15"); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); + assert!( + !document_type.documents_can_be_erased(), + "erasure is not implied by deletion; it has to be asked for" + ); +} + +/// The contract config defaults `canBeDeleted` to true, so a keep-history type +/// that says nothing about deletion is deletable — the same value it carried +/// before protocol 14, now with a delete that works. +#[test] +fn should_accept_a_keep_history_type_that_omits_the_delete_flag() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + }); + let document_type = parse(schema).expect("the omitted flag defaults to deletable"); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); +} + +/// A keep-history type that withholds deletion is still valid: its documents +/// are append-only and can never leave ordinary reads. +#[test] +fn should_accept_a_keep_history_type_that_withholds_deletion() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }); + let document_type = parse(schema).expect("an append-only keep-history type is valid"); + assert!(document_type.documents_keep_history()); + assert!(!document_type.documents_can_be_deleted()); +} + +/// The released parsers must keep parsing the same schemas the same way, so a +/// historical block replays identically: generation 2 never saw a rule about +/// the pair, and generation 3 refuses it under full validation. +#[test] +fn should_keep_the_released_parsers_verdict_on_a_keep_history_deletable_type() { + for protocol in [12, 13] { + let document_type = parse_at_version(keep_history_deletable_schema(), protocol, true) + .unwrap_or_else(|error| { + panic!("protocol {protocol} must still accept the schema: {error:?}") + }); + assert!(document_type.documents_keep_history()); + assert!(document_type.documents_can_be_deleted()); + } + assert!( + parse_at_version(keep_history_deletable_schema(), 14, true).is_err(), + "protocol 14 refuses a deletable keep-history type under full validation" + ); +} + +fn erasable_schema(keep_history: bool, can_be_deleted: bool, can_be_erased: bool) -> Value { + platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": keep_history, + "canBeDeleted": can_be_deleted, + "canBeErased": can_be_erased, + }) +} + +#[test] +fn should_accept_an_erasable_keep_history_deletable_type() { + let document_type = + parse(erasable_schema(true, true, true)).expect("the admitted combination must parse"); + assert!(document_type.documents_can_be_erased()); +} + +/// Erasure purges retained revisions of a document that has already been +/// deleted, so a type with no history has nothing to purge and a type whose +/// documents can never be deleted can never reach the state erasure acts on. +/// Neither is silently ignored. +#[test] +fn should_reject_erasure_without_history_or_without_deletion() { + for (keep_history, can_be_deleted) in [(false, true), (true, false), (false, false)] { + let error = parse(erasable_schema(keep_history, can_be_deleted, true)).unwrap_err(); + let message = format!("{error:?}"); + assert!( + message.contains("canBeErased"), + "the error must name the flag the author has to change; got {message}" + ); + assert_contract_structure_consensus_error(&error); + } +} + +/// A signed contract carrying a refused combination must be a paid rejection +/// with a nonce bump, and only the consensus variant becomes one: the bare +/// data-contract variant escapes the transformation as an internal execution +/// error, which costs the submitter nothing and reports nothing useful. +#[cfg(feature = "validation")] +fn assert_contract_structure_consensus_error(error: &ProtocolError) { + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + + let ProtocolError::ConsensusError(consensus) = error else { + panic!("expected a consensus error, got {error:?}"); + }; + assert!( + matches!( + consensus.as_ref(), + ConsensusError::BasicError(BasicError::ContractError( + DataContractError::InvalidContractStructure(_) + )) + ), + "expected an invalid-contract-structure basic error, got {consensus:?}" + ); +} + +#[cfg(not(feature = "validation"))] +fn assert_contract_structure_consensus_error(error: &ProtocolError) { + assert!( + matches!( + error, + ProtocolError::DataContractError(DataContractError::InvalidContractStructure(_)) + ), + "without the validation feature the structural variant is all there is, got {error:?}" + ); +} + +/// Erasure defaults to off: a type that says nothing about it cannot have its +/// revisions purged. +#[test] +fn should_default_erasure_to_off() { + let document_type = parse(keep_history_deletable_schema()).expect("parses"); + assert!(!document_type.documents_can_be_erased()); +} + +/// The keyword does not exist in the earlier meta-schemas, so a contract that +/// declares it is refused there rather than silently parsing without it. +#[test] +fn should_reject_the_erasure_keyword_at_released_protocol_versions() { + for protocol in [12, 13, 14] { + assert!( + parse_at_version(erasable_schema(true, true, true), protocol, true).is_err(), + "protocol {protocol} has no canBeErased keyword" + ); + } +} + +/// A contested resource is awarded outside transition validation, at an id +/// derived from the winner rather than from the contested values, so that award +/// can land on an id whose retained history already exists. The two are kept +/// apart at registration until the contested machinery can handle it. +#[test] +fn should_reject_a_keep_history_type_that_carries_a_contested_index() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], + "additionalProperties": false, + "documentsMutable": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }); + let error = parse(schema).expect_err("a contested keep-history type must be refused"); + let message = format!("{error:?}"); + assert!( + message.contains("contested"), + "the error must say which index is the problem; got {message}" + ); + assert_contract_structure_consensus_error(&error); +} + +/// The refusal is a registration rule. A contract that carries the combination +/// was registered under a protocol that allowed it, and every stored contract +/// is loaded through the structural parse, so that parse must keep reading it +/// or the activation that migrates its history could not even start. +#[test] +fn should_still_load_a_legacy_contested_keep_history_type_without_validation() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], + "additionalProperties": false, + "documentsMutable": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }); + let document_type = parse_at_version(schema, 15, false) + .expect("a stored contract with this combination must still load"); + assert!(document_type.documents_keep_history()); +} + +/// The same index without history is unaffected: the refusal is about the +/// combination, not about contested indexes. +#[test] +fn should_accept_a_contested_index_without_history() { + let schema = platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "indices": [ + { + "name": "byLabel", + "properties": [{"label": "asc"}], + "unique": true, + "contested": { + "fieldMatches": [{"field": "label", "regexPattern": "^[a-z]{3,10}$"}], + "resolution": 0, + }, + }, + ], + "required": ["label"], + "additionalProperties": false, + "documentsMutable": false, + }); + parse(schema).expect("a contested index alone is fine"); +} + +fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { + platform_value!({ + "type": "object", + "properties": { + "label": {"type": "string", "maxLength": 50, "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": keep_history, + "canBeDeleted": can_be_deleted, + }) +} + +/// A keep-history type may withdraw deletion, and only in that direction. +#[test] +fn should_allow_a_keep_history_type_to_withdraw_deletion_at_protocol_15() { + let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); + let new = parse_at_version(repair_schema(true, false), 15, true).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .expect("the update must reach a consensus result"); + assert!(result.is_valid(), "rejected: {:?}", result.errors); +} + +#[test] +fn should_preserve_the_immutable_delete_flag_through_protocol_13() { + for protocol in [12, 13] { + let old = parse_at_version(repair_schema(true, true), protocol, true).unwrap(); + let new = parse_at_version(repair_schema(true, false), protocol, true).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(protocol).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "protocol {protocol} must still refuse the change" + ); + } +} + +#[test] +fn should_reject_other_delete_and_history_flag_changes_at_protocol_15() { + for (old_flags, new_flags) in [ + ((false, true), (false, false)), + ((false, false), (false, true)), + ((true, false), (true, true)), + ((true, true), (false, false)), + ((false, true), (true, false)), + ] { + let old = parse_at_version(repair_schema(old_flags.0, old_flags.1), 13, true).unwrap(); + // A caller may already have a parsed contract; update validation must + // enforce immutability even without the full-validation parser guard. + let new = parse_at_version(repair_schema(new_flags.0, new_flags.1), 15, false).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "unexpectedly accepted {old_flags:?} -> {new_flags:?}" + ); + } +} + +/// Erasability is immutable in both directions. Widening it hands an +/// irreversible operation to a type registered without it; narrowing it after a +/// first chunk has removed revisions strands a partially erased document. +#[test] +fn should_reject_every_change_to_the_erasure_flag() { + for (before, after) in [(false, true), (true, false)] { + let old = parse_at_version(erasable_schema(true, true, before), 15, false).unwrap(); + let new = parse_at_version(erasable_schema(true, true, after), 15, false).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "unexpectedly accepted canBeErased {before} -> {after}" + ); + } +} + +/// An erasable type can never stop being deletable, whichever way the update is +/// spelled: erasure applies to deleted documents only, and since erasability +/// itself cannot be withdrawn, such a type could never reach a state erasure +/// acts on again. Keeping the flag makes the type unparseable; dropping it +/// changes an immutable flag. +#[test] +fn should_refuse_to_withdraw_deletion_from_an_erasable_type() { + assert!( + parse_at_version(erasable_schema(true, false, true), 15, false).is_err(), + "an erasable type that forbids deletion is not a valid type at all" + ); + + let old = parse_at_version(erasable_schema(true, true, true), 15, false).unwrap(); + let new = parse_at_version(repair_schema(true, false), 15, false).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!(!result.is_valid(), "an erasable type kept its delete flag"); +} + +#[test] +fn should_reject_incompatible_properties_during_a_delete_flag_withdrawal() { + let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); + let new = parse_at_version( + platform_value!({ + "type": "object", + "properties": { + "label": {"type": "integer", "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + }), + 15, + true, + ) + .unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "the withdrawal must not bypass schema compatibility" + ); +} + +#[test] +fn should_reject_mutability_change_during_a_delete_flag_withdrawal() { + let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); + let mut schema = repair_schema(true, false); + schema.set_value("documentsMutable", false.into()).unwrap(); + let new = parse_at_version(schema, 15, true).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "the withdrawal must not bypass other configuration checks" + ); +} + +#[test] +fn should_still_validate_a_property_named_can_be_deleted_during_a_withdrawal() { + let mut old_schema = repair_schema(true, true); + old_schema + .set_value( + "properties", + platform_value!({ + "canBeDeleted": {"type": "string", "maxLength": 50, "position": 0}, + }), + ) + .unwrap(); + let mut new_schema = old_schema.clone(); + new_schema.set_value("canBeDeleted", false.into()).unwrap(); + new_schema + .set_value( + "properties", + platform_value!({ + "canBeDeleted": {"type": "integer", "position": 0}, + }), + ) + .unwrap(); + let old = parse_at_version(old_schema, 13, true).unwrap(); + let new = parse_at_version(new_schema, 15, true).unwrap(); + let result = old + .as_ref() + .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .unwrap(); + assert!( + !result.is_valid(), + "only the top-level config flag may be stripped" + ); +} diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/mod.rs new file mode 100644 index 00000000000..e1748a856db --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/mod.rs @@ -0,0 +1,170 @@ +//! Document-type parser **generation 4** — protocol version 15 and later. +//! +//! Generation 4 is generation 3 plus the keep-history document lifecycle: a +//! keep-history type may allow deletion, may additionally declare +//! `canBeErased`, and may not carry a contested index. Its index grammar, the +//! ranked index-key ceilings and the prefix-overlap rule are generation 3's, +//! reused from that module rather than copied. +//! +//! It exists as its own generation — rather than as a version gate inside the +//! shipped ones — because that is what keeps a historical block from ever +//! picking up grammar that did not exist when it was produced: the dispatcher +//! only routes here from `try_from_schema: 4`, and the grammar this module +//! admits is spelled out below as literals rather than looked up in a version +//! table. +//! +//! The parsing steps themselves are shared with the earlier generations in +//! [`super::common`]. + +use crate::data_contract::config::DataContractConfig; +use crate::data_contract::document_type::index::IndexGrammarAdmissions; +use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::v3::DocumentTypeV3; +use crate::data_contract::document_type::DocumentType; +use crate::data_contract::{TokenConfiguration, TokenContractPosition}; +use crate::validation::operations::ProtocolValidationOperation; +use crate::version::PlatformVersion; +use crate::ProtocolError; +use platform_value::{Identifier, Value}; +use std::collections::BTreeMap; + +use super::common; +use super::v3::ranked_prefix_overlap::validate_no_ranked_prefix_overlap; +use super::v3::RANKED_INDEX_KEY_LENGTH_CHECK; + +/// Parses a document type schema through the generation-4 grammar: the whole +/// generation-3 grammar plus the keep-history document lifecycle keywords. +/// +/// This parser is only reachable from protocol version 15+ (via +/// CONTRACT_VERSIONS_V7). +/// +/// Generation 4 admits `documentsKeepHistory: true` together with +/// `canBeDeleted: true`: a delete on such a type removes the document from +/// ordinary reads while its retained revisions stay readable. It also admits +/// the `canBeErased` keyword, which additionally allows a deleted document's +/// revisions to be purged, and it refuses a keep-history type that carries a +/// contested index. +#[allow(clippy::too_many_arguments)] +fn try_from_schema_generation_4( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, +) -> Result { + // Read the aggregate and indexOnly keywords before the core parser + // consumes `schema`. + let aggregates = common::parse_doctype_aggregate_keywords(&schema, name)?; + let index_only = common::parse_index_only_keyword(&schema)?; + let can_be_erased = common::parse_can_be_erased_keyword(&schema)?; + + let v1 = common::parse_document_type_core( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + // Lets the core default omitted index terminals to `$ownerId` before + // it builds the index structure, so the structure's level info is + // born normalized (`apply_index_only` below validates the + // already-normalized set). + index_only, + validation_operations, + &common::ParserGeneration { + // Generation 4 exists if and only if `document_type_schema` is 4: + // CONTRACT_VERSIONS_V7 is the only table that selects this parser, + // and it is the only table naming meta-schema v4. So every constant + // here is a property of the generation, not of a platform version, + // and none of them is read out of a table. + document_type_schema_version: 4, + // Meta-schema v4 carries the `keeps*History` flags forward, so they + // are unconditionally part of this generation's grammar. + admit_history: true, + // Count indexes arrived at PV12; every version selecting this + // generation is far past that boundary. + admit_count_indexes: true, + meta_schema_method_name: "DocumentType::try_from_schema_v4 (document_type_schema)", + // The index grammar is generation 3's, read from the shared + // generation → admission mapping so the registration-cost re-parse + // can never drift from what this parser accepts. The ranked key + // ceilings and the prefix-overlap rule are generation 3's as well. + admit_ranked: IndexGrammarAdmissions::for_schema_generation(4).ranked, + ranked_index_key_length_check: RANKED_INDEX_KEY_LENGTH_CHECK, + ranked_index_structure_check: validate_no_ranked_prefix_overlap, + admit_time_range: IndexGrammarAdmissions::for_schema_generation(4).time_range, + admit_index_terminal: IndexGrammarAdmissions::for_schema_generation(4).terminal, + admit_index_preallocated: IndexGrammarAdmissions::for_schema_generation(4).preallocated, + admit_index_skip_if_absent: IndexGrammarAdmissions::for_schema_generation(4) + .skip_if_absent, + }, + platform_version, + )?; + + let mut v2: DocumentTypeV2 = v1.into(); + common::apply_doctype_aggregates(&mut v2, aggregates, name)?; + // After the aggregates: `apply_index_only` rejects the doctype-level + // aggregate flags (they describe the primary-key tree, which an + // indexOnly type does not have), so it has to see them already applied. + common::apply_index_only(&mut v2, index_only, name)?; + // The lifecycle keyword lives on the document type version this + // generation produces. + let mut v3: DocumentTypeV3 = v2.into(); + // Reads `canBeDeleted` off the parsed result rather than the raw schema, so + // it sees the value resolved against the contract config default (`true` + // when the key is omitted). + common::apply_can_be_erased(&mut v3, can_be_erased, name)?; + // A registration-time rule only: a contract that already carries this + // combination was registered under an earlier protocol, and the structural + // parse that loads stored contracts must keep reading it. + if full_validation { + common::reject_contested_keep_history(&v3, name)?; + } + + Ok(v3) +} + +impl DocumentType { + /// Dispatches to this module's generation-4 parser and wraps the result. + #[allow(clippy::too_many_arguments)] + pub(in crate::data_contract::document_type::class_methods) fn try_from_schema_v4( + data_contract_id: Identifier, + data_contract_system_version: u16, + contract_config_version: u16, + name: &str, + schema: Value, + schema_defs: Option<&BTreeMap>, + token_configurations: &BTreeMap, + data_contact_config: &DataContractConfig, + full_validation: bool, + validation_operations: &mut impl Extend, + platform_version: &PlatformVersion, + ) -> Result { + try_from_schema_generation_4( + data_contract_id, + data_contract_system_version, + contract_config_version, + name, + schema, + schema_defs, + token_configurations, + data_contact_config, + full_validation, + validation_operations, + platform_version, + ) + .map(DocumentType::V3) + } +} + +#[cfg(test)] +mod keep_history_tests; diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs index a9cac78317a..54c30c3ff3c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/mod.rs @@ -794,6 +794,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.requires_revision() } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.requires_revision() + } } } @@ -808,6 +811,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.initial_revision() } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.initial_revision() + } } } @@ -822,6 +828,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.top_level_indices() } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.top_level_indices() + } } } @@ -836,6 +845,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.top_level_indices_of_contested_unique_indexes() } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.top_level_indices_of_contested_unique_indexes() + } } } @@ -851,6 +863,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.index_structure() } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.index_structure() + } } } @@ -869,6 +884,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.unique_id_for_document_field(index_level, base_event) } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.unique_id_for_document_field(index_level, base_event) + } } } @@ -883,6 +901,9 @@ mod tests { crate::data_contract::document_type::DocumentTypeRef::V2(v2) => { v2.sanitize_document_properties(properties) } + crate::data_contract::document_type::DocumentTypeRef::V3(v3) => { + v3.sanitize_document_properties(properties) + } } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs index 6cff95b8b33..fb9bf77de5b 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/common/mod.rs @@ -170,28 +170,6 @@ impl DocumentTypeRef<'_> { ); } - // Erasability is immutable in both directions. Widening it would give - // an operation over already-stored revisions to a type registered - // without it; narrowing it after a first erase chunk has irreversibly - // removed revisions would strand a partially erased, invisible - // document forever. A document type parsed by an earlier grammar has - // no such keyword and reports false on both sides, so this check is - // inert before protocol version 14. - if new_document_type.documents_can_be_erased() != self.documents_can_be_erased() { - return SimpleConsensusValidationResult::new_with_error( - DocumentTypeUpdateError::new( - self.data_contract_id(), - self.name(), - format!( - "document type can not change whether its documents can be erased: changing from {} to {}", - self.documents_can_be_erased(), - new_document_type.documents_can_be_erased() - ), - ) - .into(), - ); - } - if new_document_type.documents_keep_history() != self.documents_keep_history() { return SimpleConsensusValidationResult::new_with_error( DocumentTypeUpdateError::new( diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/mod.rs index 6e4418eaeeb..dcf484fe480 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/mod.rs @@ -6,6 +6,7 @@ use platform_version::version::PlatformVersion; mod common; mod v0; mod v1; +mod v2; impl DocumentTypeRef<'_> { /// Verify that the update to the document type is valid. @@ -27,9 +28,10 @@ impl DocumentTypeRef<'_> { { 0 => self.validate_update_v0(new_document_type, platform_version), 1 => self.validate_update_v1(new_document_type, new_contract_version, platform_version), + 2 => self.validate_update_v2(new_document_type, new_contract_version, platform_version), version => Err(ProtocolError::UnknownVersionMismatch { method: "validate_update".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs index d90ad99d01b..ba7ce64a0b0 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs @@ -23,9 +23,7 @@ use crate::consensus::basic::data_contract::{ DataContractInvalidIndexDefinitionUpdateError, DataContractInvalidRequiredFieldsUpdateError, }; -use crate::data_contract::document_type::accessors::{ - DocumentTypeV0Getters, DocumentTypeV2Getters, -}; +use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; use crate::data_contract::document_type::DocumentTypeRef; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; @@ -41,20 +39,14 @@ impl DocumentTypeRef<'_> { new_contract_version: u32, platform_version: &PlatformVersion, ) -> Result { - // A keep-history type may withdraw deletion, and only in that - // direction: turning delete back on would hand an operation over - // already-stored documents to a type registered without it. The - // narrowing is refused while the type is erasable, because erase - // applies to deleted documents only and erasability is itself - // immutable, so such a type could never reach a state erase acts on - // again. Every other config and schema check still runs, and v0 stays - // immutable. + // Legacy keep-history types advertised deletes that Drive never allowed. + // Permit only true -> false for that flag while keeping history enabled. + // Every other config and schema check still runs, and v0 stays immutable. let options = UpdateValidationOptions { allow_history_delete_repair: self.documents_keep_history() && new_document_type.documents_keep_history() && self.documents_can_be_deleted() - && !new_document_type.documents_can_be_deleted() - && !self.documents_can_be_erased(), + && !new_document_type.documents_can_be_deleted(), }; let result = self.validate_config_with_options(new_document_type, &options); @@ -101,7 +93,7 @@ impl DocumentTypeRef<'_> { /// existing properties stay frozen by the schema compatibility differ; /// this check judges the top-level `required` key, which is stripped /// from the diff exactly like `indices`. - fn validate_required_fields_update( + pub(super) fn validate_required_fields_update( &self, new_document_type: DocumentTypeRef, new_contract_version: u32, @@ -179,7 +171,7 @@ impl DocumentTypeRef<'_> { /// on-disk subtrees. Compare the definitions by index name — the /// comparison must not depend on where a changed index's name sorts /// relative to the document type's other indexes. - fn validate_index_definitions_unchanged( + pub(super) fn validate_index_definitions_unchanged( &self, new_document_type: DocumentTypeRef, ) -> SimpleConsensusValidationResult { diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs new file mode 100644 index 00000000000..1c881301131 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs @@ -0,0 +1,86 @@ +//! Protocol v15 generation of document type update validation. +//! +//! v2 is v1 plus the keep-history document lifecycle. Two rules are new: +//! +//! * `canBeErased` is immutable in both directions. Widening it would give an +//! operation over already-stored revisions to a type registered without it; +//! narrowing it after a first erase chunk has irreversibly removed revisions +//! would strand a partially erased, invisible document forever. +//! * A keep-history type may withdraw deletion (`canBeDeleted: true -> false`) +//! while keeping history, and only in that direction, and only while it is +//! not erasable: erase applies to deleted documents only and erasability is +//! itself immutable, so an erasable type that stopped allowing deletion could +//! never again reach a state erase acts on. +//! +//! Every other check is v1's: index definitions compared by name, byte array +//! encodings frozen, top-level requiredness frozen except for `requiredSince` +//! additions, and schema compatibility. + +use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV3Getters, +}; +use crate::data_contract::document_type::DocumentTypeRef; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::common::UpdateValidationOptions; + +impl DocumentTypeRef<'_> { + #[inline(always)] + pub(super) fn validate_update_v2( + &self, + new_document_type: DocumentTypeRef, + new_contract_version: u32, + platform_version: &PlatformVersion, + ) -> Result { + if new_document_type.documents_can_be_erased() != self.documents_can_be_erased() { + return Ok(SimpleConsensusValidationResult::new_with_error( + DocumentTypeUpdateError::new( + self.data_contract_id(), + self.name(), + format!( + "document type can not change whether its documents can be erased: changing from {} to {}", + self.documents_can_be_erased(), + new_document_type.documents_can_be_erased() + ), + ) + .into(), + )); + } + + let options = UpdateValidationOptions { + allow_history_delete_repair: self.documents_keep_history() + && new_document_type.documents_keep_history() + && self.documents_can_be_deleted() + && !new_document_type.documents_can_be_deleted() + && !self.documents_can_be_erased(), + }; + let result = self.validate_config_with_options(new_document_type, &options); + + if !result.is_valid() { + return Ok(result); + } + + let result = self.validate_index_definitions_unchanged(new_document_type); + + if !result.is_valid() { + return Ok(result); + } + + let result = self.validate_byte_array_encoding_stability(new_document_type); + + if !result.is_valid() { + return Ok(result); + } + + let result = self.validate_required_fields_update(new_document_type, new_contract_version); + + if !result.is_valid() { + return Ok(result); + } + + self.validate_schema_with_options(new_document_type, platform_version, &options) + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs index c1b95d40cdc..76d59f2b210 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/versioned_methods.rs @@ -3,6 +3,7 @@ use crate::data_contract::document_type::methods::DocumentTypeBasicMethods; use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::v3::DocumentTypeV3; use crate::data_contract::document_type::{ DocumentPropertyType, DocumentType, DocumentTypeRef, Index, CONTRACT_VERSION_STAMP_MAX_SIZE, DEFAULT_HASH_SIZE, MAX_INDEX_SIZE, @@ -780,6 +781,7 @@ impl DocumentTypeV0MethodsVersioned for DocumentTypeV0 {} impl DocumentTypeV0MethodsVersioned for DocumentTypeV1 {} impl DocumentTypeV0MethodsVersioned for DocumentTypeV2 {} +impl DocumentTypeV0MethodsVersioned for DocumentTypeV3 {} impl DocumentTypeV0MethodsVersioned for DocumentType {} impl DocumentTypeV0MethodsVersioned for DocumentTypeRef<'_> {} diff --git a/packages/rs-dpp/src/data_contract/document_type/mod.rs b/packages/rs-dpp/src/data_contract/document_type/mod.rs index a40213c2ddd..289324e9ac4 100644 --- a/packages/rs-dpp/src/data_contract/document_type/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/mod.rs @@ -21,6 +21,7 @@ pub(crate) use validate_required_since_within_contract_version::validate_require pub mod v0; pub mod v1; pub mod v2; +pub mod v3; #[cfg(feature = "validation")] pub(crate) mod validator; @@ -30,6 +31,7 @@ use crate::data_contract::document_type::methods::{ use crate::data_contract::document_type::v0::DocumentTypeV0; use crate::data_contract::document_type::v1::DocumentTypeV1; use crate::data_contract::document_type::v2::DocumentTypeV2; +use crate::data_contract::document_type::v3::DocumentTypeV3; use crate::document::Document; use crate::fee::Credits; use crate::version::PlatformVersion; @@ -129,8 +131,8 @@ pub(crate) mod property_names { /// Doctype-level flag declaring that a deleted document of this type may /// have its retained revisions purged by an erase transition. Requires /// `documentsKeepHistory: true` and `canBeDeleted: true`, defaults to - /// false, and is immutable across contract updates. Meta-schema v3+ - /// (protocol version 14). + /// false, and is immutable across contract updates. Meta-schema v4+ + /// (protocol version 15). pub const CAN_BE_ERASED: &str = "canBeErased"; } @@ -139,6 +141,7 @@ pub enum DocumentTypeRef<'a> { V0(&'a DocumentTypeV0), V1(&'a DocumentTypeV1), V2(&'a DocumentTypeV2), + V3(&'a DocumentTypeV3), } #[derive(Debug)] @@ -146,6 +149,7 @@ pub enum DocumentTypeMutRef<'a> { V0(&'a mut DocumentTypeV0), V1(&'a mut DocumentTypeV1), V2(&'a mut DocumentTypeV2), + V3(&'a mut DocumentTypeV3), } #[allow(clippy::large_enum_variant)] @@ -154,6 +158,7 @@ pub enum DocumentType { V0(DocumentTypeV0), V1(DocumentTypeV1), V2(DocumentTypeV2), + V3(DocumentTypeV3), } impl DocumentType { @@ -162,6 +167,7 @@ impl DocumentType { DocumentType::V0(v0) => DocumentTypeRef::V0(v0), DocumentType::V1(v1) => DocumentTypeRef::V1(v1), DocumentType::V2(v2) => DocumentTypeRef::V2(v2), + DocumentType::V3(v3) => DocumentTypeRef::V3(v3), } } @@ -170,6 +176,7 @@ impl DocumentType { DocumentType::V0(v0) => DocumentTypeMutRef::V0(v0), DocumentType::V1(v1) => DocumentTypeMutRef::V1(v1), DocumentType::V2(v2) => DocumentTypeMutRef::V2(v2), + DocumentType::V3(v3) => DocumentTypeMutRef::V3(v3), } } @@ -188,6 +195,9 @@ impl DocumentType { DocumentType::V2(v2) => { v2.prefunded_voting_balance_for_document(document, platform_version) } + DocumentType::V3(v3) => { + v3.prefunded_voting_balance_for_document(document, platform_version) + } } } } @@ -198,6 +208,7 @@ impl DocumentTypeRef<'_> { DocumentTypeRef::V0(v0) => DocumentType::V0((*v0).to_owned()), DocumentTypeRef::V1(v1) => DocumentType::V1((*v1).to_owned()), DocumentTypeRef::V2(v2) => DocumentType::V2((*v2).to_owned()), + DocumentTypeRef::V3(v3) => DocumentType::V3((*v3).to_owned()), } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs index 5c4b95d1922..bcd7c487f75 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v1/mod.rs @@ -198,7 +198,8 @@ mod tests { match dt { crate::data_contract::document_type::DocumentType::V0(v0) => v0, crate::data_contract::document_type::DocumentType::V1(_) - | crate::data_contract::document_type::DocumentType::V2(_) => { + | crate::data_contract::document_type::DocumentType::V2(_) + | crate::data_contract::document_type::DocumentType::V3(_) => { panic!("expected V0 from first() version routing") } } diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs index 5b3e7f0bff7..3964010b1d9 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v2/accessors.rs @@ -230,10 +230,6 @@ impl DocumentTypeV2Getters for DocumentTypeV2 { fn index_only(&self) -> bool { self.index_only } - - fn documents_can_be_erased(&self) -> bool { - self.documents_can_be_erased - } } impl DocumentTypeV2Setters for DocumentTypeV2 { diff --git a/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs index af983d7ab88..07242298e31 100644 --- a/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/v2/mod.rs @@ -109,15 +109,6 @@ pub struct DocumentTypeV2 { /// indexed, `$ownerId` recoverable from at least one index, immutable / /// non-transferable / no history, and per-index terminal typing. pub(in crate::data_contract) index_only: bool, - /// When true, a deleted document of this type may have its retained - /// revisions purged by an erase transition. Requires - /// [`Self::documents_keep_history`] and [`Self::documents_can_be_deleted`] - /// — erase applies to deleted documents only, so an erase-only type would - /// have nothing to act on. The parser enforces the pairing at contract - /// registration and the update validator keeps the flag immutable, because - /// narrowing it after a first chunk has irreversibly removed revisions - /// would strand a partially erased document forever. - pub(in crate::data_contract) documents_can_be_erased: bool, } impl DocumentTypeBasicMethods for DocumentTypeV2 {} @@ -186,7 +177,6 @@ impl From for DocumentTypeV2 { documents_summable: None, range_summable: false, index_only: false, - documents_can_be_erased: false, } } } @@ -227,7 +217,6 @@ impl From for DocumentTypeV2 { documents_summable: None, range_summable: false, index_only: false, - documents_can_be_erased: false, } } } diff --git a/packages/rs-dpp/src/data_contract/document_type/v3/accessors.rs b/packages/rs-dpp/src/data_contract/document_type/v3/accessors.rs new file mode 100644 index 00000000000..55f07700155 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v3/accessors.rs @@ -0,0 +1,274 @@ +use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV0MutGetters, DocumentTypeV0Setters, DocumentTypeV1Getters, + DocumentTypeV2Getters, DocumentTypeV2Setters, DocumentTypeV3Getters, +}; +use crate::data_contract::document_type::index::Index; +use crate::data_contract::document_type::index_level::IndexLevel; +use crate::data_contract::document_type::property::DocumentProperty; + +use platform_value::{Identifier, Value}; + +use crate::data_contract::document_type::restricted_creation::CreationRestrictionMode; +use crate::data_contract::document_type::token_costs::accessors::TokenCostGettersV0; +use crate::data_contract::document_type::v3::DocumentTypeV3; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; +use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; +use crate::data_contract::TokenContractPosition; +use crate::document::transfer::Transferable; +use crate::identity::SecurityLevel; +use crate::nft::TradeMode; +use crate::tokens::token_amount_on_contract_token::DocumentActionTokenCost; +use indexmap::IndexMap; +use std::collections::{BTreeMap, BTreeSet}; + +impl DocumentTypeV0MutGetters for DocumentTypeV3 { + fn schema_mut(&mut self) -> &mut Value { + &mut self.schema + } +} + +impl DocumentTypeV0Getters for DocumentTypeV3 { + fn name(&self) -> &String { + &self.name + } + + fn schema(&self) -> &Value { + &self.schema + } + + fn schema_owned(self) -> Value { + self.schema + } + + fn indexes(&self) -> &BTreeMap { + &self.indices + } + + fn find_contested_index(&self) -> Option<&Index> { + self.indices + .iter() + .find(|(_, index)| index.contested_index.is_some()) + .map(|(_, contested_index)| contested_index) + } + + fn index_structure(&self) -> &IndexLevel { + &self.index_structure + } + + fn flattened_properties(&self) -> &IndexMap { + &self.flattened_properties + } + + fn properties(&self) -> &IndexMap { + &self.properties + } + + fn identifier_paths(&self) -> &BTreeSet { + &self.identifier_paths + } + + fn binary_paths(&self) -> &BTreeSet { + &self.binary_paths + } + + fn required_fields(&self) -> &BTreeSet { + &self.required_fields + } + fn transient_fields(&self) -> &BTreeSet { + &self.transient_fields + } + + fn documents_keep_history(&self) -> bool { + self.documents_keep_history + } + + fn documents_keep_transfer_history(&self) -> bool { + self.documents_keep_transfer_history + } + + fn documents_keep_purchase_history(&self) -> bool { + self.documents_keep_purchase_history + } + + fn documents_keep_pricing_history(&self) -> bool { + self.documents_keep_pricing_history + } + + fn documents_mutable(&self) -> bool { + self.documents_mutable + } + + fn documents_can_be_deleted(&self) -> bool { + self.documents_can_be_deleted + } + + fn documents_transferable(&self) -> Transferable { + self.documents_transferable + } + + fn trade_mode(&self) -> TradeMode { + self.trade_mode + } + + fn creation_restriction_mode(&self) -> CreationRestrictionMode { + self.creation_restriction_mode + } + + fn data_contract_id(&self) -> Identifier { + self.data_contract_id + } + + fn requires_identity_encryption_bounded_key(&self) -> Option { + self.requires_identity_encryption_bounded_key + } + + fn requires_identity_decryption_bounded_key(&self) -> Option { + self.requires_identity_decryption_bounded_key + } + + fn security_level_requirement(&self) -> SecurityLevel { + self.security_level_requirement + } + + #[cfg(feature = "validation")] + fn json_schema_validator_ref(&self) -> &StatelessJsonSchemaLazyValidator { + &self.json_schema_validator + } +} + +impl DocumentTypeV0Setters for DocumentTypeV3 { + fn set_data_contract_id(&mut self, data_contract_id: Identifier) { + self.data_contract_id = data_contract_id; + } +} + +impl DocumentTypeV1Getters for DocumentTypeV3 { + fn document_creation_token_cost(&self) -> Option { + self.token_costs.document_creation_token_cost() + } + + fn document_replacement_token_cost(&self) -> Option { + self.token_costs.document_replacement_token_cost() + } + + fn document_deletion_token_cost(&self) -> Option { + self.token_costs.document_deletion_token_cost() + } + + fn document_transfer_token_cost(&self) -> Option { + self.token_costs.document_transfer_token_cost() + } + + fn document_update_price_token_cost(&self) -> Option { + self.token_costs.document_price_update_token_cost() + } + + fn document_purchase_token_cost(&self) -> Option { + self.token_costs.document_purchase_token_cost() + } + + fn all_document_token_costs(&self) -> Vec<&DocumentActionTokenCost> { + let mut result = Vec::new(); + + if let Some(cost) = self.token_costs.document_creation_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_replacement_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_deletion_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_transfer_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_price_update_token_cost_ref() { + result.push(cost); + } + if let Some(cost) = self.token_costs.document_purchase_token_cost_ref() { + result.push(cost); + } + + result + } + + fn all_external_token_costs_contract_tokens( + &self, + ) -> BTreeMap> { + let mut map = BTreeMap::new(); + + for cost in self.all_document_token_costs() { + if let Some(contract_id) = cost.contract_id { + map.entry(contract_id) + .or_insert_with(BTreeSet::new) + .insert(cost.token_contract_position); + } + } + + map + } +} + +impl DocumentTypeV2Getters for DocumentTypeV3 { + fn documents_countable(&self) -> bool { + self.documents_countable || self.range_countable + } + + fn range_countable(&self) -> bool { + self.range_countable + } + + fn documents_summable(&self) -> Option<&str> { + self.documents_summable.as_deref() + } + + fn range_summable(&self) -> bool { + self.range_summable + } + + fn index_only(&self) -> bool { + self.index_only + } +} + +impl DocumentTypeV2Setters for DocumentTypeV3 { + fn set_documents_countable(&mut self, countable: bool) { + self.documents_countable = countable; + if !countable { + // Preserve invariant: range_countable implies documents_countable + self.range_countable = false; + } + } + + fn set_range_countable(&mut self, range_countable: bool) { + self.range_countable = range_countable; + if range_countable { + self.documents_countable = true; + } + } + + fn set_documents_summable(&mut self, property: Option) { + let cleared = property.is_none(); + self.documents_summable = property; + if cleared { + // Preserve invariant: range_summable requires + // documents_summable.is_some() + self.range_summable = false; + } + } + + fn set_range_summable(&mut self, range_summable: bool) { + // Normalize unconditionally: `range_summable` requires a property + // to sum on, so clamp to false when `documents_summable` is unset. + // This way an existing-true-but-inconsistent state can't survive + // a setter call — the invariant always holds after this returns. + self.range_summable = range_summable && self.documents_summable.is_some(); + } +} + +impl DocumentTypeV3Getters for DocumentTypeV3 { + fn documents_can_be_erased(&self) -> bool { + self.documents_can_be_erased + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/v3/mod.rs b/packages/rs-dpp/src/data_contract/document_type/v3/mod.rs new file mode 100644 index 00000000000..2797e0d5808 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v3/mod.rs @@ -0,0 +1,350 @@ +use indexmap::IndexMap; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::data_contract::document_type::index::Index; +use crate::data_contract::document_type::index_level::IndexLevel; +use crate::data_contract::document_type::property::DocumentProperty; +use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements; + +use crate::data_contract::document_type::methods::{ + DocumentTypeBasicMethods, DocumentTypeV0Methods, +}; +use crate::data_contract::document_type::restricted_creation::CreationRestrictionMode; +use crate::data_contract::document_type::token_costs::accessors::TokenCostSettersV0; +use crate::data_contract::document_type::token_costs::TokenCosts; +use crate::data_contract::document_type::v0::DocumentTypeV0; +use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v2::DocumentTypeV2; +#[cfg(feature = "validation")] +use crate::data_contract::document_type::validator::StatelessJsonSchemaLazyValidator; +use crate::document::transfer::Transferable; +use crate::identity::SecurityLevel; +use crate::nft::TradeMode; +use crate::tokens::token_amount_on_contract_token::DocumentActionTokenCost; +use platform_value::{Identifier, Value}; + +mod accessors; +#[cfg(feature = "random-document-types")] +pub mod random_document_type; + +/// The document type of protocol version 15 and later: [`DocumentTypeV2`] plus +/// the `canBeErased` keyword of meta-schema v4. Parser generation 4 produces it; +/// earlier generations keep producing the version their grammar knows. +#[derive(Debug, PartialEq, Clone)] +pub struct DocumentTypeV3 { + pub(in crate::data_contract) name: String, + pub(in crate::data_contract) schema: Value, + pub(in crate::data_contract) indices: BTreeMap, + pub(in crate::data_contract) index_structure: IndexLevel, + /// Flattened properties flatten all objects for quick lookups for indexes + /// Document field should not contain sub objects. + pub(in crate::data_contract) flattened_properties: IndexMap, + /// Document field can contain sub objects. + pub(in crate::data_contract) properties: IndexMap, + pub(in crate::data_contract) identifier_paths: BTreeSet, + pub(in crate::data_contract) binary_paths: BTreeSet, + /// The required fields on the document type + pub(in crate::data_contract) required_fields: BTreeSet, + /// The transient fields on the document type + pub(in crate::data_contract) transient_fields: BTreeSet, + /// Should documents keep history? + pub(in crate::data_contract) documents_keep_history: bool, + /// Should transfers of documents of this type be recorded in the document + /// history system contract? + pub(in crate::data_contract) documents_keep_transfer_history: bool, + /// Should purchases of documents of this type be recorded in the document + /// history system contract? + pub(in crate::data_contract) documents_keep_purchase_history: bool, + /// Should price updates on documents of this type be recorded in the + /// document history system contract? + pub(in crate::data_contract) documents_keep_pricing_history: bool, + /// Are documents mutable? + pub(in crate::data_contract) documents_mutable: bool, + /// Can documents of this type be deleted? + pub(in crate::data_contract) documents_can_be_deleted: bool, + /// Can documents be transferred without a trade? + pub(in crate::data_contract) documents_transferable: Transferable, + /// How are these documents traded? + pub(in crate::data_contract) trade_mode: TradeMode, + /// Is document creation restricted? + pub(in crate::data_contract) creation_restriction_mode: CreationRestrictionMode, + /// The data contract id + pub(in crate::data_contract) data_contract_id: Identifier, + /// Encryption key storage requirements + pub(in crate::data_contract) requires_identity_encryption_bounded_key: + Option, + /// Decryption key storage requirements + pub(in crate::data_contract) requires_identity_decryption_bounded_key: + Option, + pub(in crate::data_contract) security_level_requirement: SecurityLevel, + #[cfg(feature = "validation")] + pub(in crate::data_contract) json_schema_validator: StatelessJsonSchemaLazyValidator, + /// The token costs associated with state transitions on this document type + pub(in crate::data_contract) token_costs: TokenCosts, + /// When true, the primary key tree uses a CountTree enabling O(1) total document count queries + pub(in crate::data_contract) documents_countable: bool, + /// When true, the primary key tree uses a ProvableCountTree enabling range countable. + /// Implies documents_countable = true. + pub(in crate::data_contract) range_countable: bool, + /// When `Some(property_name)`, the primary key tree is a `SumTree` (or + /// `ProvableSumTree` if [`Self::range_summable`] is also set) summing + /// the named integer property across every document of this type. + /// Enables O(log n) `GetDocumentsSum` queries with no `where` filter. + /// + /// The named property must be `type: integer` and listed in + /// [`Self::required_fields`]; the parser enforces this at contract + /// creation. Composes orthogonally with `documents_countable` — + /// setting both yields a `CountSumTree` (or `ProvableCountSumTree`) + /// that carries both a count and a sum, queryable independently. + pub(in crate::data_contract) documents_summable: Option, + /// When true, the primary key sum tree is a `ProvableSumTree` + /// (committing aggregated sub-sums to every internal merk node), + /// enabling O(log n) `AggregateSumOnRange` queries. Implies + /// [`Self::documents_summable`] is `Some` — enforced by + /// [`crate::data_contract::document_type::accessors::DocumentTypeV2Setters::set_range_summable`]. + pub(in crate::data_contract) range_summable: bool, + /// When true, documents of this type are **indexOnly**: nothing is + /// written to primary storage (there is no `[0]` primary-key tree at + /// all) — the index entries are the rows, each terminating in an `Item` + /// keyed by the index's `terminal` property instead of a `Reference` + /// keyed by the document id. Only what is in the indexes exists and is + /// recoverable. The parser (`apply_index_only`) enforces the structural + /// constraints this layout depends on: every property required and + /// indexed, `$ownerId` recoverable from at least one index, immutable / + /// non-transferable / no history, and per-index terminal typing. + pub(in crate::data_contract) index_only: bool, + /// When true, a deleted document of this type may have its retained + /// revisions purged by an erase transition. Requires + /// [`Self::documents_keep_history`] and [`Self::documents_can_be_deleted`]: + /// erase applies to deleted documents only, so an erase-only type would + /// have nothing to act on. The parser enforces the pairing at contract + /// registration and the update validator keeps the flag immutable, because + /// narrowing it after a first chunk has irreversibly removed revisions + /// would strand a partially erased document forever. + pub(in crate::data_contract) documents_can_be_erased: bool, +} + +impl DocumentTypeBasicMethods for DocumentTypeV3 {} + +impl DocumentTypeV0Methods for DocumentTypeV3 {} + +impl crate::data_contract::document_type::accessors::DocumentTypeV1Setters for DocumentTypeV3 { + fn set_document_creation_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_creation_token_cost(cost) + } + + fn set_document_replacement_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_replacement_token_cost(cost) + } + + fn set_document_deletion_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_deletion_token_cost(cost) + } + + fn set_document_transfer_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_transfer_token_cost(cost) + } + + fn set_document_price_update_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_price_update_token_cost(cost) + } + + fn set_document_purchase_token_cost(&mut self, cost: Option) { + self.token_costs.set_document_purchase_token_cost(cost) + } +} + +impl From for DocumentTypeV3 { + fn from(value: DocumentTypeV0) -> Self { + DocumentTypeV3 { + name: value.name, + schema: value.schema, + indices: value.indices, + index_structure: value.index_structure, + flattened_properties: value.flattened_properties, + properties: value.properties, + identifier_paths: value.identifier_paths, + binary_paths: value.binary_paths, + required_fields: value.required_fields, + transient_fields: value.transient_fields, + documents_keep_history: value.documents_keep_history, + documents_keep_transfer_history: value.documents_keep_transfer_history, + documents_keep_purchase_history: value.documents_keep_purchase_history, + documents_keep_pricing_history: value.documents_keep_pricing_history, + documents_mutable: value.documents_mutable, + documents_can_be_deleted: value.documents_can_be_deleted, + documents_transferable: value.documents_transferable, + trade_mode: value.trade_mode, + creation_restriction_mode: value.creation_restriction_mode, + data_contract_id: value.data_contract_id, + requires_identity_encryption_bounded_key: value + .requires_identity_encryption_bounded_key, + requires_identity_decryption_bounded_key: value + .requires_identity_decryption_bounded_key, + security_level_requirement: value.security_level_requirement, + #[cfg(feature = "validation")] + json_schema_validator: value.json_schema_validator, + token_costs: TokenCosts::V0(Default::default()), + documents_countable: false, + range_countable: false, + documents_summable: None, + range_summable: false, + index_only: false, + documents_can_be_erased: false, + } + } +} + +impl From for DocumentTypeV3 { + fn from(value: DocumentTypeV1) -> Self { + DocumentTypeV3 { + name: value.name, + schema: value.schema, + indices: value.indices, + index_structure: value.index_structure, + flattened_properties: value.flattened_properties, + properties: value.properties, + identifier_paths: value.identifier_paths, + binary_paths: value.binary_paths, + required_fields: value.required_fields, + transient_fields: value.transient_fields, + documents_keep_history: value.documents_keep_history, + documents_keep_transfer_history: value.documents_keep_transfer_history, + documents_keep_purchase_history: value.documents_keep_purchase_history, + documents_keep_pricing_history: value.documents_keep_pricing_history, + documents_mutable: value.documents_mutable, + documents_can_be_deleted: value.documents_can_be_deleted, + documents_transferable: value.documents_transferable, + trade_mode: value.trade_mode, + creation_restriction_mode: value.creation_restriction_mode, + data_contract_id: value.data_contract_id, + requires_identity_encryption_bounded_key: value + .requires_identity_encryption_bounded_key, + requires_identity_decryption_bounded_key: value + .requires_identity_decryption_bounded_key, + security_level_requirement: value.security_level_requirement, + #[cfg(feature = "validation")] + json_schema_validator: value.json_schema_validator, + token_costs: value.token_costs, + documents_countable: false, + range_countable: false, + documents_summable: None, + range_summable: false, + index_only: false, + documents_can_be_erased: false, + } + } +} + +impl From for DocumentTypeV3 { + fn from(value: DocumentTypeV2) -> Self { + DocumentTypeV3 { + name: value.name, + schema: value.schema, + indices: value.indices, + index_structure: value.index_structure, + flattened_properties: value.flattened_properties, + properties: value.properties, + identifier_paths: value.identifier_paths, + binary_paths: value.binary_paths, + required_fields: value.required_fields, + transient_fields: value.transient_fields, + documents_keep_history: value.documents_keep_history, + documents_keep_transfer_history: value.documents_keep_transfer_history, + documents_keep_purchase_history: value.documents_keep_purchase_history, + documents_keep_pricing_history: value.documents_keep_pricing_history, + documents_mutable: value.documents_mutable, + documents_can_be_deleted: value.documents_can_be_deleted, + documents_transferable: value.documents_transferable, + trade_mode: value.trade_mode, + creation_restriction_mode: value.creation_restriction_mode, + data_contract_id: value.data_contract_id, + requires_identity_encryption_bounded_key: value + .requires_identity_encryption_bounded_key, + requires_identity_decryption_bounded_key: value + .requires_identity_decryption_bounded_key, + security_level_requirement: value.security_level_requirement, + #[cfg(feature = "validation")] + json_schema_validator: value.json_schema_validator, + token_costs: value.token_costs, + documents_countable: value.documents_countable, + range_countable: value.range_countable, + documents_summable: value.documents_summable, + range_summable: value.range_summable, + index_only: value.index_only, + documents_can_be_erased: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, DocumentTypeV3Getters, + }; + use crate::data_contract::document_type::v0::DocumentTypeV0; + use crate::data_contract::document_type::DocumentType; + + fn make_v0() -> DocumentTypeV0 { + DocumentTypeV0 { + name: "test".to_string(), + schema: Value::Null, + indices: BTreeMap::new(), + index_structure: IndexLevel::try_from_indices( + Vec::::new(), + "test", + platform_version::version::PlatformVersion::latest(), + ) + .unwrap(), + flattened_properties: IndexMap::new(), + properties: IndexMap::new(), + identifier_paths: BTreeSet::new(), + binary_paths: BTreeSet::new(), + required_fields: BTreeSet::new(), + transient_fields: BTreeSet::new(), + documents_keep_history: true, + documents_keep_transfer_history: false, + documents_keep_purchase_history: false, + documents_keep_pricing_history: false, + documents_mutable: true, + documents_can_be_deleted: true, + documents_transferable: Transferable::Never, + trade_mode: TradeMode::None, + creation_restriction_mode: CreationRestrictionMode::NoRestrictions, + data_contract_id: Identifier::default(), + requires_identity_encryption_bounded_key: None, + requires_identity_decryption_bounded_key: None, + security_level_requirement: SecurityLevel::HIGH, + #[cfg(feature = "validation")] + json_schema_validator: Default::default(), + } + } + + #[test] + fn should_keep_every_earlier_field_and_default_erasability_to_false() { + let mut v2: DocumentTypeV2 = make_v0().into(); + v2.index_only = false; + v2.documents_countable = true; + let v3: DocumentTypeV3 = v2.into(); + + assert_eq!(v3.name(), "test"); + assert!(v3.documents_keep_history()); + assert!(v3.documents_can_be_deleted()); + assert!(v3.documents_countable()); + assert!( + !v3.documents_can_be_erased(), + "a type parsed by an earlier grammar never declared the keyword" + ); + } + + #[test] + fn should_report_erasability_only_from_the_version_that_knows_it() { + let mut v3: DocumentTypeV3 = make_v0().into(); + v3.documents_can_be_erased = true; + + assert!(DocumentType::V3(v3).documents_can_be_erased()); + assert!(!DocumentType::V0(make_v0()).documents_can_be_erased()); + assert!(!DocumentType::V2(make_v0().into()).documents_can_be_erased()); + } +} diff --git a/packages/rs-dpp/src/data_contract/document_type/v3/random_document_type.rs b/packages/rs-dpp/src/data_contract/document_type/v3/random_document_type.rs new file mode 100644 index 00000000000..9a12470f94c --- /dev/null +++ b/packages/rs-dpp/src/data_contract/document_type/v3/random_document_type.rs @@ -0,0 +1,40 @@ +use crate::data_contract::document_type::v0::random_document_type::RandomDocumentTypeParameters; +use crate::data_contract::document_type::v1::DocumentTypeV1; +use crate::data_contract::document_type::v3::DocumentTypeV3; +use crate::version::PlatformVersion; +use crate::ProtocolError; +use platform_value::Identifier; +use rand::rngs::StdRng; + +impl DocumentTypeV3 { + pub fn random_document_type( + parameters: RandomDocumentTypeParameters, + data_contract_id: Identifier, + rng: &mut StdRng, + platform_version: &PlatformVersion, + ) -> Result { + Ok(DocumentTypeV1::random_document_type( + parameters, + data_contract_id, + rng, + platform_version, + )? + .into()) + } + + /// This is used to create an invalid random document type, often for testing + pub fn invalid_random_document_type( + parameters: RandomDocumentTypeParameters, + data_contract_id: Identifier, + rng: &mut StdRng, + platform_version: &PlatformVersion, + ) -> Result { + Ok(DocumentTypeV1::invalid_random_document_type( + parameters, + data_contract_id, + rng, + platform_version, + )? + .into()) + } +} diff --git a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs index 6cea9ac97b6..1e21df5ba69 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs @@ -237,6 +237,7 @@ mod tests { DocumentTypeMutRef::V0(dt) => dt.documents_mutable = false, DocumentTypeMutRef::V1(dt) => dt.documents_mutable = false, DocumentTypeMutRef::V2(dt) => dt.documents_mutable = false, + DocumentTypeMutRef::V3(dt) => dt.documents_mutable = false, } let result = old_data_contract diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index f08f7f82c91..3a80a422b3c 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -897,7 +897,7 @@ impl StateTransition { StateTransition::Batch(batch_transition) => match batch_transition { BatchTransition::V0(_) => ALL_VERSIONS, BatchTransition::V1(_) => 9..=LATEST_VERSION, - BatchTransition::V2(_) => 14..=LATEST_VERSION, + BatchTransition::V2(_) => 15..=LATEST_VERSION, }, StateTransition::IdentityCreate(_) | StateTransition::IdentityTopUp(_) @@ -3308,6 +3308,118 @@ mod tests { } } + // --- Batch wire formats. Format 2 is the first shell that can carry an + // erase, so it becomes active with the erase kind at protocol version 15 + // while formats 0 and 1 keep the ranges they shipped with. A format 2 + // batch must therefore never decode under a released protocol version, + // whatever it carries. --- + fn sample_batch_v2_st_with_erase() -> StateTransition { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransitionV1, DocumentEraseTransition, DocumentTransitionV1, + }; + use crate::state_transition::batch_transition::BatchTransitionV2; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::from([1u8; 32]), + identity_contract_nonce: 3, + document_type_name: "note".to_string(), + data_contract_id: Identifier::from([2u8; 32]), + }); + let erase = + DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base, + })); + StateTransition::Batch(BatchTransition::V2(BatchTransitionV2 { + owner_id: Identifier::from([8u8; 32]), + transitions: vec![BatchedTransitionV1::Document(erase)], + user_fee_increase: 2, + signature_public_key_id: 7, + signature: BinaryData::new(vec![0xEE; 65]), + })) + } + + #[test] + fn test_active_version_range_batch_wire_formats() { + use crate::state_transition::batch_transition::batched_transition::BatchedTransition; + use crate::state_transition::batch_transition::BatchTransitionV1; + + assert_eq!( + sample_batch_st_with_delete().active_version_range(), + ALL_VERSIONS, + "format 0 predates protocol version ranges" + ); + + let StateTransition::Batch(BatchTransition::V0(format_0)) = sample_batch_st_with_delete() + else { + unreachable!("the sample is a format 0 batch") + }; + let format_1 = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: format_0.owner_id, + transitions: format_0 + .transitions + .into_iter() + .map(BatchedTransition::Document) + .collect(), + user_fee_increase: format_0.user_fee_increase, + signature_public_key_id: format_0.signature_public_key_id, + signature: format_0.signature, + })); + assert_eq!( + format_1.active_version_range(), + 9..=LATEST_VERSION, + "format 1 arrived with tokens at protocol version 9" + ); + + assert_eq!( + sample_batch_v2_st_with_erase().active_version_range(), + 15..=LATEST_VERSION, + "format 2 arrived with the erase kind at protocol version 15" + ); + } + + #[cfg(all(feature = "state-transitions", feature = "validation"))] + #[test] + fn test_deserialize_format_2_batch_in_version_rejects_released_protocols() { + use crate::serialization::PlatformSerializable; + use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + + let bytes = PlatformSerializable::serialize_to_bytes(&sample_batch_v2_st_with_erase()) + .expect("serialize succeeds"); + + let released = PlatformVersion::get(14).expect("protocol version 14 exists"); + let err = StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, released) + .expect_err("a format 2 batch must not decode under protocol version 14"); + match err { + ProtocolError::StateTransitionError(StateTransitionIsNotActiveError { + state_transition_type, + active_version_range, + current_protocol_version, + }) => { + assert_eq!(state_transition_type, "DocumentsBatch([Erase])"); + assert_eq!(active_version_range, 15..=LATEST_VERSION); + assert_eq!(current_protocol_version, 14); + } + other => panic!("expected StateTransitionIsNotActiveError, got {other:?}"), + } + + let decoded = StateTransition::deserialize_from_bytes_untrusted_in_version( + &bytes, + PlatformVersion::latest(), + ) + .expect("a format 2 batch decodes under the latest protocol version"); + let StateTransition::Batch(batch) = decoded else { + panic!("expected a batch, got {decoded:?}"); + }; + assert!(matches!(batch, BatchTransition::V2(_))); + assert_eq!(batch.transitions_len_v1(), 1); + assert_eq!( + batch.transitions_len(), + 0, + "the shell of formats 0 and 1 cannot see an erase and reports the batch as empty" + ); + } + // ----------------------------------------------------------------------- // Additional coverage: variants not yet exercised. // diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs index a2ac1110b4e..2e691425711 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs @@ -54,10 +54,11 @@ impl<'a> Iterator for DocumentBatchIterator<'a> { /// [`BatchedTransitionRef`] cannot represent, so a format 2 batch has no view /// here: every accessor reports an empty batch for it. Code that must see every /// format uses [`DocumentsBatchTransitionAccessorsV1`]. The generations that -/// still read this view are the ones selected for protocol versions that reject -/// a format 2 batch by its wire version before any of them runs, and the first -/// of them rejects an empty batch, so the empty view fails closed rather than -/// silently dropping transitions. +/// still read this view (structure, nonce, admission and state validation +/// alike) are the ones selected for protocol versions that reject a format 2 +/// batch by its wire version while decoding it, before any of them runs; and +/// the structure generation among them rejects an empty batch, so the empty +/// view fails closed rather than silently dropping transitions. impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { type IterType<'a> = DocumentBatchIterator<'a> diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs index 3b515a9a18f..3f6d676ecf3 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_erase_transition/from_document.rs @@ -20,7 +20,7 @@ impl DocumentEraseTransition { platform_version: &PlatformVersion, ) -> Result { // `None` bounds mean the kind does not exist at this platform version - // (it joined the wire at protocol version 14) — constructing one there + // (it joined the wire at protocol version 15) — constructing one there // could only ever produce a transition the network refuses. let bounds = platform_version .dpp diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs index fad5f49c777..f88846eaed5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs @@ -423,6 +423,43 @@ mod tests { )); } + /// A format 2 batch never reaches the released structure generation in + /// production, because decoding rejects it by wire version first. Should + /// one arrive anyway, the shell that generation reads through cannot see + /// an erase and must report the batch as empty, so the batch is refused + /// rather than passed on with its transitions dropped. + #[test] + fn should_fail_closed_on_a_format_2_batch_through_the_released_structure_generation() { + let batch = make_batch_v2(vec![make_erase(1, 1)]); + + let released = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert_eq!( + released + .dpp + .state_transitions + .documents + .documents_batch_transition + .validation + .validate_base_structure, + 0, + "protocol version 14 selects the released structure generation" + ); + let result = batch + .validate_base_structure(released) + .expect("no protocol err"); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::DocumentTransitionsAreAbsentError(_) + )) + )); + + let result = batch + .validate_base_structure(PlatformVersion::latest()) + .expect("no protocol err"); + assert!(result.is_valid(), "expected valid, got {:?}", result.errors); + } + #[test] fn should_bound_the_nonce_of_an_erase_in_a_format_2_batch() { let result = make_batch_v2(vec![make_erase(1, u64::MAX)]) diff --git a/packages/rs-dpp/src/validation/meta_validators/mod.rs b/packages/rs-dpp/src/validation/meta_validators/mod.rs index 55d2bd4f054..613a42860ed 100644 --- a/packages/rs-dpp/src/validation/meta_validators/mod.rs +++ b/packages/rs-dpp/src/validation/meta_validators/mod.rs @@ -52,6 +52,10 @@ lazy_static! { "../../../schema/meta_schemas/document/v3/document-meta.json" )) .expect("v3 document meta-schema JSON must be valid"); + static ref DOCUMENT_META_JSON_V4: Value = serde_json::from_str::(include_str!( + "../../../schema/meta_schemas/document/v4/document-meta.json" + )) + .expect("v4 document meta-schema JSON must be valid"); pub static ref DRAFT_202012_META_SCHEMA: JSONSchema = JSONSchema::options() .with_draft(Draft::Draft202012) @@ -299,6 +303,61 @@ lazy_static! { .compile(&DOCUMENT_META_JSON_V3) .expect("Invalid data contract schema"); + // Compiled version of document meta schema v4 + // Introduced for protocol version 15 (the keep-history document + // lifecycle). v3 plus the document-type keyword `canBeErased`, which lets + // a deleted keep-history document have its retained revisions purged by + // an erase transition. Hosting it on a schema only v15+ contracts validate + // against leaves v14 validation untouched: under v3 the key still fails + // `additionalProperties: false` on a document type. + pub static ref DOCUMENT_META_SCHEMA_V4: JSONSchema = JSONSchema::options() + .with_keyword( + "byteArray", + |_, _, _| Ok(Box::new(ByteArrayKeyword)), + ) + .with_patterns_regex_engine(RegexEngine::Regex(RegexOptions { + size_limit: Some(5 * (1 << 20)), + ..Default::default() + })) + .should_ignore_unknown_formats(false) + .should_validate_formats(true) + .with_draft(Draft::Draft202012) + .with_document( + "https://json-schema.org/draft/2020-12/meta/applicator".to_string(), + DRAFT202012_APPLICATOR.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/core".to_string(), + DRAFT202012_CORE.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/unevaluated".to_string(), + DRAFT202012_UNEVALUATED.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/validation".to_string(), + DRAFT202012_VALIDATION.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/meta-data".to_string(), + DRAFT202012_META_DATA.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/format-annotation".to_string(), + DRAFT202012_FORMAT_ANNOTATION.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/meta/content".to_string(), + DRAFT202012_CONTENT.clone(), + ) + .with_document( + "https://json-schema.org/draft/2020-12/schema".to_string(), + DRAFT202012.clone(), + ) + .to_owned() + .compile(&DOCUMENT_META_JSON_V4) + .expect("Invalid data contract schema"); + } #[cfg(test)] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs index 6d67acc456c..5719164e323 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/mod.rs @@ -11,6 +11,7 @@ use crate::execution::types::state_transition_execution_context::StateTransition use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v0::DocumentCreateTransitionActionStateValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v2::DocumentCreateTransitionActionStateValidationV2; +use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v3::DocumentCreateTransitionActionStateValidationV3; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::advanced_structure_v0::DocumentCreateTransitionActionStructureValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::advanced_structure_v1::DocumentCreateTransitionActionStructureValidationV1; use crate::platform_types::platform::PlatformStateRef; @@ -20,6 +21,7 @@ mod advanced_structure_v1; mod state_v0; mod state_v1; mod state_v2; +mod state_v3; pub trait DocumentCreateTransitionActionValidation { fn validate_structure( @@ -111,9 +113,18 @@ impl DocumentCreateTransitionActionValidation for DocumentCreateTransitionAction transaction, platform_version, ), + // V3 refuses the id of a deleted or erasing keep-history document on top of V2 + 3 => self.validate_state_v3( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentCreateTransitionAction::validate_state".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs index 1da1751ecca..177acedc9da 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v2/mod.rs @@ -1,10 +1,7 @@ use dpp::block::block_info::BlockInfo; -use dpp::consensus::basic::document::InvalidDocumentTypeError; use dpp::consensus::state::document::document_contest_document_with_same_id_already_present_error::DocumentContestDocumentWithSameIdAlreadyPresentError; -use dpp::consensus::state::document::document_already_present_error::DocumentAlreadyPresentError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; -use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::identifier::Identifier; use dpp::validation::{ConsensusValidationResult, SimpleConsensusValidationResult}; @@ -22,7 +19,6 @@ use crate::execution::types::state_transition_execution_context::{ }; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v1::DocumentCreateTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_reference_validation::DocumentReferenceValidation; -use crate::execution::validation::state_transition::batch::state::fetch_keep_history_document_lifecycle::fetch_keep_history_document_lifecycle; use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::has_contested_document_with_document_id; use crate::platform_types::platform::PlatformStateRef; @@ -61,40 +57,6 @@ impl DocumentCreateTransitionActionStateValidationV2 for DocumentCreateTransitio return Ok(validation_result); } - // A keep-history document that has been deleted keeps its revisions and - // keeps its id: nothing in the primary-key tree marks the id as taken, - // so v1's by-id read cannot see the reservation. Creating over it would - // append a new document's revision to a deleted one's record. The id - // becomes free again once an erase has removed the last revision. - let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract = &contract_fetch_info.contract; - let document_type_name = self.base().document_type_name(); - let Some(document_type) = contract.document_type_optional_for_name(document_type_name) - else { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), - )); - }; - if document_type.documents_keep_history() { - let lifecycle = fetch_keep_history_document_lifecycle( - platform.drive, - contract, - document_type, - self.base().id(), - &block_info.epoch, - execution_context, - transaction, - platform_version, - )?; - if lifecycle.is_present() { - return Ok(SimpleConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::DocumentAlreadyPresentError( - DocumentAlreadyPresentError::new(self.base().id()), - )), - )); - } - } - let reference_result = self.base().validate_document_references( self.data(), None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v3/mod.rs new file mode 100644 index 00000000000..56abe4492d4 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v3/mod.rs @@ -0,0 +1,99 @@ +use dpp::block::block_info::BlockInfo; +use dpp::consensus::basic::document::InvalidDocumentTypeError; +use dpp::consensus::state::document::document_already_present_error::DocumentAlreadyPresentError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +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::{ + DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0, +}; + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::state_v2::DocumentCreateTransitionActionStateValidationV2; +use crate::execution::validation::state_transition::batch::state::fetch_keep_history_document_lifecycle::fetch_keep_history_document_lifecycle; +use crate::platform_types::platform::PlatformStateRef; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentCreateTransitionActionStateValidationV3 +{ + fn validate_state_v3( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentCreateTransitionActionStateValidationV3 for DocumentCreateTransitionAction { + /// V3 is V2 plus the keep-history document lifecycle: a deleted or erasing + /// keep-history document keeps its id, so its id is not free to create. + fn validate_state_v3( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.validate_state_v2( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + // A keep-history document that has been deleted keeps its revisions and + // keeps its id: nothing in the primary-key tree marks the id as taken, + // so the by-id read of the earlier generations cannot see the + // reservation. Creating over it would append a new document's revision + // to a deleted one's record. The id becomes free again once an erase + // has removed the last revision. + let contract_fetch_info = self.base().data_contract_fetch_info(); + let contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + let Some(document_type) = contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), contract.id()).into(), + )); + }; + if !document_type.documents_keep_history() { + return Ok(SimpleConsensusValidationResult::new()); + } + + let lifecycle = fetch_keep_history_document_lifecycle( + platform.drive, + contract, + document_type, + self.base().id(), + &block_info.epoch, + execution_context, + transaction, + platform_version, + )?; + if lifecycle.is_present() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DocumentAlreadyPresentError( + DocumentAlreadyPresentError::new(self.base().id()), + )), + )); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs index 199e88b0850..b90d4f2a309 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v1/mod.rs @@ -12,15 +12,10 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: fn validate_structure_v1(&self) -> Result; } impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTransitionAction { - /// From protocol 14 a keep-history type takes part in the document - /// lifecycle: a delete removes the document from ordinary reads and leaves - /// its revisions readable. What v1 adds over v0 is the refusal of a - /// keep-history type that carries a contested index, which the parser - /// rejects for new contracts but which a contract stored before protocol 14 - /// could still declare. A contested resource is awarded outside transition - /// validation, at an id derived from the winner rather than from the - /// contested values, so that award can land on an id whose retained history - /// already exists. + /// Protocol 14 checks both deletion permission and history retention before + /// executing a delete. Legacy history-bearing types may advertise deletion, + /// but Drive refuses it; return the usual paid consensus rejection here. + /// V0 retains the historical InternalError outcome for protocol 13 and earlier. fn validate_structure_v1(&self) -> Result { let contract_fetch_info = self.base().data_contract_fetch_info(); let data_contract = &contract_fetch_info.contract; @@ -35,7 +30,9 @@ impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTrans )); }; - if !document_type.documents_can_be_deleted() { + // A legacy contract may enable both flags, but retaining history always + // prevents deletion, regardless of the advertised canBeDeleted value. + if !document_type.documents_can_be_deleted() || document_type.documents_keep_history() { return Ok(SimpleConsensusValidationResult::new_with_error( InvalidDocumentTransitionActionError::new(format!( "documents of type {} can not be deleted", @@ -45,22 +42,6 @@ impl DocumentDeleteTransitionActionStructureValidationV1 for DocumentDeleteTrans )); } - if document_type.documents_keep_history() - && document_type - .indexes() - .values() - .any(|index| index.contested_index.is_some()) - { - return Ok(SimpleConsensusValidationResult::new_with_error( - InvalidDocumentTransitionActionError::new(format!( - "documents of keep-history type {} carry a contested index and can not take \ - part in the document lifecycle", - document_type_name - )) - .into(), - )); - } - // Pair the delete KIND with the doctype's storage mode: an // indexOnly document has no primary-storage row a by-id delete // could fetch, so its deletes must come as the indexOnlyDelete diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v2/mod.rs new file mode 100644 index 00000000000..3e51bf51a1e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/advanced_structure_v2/mod.rs @@ -0,0 +1,84 @@ +use dpp::consensus::basic::document::{InvalidDocumentTransitionActionError, InvalidDocumentTypeError}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::validation::SimpleConsensusValidationResult; +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_delete_transition_action::DocumentDeleteTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; + +use crate::error::Error; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentDeleteTransitionActionStructureValidationV2 { + fn validate_structure_v2(&self) -> Result; +} +impl DocumentDeleteTransitionActionStructureValidationV2 for DocumentDeleteTransitionAction { + /// From protocol 15 a keep-history type takes part in the document + /// lifecycle: a delete removes the document from ordinary reads and leaves + /// its revisions readable, so v2 no longer refuses a delete because the + /// type keeps history. What it refuses instead is a keep-history type that + /// carries a contested index, which the parser rejects for new contracts + /// but which a contract stored before protocol 15 could still declare. A + /// contested resource is awarded outside transition validation, at an id + /// derived from the winner rather than from the contested values, so that + /// award can land on an id whose retained history already exists. + fn validate_structure_v2(&self) -> Result { + let contract_fetch_info = self.base().data_contract_fetch_info(); + let data_contract = &contract_fetch_info.contract; + let document_type_name = self.base().document_type_name(); + + // Make sure that the document type is defined in the contract + let Some(document_type) = data_contract.document_type_optional_for_name(document_type_name) + else { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTypeError::new(document_type_name.clone(), data_contract.id()) + .into(), + )); + }; + + if !document_type.documents_can_be_deleted() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of type {} can not be deleted", + document_type_name + )) + .into(), + )); + } + + if document_type.documents_keep_history() + && document_type + .indexes() + .values() + .any(|index| index.contested_index.is_some()) + { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of keep-history type {} carry a contested index and can not take \ + part in the document lifecycle", + document_type_name + )) + .into(), + )); + } + + // Pair the delete KIND with the doctype's storage mode: an + // indexOnly document has no primary-storage row a by-id delete + // could fetch, so its deletes must come as the indexOnlyDelete + // (delete-by-values) kind — its structure validation enforces the + // mirror rule. `index_only()` can only be true on a PV14+ + // contract, so this branch is unreachable for every historical + // transition. + if document_type.index_only() { + return Ok(SimpleConsensusValidationResult::new_with_error( + InvalidDocumentTransitionActionError::new(format!( + "documents of indexOnly type {} must be deleted with an indexOnlyDelete \ + (delete-by-values) transition carrying the document's values", + document_type_name + )) + .into(), + )); + } + + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs index 0d872a96c59..b97b62eb975 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/mod.rs @@ -11,10 +11,12 @@ use crate::execution::validation::state_transition::batch::action_validation::do use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::state_v1::DocumentDeleteTransitionActionStateValidationV1; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::advanced_structure_v0::DocumentDeleteTransitionActionStructureValidationV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::advanced_structure_v1::DocumentDeleteTransitionActionStructureValidationV1; +use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::advanced_structure_v2::DocumentDeleteTransitionActionStructureValidationV2; use crate::platform_types::platform::PlatformStateRef; mod advanced_structure_v0; mod advanced_structure_v1; +mod advanced_structure_v2; mod state_v0; mod state_v1; @@ -49,9 +51,10 @@ impl DocumentDeleteTransitionActionValidation for DocumentDeleteTransitionAction { 0 => self.validate_structure_v0(), 1 => self.validate_structure_v1(), + 2 => self.validate_structure_v2(), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentDeleteTransitionAction::validate_structure".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs index fbcbecc2de9..d514a99a167 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_erase_transition_action/advanced_structure_v0/mod.rs @@ -3,7 +3,7 @@ use dpp::consensus::basic::document::{ InvalidDocumentTransitionActionError, InvalidDocumentTypeError, }; use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV3Getters}; use dpp::validation::SimpleConsensusValidationResult; 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_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; @@ -52,7 +52,7 @@ impl DocumentEraseTransitionActionStructureValidationV0 for DocumentEraseTransit )); } - // Defensive, for a contract stored before protocol 14: the parser keeps + // Defensive, for a contract stored before protocol 15: the parser keeps // the two apart for every contract registered since. if document_type .indexes() diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs index 600f8ed8730..abe20464afd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs @@ -43,7 +43,7 @@ use crate::execution::validation::state_transition::batch::action_validation::do use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; /// Generation 1 validates the structure of batch action format 1, which is -/// what a batch of any wire format becomes for protocol version 14 and later, +/// what a batch of any wire format becomes for protocol version 15 and later, /// including the erase of a keep-history document. pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStructureValidationV1 { 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 aeb9aaff2af..d8fd10e8e95 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::{ @@ -213,9 +214,9 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( /// Like [`fetch_documents_for_transitions_knowing_contract_and_document_type`], /// for transitions viewed through the shell that sees every batch wire format. /// -/// Generation 2 of the method is the first one reachable through this view; -/// generations 0 and 1 only see the shell of batch formats 0 and 1 and are -/// reached through the sibling function. +/// The helper has its own version slot, which is dormant at the protocol +/// versions whose batches only exist in wire formats 0 and 1; those reach the +/// sibling function. #[allow(clippy::too_many_arguments)] pub(crate) fn fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type( drive: &Drive, @@ -232,33 +233,40 @@ pub(crate) fn fetch_documents_for_transitions_of_any_format_knowing_contract_and .validation_and_processing .state_transitions .batch_state_transition - .fetch_documents_for_transitions_knowing_contract_and_document_type + .fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type { - 2 => fetch_documents_for_transitions_knowing_contract_and_document_type_v2( - drive, - contract, - document_type, - transitions, - epoch, - execution_context, - transaction, - platform_version, - ), - version => Err(Error::Execution( - crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: "fetch_documents_for_transitions_knowing_contract_and_document_type" + Some(0) => { + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type_v0( + drive, + contract, + document_type, + transitions, + epoch, + execution_context, + transaction, + platform_version, + ) + } + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: + "fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type" .to_string(), - known_versions: vec![2], - received: version, - }, - )), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: + "fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type" + .to_string(), + known_versions: vec![0], + })), } } -/// Generation 2: the billing of generation 1, for transitions of any batch +/// Bills like generation 1 of the sibling helper, for transitions of any batch /// wire format. #[allow(clippy::too_many_arguments)] -fn fetch_documents_for_transitions_knowing_contract_and_document_type_v2( +fn fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type_v0( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs index 2e77dbf1acc..78e55531a3d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs @@ -9,7 +9,7 @@ use dpp::state_transition::batch_transition::BatchTransition; use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; -/// Generation 2 of the batch transformation, selected for protocol version 14 +/// Generation 2 of the batch transformation, selected for protocol version 15 /// and later: like generation 1 it threads the caller's execution context into /// the transformer, and it runs transformer generation 1, which sees every batch /// wire format and produces batch action format 1. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs index 62b1f0ffc56..603d491e2c5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs @@ -38,7 +38,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::{data_ use crate::platform_types::platform::PlatformStateRef; /// Generation 1 validates batch action format 1 against state, which is what a -/// batch of any wire format becomes for protocol version 14 and later, +/// batch of any wire format becomes for protocol version 15 and later, /// including the erase of a keep-history document. pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionValidationV1 { @@ -290,34 +290,9 @@ impl DocumentsBatchStateTransitionValidationV1 for BatchTransition { BatchedTransitionAction::DocumentAction(document_transition), ) = &transition { - // Pre-PR this site allocated a default-initialized local - // `StateTransitionExecutionContext` and passed `&local` to - // the trigger context. The local was dropped on return and - // all of its add_operation calls were silently discarded. - // `_v1` triggers need to actually bill (call add_operation), - // so the trigger context now references the OUTER mutable - // execution_context that the processor threaded in. - // - // PROTOCOL_VERSION_11 consensus-safety: on PV11 the - // per-trigger version fields stay at 0, so wrappers - // dispatch to `_v0` triggers whose bodies are - // byte-identical to v3.1-dev (only their param signature - // gained `&mut`, the body never mutates). _v0 triggers - // do not call `add_operation`, so the outer - // execution_context is read-only from the trigger's - // perspective on PV11 — same chain state as pre-PR. - // - // Non-consensus side-effect on PV11 mempool: the trigger - // now sees the outer ctx's real `dry_run` flag instead of - // the previous-default `false`. During CheckTx with - // `dry_run: true`, _v0 triggers short-circuit their - // `query_documents` (via `query_documents_v0`'s internal - // dry-run guard) and skip the post-query validation. - // Doesn't affect block validation (Validator mode is - // always `dry_run: false`), so chain replay matches - // pre-PR byte-for-byte. Pre-PR also did the query but - // ignored its result during dry-run validation, so the - // net mempool outcome is the same. + // The trigger context borrows the outer execution + // context so the triggers' billing and the real `dry_run` + // flag reach the processor. let owner_id_value = self.owner_id(); let mut data_trigger_execution_context = DataTriggerExecutionContext { platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index dce96efab4d..5387104ec18 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -391,12 +391,39 @@ mod deletion_tests { assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); } - /// Protocol 14 carries the delete out: the document leaves every ordinary - /// read and its retained revisions stay readable. + /// What a delete of a keep-history document does at each protocol version. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + enum KeepHistoryDeleteOutcome { + /// The released generations before protocol 14 hit the storage guard, + /// an internal error the block loop reports as such. + InternalError, + /// Protocol 14 refuses the delete as a paid consensus error. + PaidRejection, + /// Protocol 15 carries the delete out: the document leaves every + /// ordinary read and its retained revisions stay readable. + Deleted, + } + #[tokio::test] - async fn test_document_delete_on_document_type_that_keeps_history_succeeds_protocol_version_14() + async fn test_document_delete_on_document_type_that_keeps_history_succeeds_protocol_version_15() { - run_document_delete_on_document_type_that_keeps_history_at_protocol_version(14, true).await; + run_document_delete_on_document_type_that_keeps_history_at_protocol_version( + 15, + KeepHistoryDeleteOutcome::Deleted, + ) + .await; + } + + /// PROTOCOL_VERSION_14 rejects deletes against keep-history document types + /// as invalid-paid consensus errors and must keep doing so for replay. + #[tokio::test] + async fn test_document_delete_on_document_type_that_keeps_history_is_rejected_protocol_version_14( + ) { + run_document_delete_on_document_type_that_keeps_history_at_protocol_version( + 14, + KeepHistoryDeleteOutcome::PaidRejection, + ) + .await; } /// PROTOCOL_VERSION_12 preserves the historical InternalError result for @@ -404,23 +431,30 @@ mod deletion_tests { #[tokio::test] async fn test_document_delete_on_document_type_that_keeps_history_replays_protocol_version_12() { - run_document_delete_on_document_type_that_keeps_history_at_protocol_version(12, false) - .await; + run_document_delete_on_document_type_that_keeps_history_at_protocol_version( + 12, + KeepHistoryDeleteOutcome::InternalError, + ) + .await; } #[tokio::test] async fn test_document_delete_on_document_type_that_keeps_history_replays_protocol_version_13() { - run_document_delete_on_document_type_that_keeps_history_at_protocol_version(13, false) - .await; + run_document_delete_on_document_type_that_keeps_history_at_protocol_version( + 13, + KeepHistoryDeleteOutcome::InternalError, + ) + .await; } - /// Exercises an already-deployed keep-history contract at both sides of the - /// v14 validation-version boundary. Loading with `full_validation: false` - /// is intentional: reparsing deployed contracts must remain allowed. + /// Exercises an already-deployed keep-history contract on every side of the + /// v14 and v15 validation-version boundaries. Loading with + /// `full_validation: false` is intentional: reparsing deployed contracts + /// must remain allowed. async fn run_document_delete_on_document_type_that_keeps_history_at_protocol_version( protocol_version: dpp::version::ProtocolVersion, - expect_success: bool, + expected_outcome: KeepHistoryDeleteOutcome, ) { let platform_version = PlatformVersion::get(protocol_version) .expect("expected platform version for the requested protocol_version"); @@ -529,8 +563,6 @@ mod deletion_tests { .unwrap() .expect("expected to commit transaction"); - // V14 deletes; v12 and v13 retain the historical InternalError - // classification for replay. let documents_batch_deletion_transition = BatchTransition::new_document_deletion_transition_from_document( altered_document, @@ -572,11 +604,15 @@ mod deletion_tests { .unwrap() .expect("expected to commit transaction"); - assert_eq!(processing_result.invalid_paid_count(), 0); assert_eq!(processing_result.invalid_unpaid_count(), 0); + assert_eq!( + processing_result.invalid_paid_count(), + usize::from(expected_outcome == KeepHistoryDeleteOutcome::PaidRejection), + "unexpected invalid-paid classification at protocol version {protocol_version}" + ); assert_eq!( processing_result.valid_count(), - usize::from(expect_success), + usize::from(expected_outcome == KeepHistoryDeleteOutcome::Deleted), "unexpected success classification at protocol version {protocol_version}" ); let internal_error_count = processing_result @@ -586,11 +622,23 @@ mod deletion_tests { .count(); assert_eq!( internal_error_count, - usize::from(!expect_success), + usize::from(expected_outcome == KeepHistoryDeleteOutcome::InternalError), "unexpected InternalError classification at protocol version {protocol_version}" ); - if expect_success { + if expected_outcome == KeepHistoryDeleteOutcome::PaidRejection { + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError( + dpp::consensus::basic::BasicError::InvalidDocumentTransitionActionError(error) + ), + .. + }] if error.action() == "documents of type note can not be deleted" + ); + } + + if expected_outcome == KeepHistoryDeleteOutcome::Deleted { // The document is gone from ordinary reads, and its retained // revisions are not. let documents = platform diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 6ac9ef80715..5bba4af4f67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -9,7 +9,7 @@ use super::*; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; use dpp::consensus::basic::BasicError; -use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV3Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; use dpp::document::Document; @@ -71,7 +71,7 @@ fn assert_successful(result: &StateTransitionExecutionResult, context: &str) { ); } -/// A platform at protocol 14 with the erasable note contract applied, one +/// A platform at protocol 15 with the erasable note contract applied, one /// identity owning a `note` document, and a second identity that owns nothing. struct Fixture { platform: TempPlatform, @@ -92,12 +92,12 @@ impl Fixture { async fn new(document_type_name: &str) -> Self { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(14) + .with_initial_protocol_version(15) .build_with_mock_rpc() .set_initial_state_structure(); let contract = json_document_to_contract(ERASABLE_CONTRACT, true, platform_version) - .expect("the erasable note contract must pass full validation at protocol 14"); + .expect("the erasable note contract must pass full validation at protocol 15"); platform .drive .apply_contract( @@ -672,7 +672,7 @@ async fn should_let_any_identity_finish_an_erasure_its_owner_started() { let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the chunk") as u64; + .expect("protocol 15 bounds the chunk") as u64; let mut fixture = Fixture::new("note").await; fixture.retain_revisions("note", chunk + 1); @@ -712,7 +712,7 @@ async fn should_reject_a_delete_of_a_document_whose_erasure_has_begun() { let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the chunk") as u64; + .expect("protocol 15 bounds the chunk") as u64; let mut fixture = Fixture::new("note").await; fixture.retain_revisions("note", chunk + 1); @@ -737,7 +737,7 @@ async fn should_finish_an_erasure_across_two_transitions_in_one_block() { let chunk = PlatformVersion::latest() .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the chunk") as u64; + .expect("protocol 15 bounds the chunk") as u64; let mut fixture = Fixture::new("note").await; fixture.retain_revisions("note", chunk + 1); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs index 854bfe5cad6..da96f4be488 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs @@ -87,15 +87,15 @@ fn contested_keep_history() -> Value { async fn refused_create( schema: Value, ) -> (TempPlatform, Identifier, IdentityNonce) { - let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let platform_version = PlatformVersion::get(15).expect("protocol 15 exists"); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(14) + .with_initial_protocol_version(15) .build_with_mock_rpc() .set_genesis_state(); let platform_state = platform.state.load(); let (identity, signer, key) = setup_identity(&mut platform, 4001, dash_to_credits!(10.0)); - let contract = DataContractFactory::new(14) + let contract = DataContractFactory::new(15) .expect("expected a contract factory") .create_with_value_config( identity.id(), @@ -195,15 +195,15 @@ async fn should_charge_for_a_signed_contested_keep_history_contract() { /// The same refusal on the update path, which has its own error-category split. #[tokio::test] async fn should_charge_for_a_signed_contract_update_asking_for_erasure_without_history() { - let platform_version = PlatformVersion::get(14).expect("protocol 14 exists"); + let platform_version = PlatformVersion::get(15).expect("protocol 15 exists"); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(14) + .with_initial_protocol_version(15) .build_with_mock_rpc() .set_genesis_state(); let platform_state = platform.state.load(); let (identity, signer, key) = setup_identity(&mut platform, 4002, dash_to_credits!(1.0)); - let mut contract = DataContractFactory::new(14) + let mut contract = DataContractFactory::new(15) .expect("expected a contract factory") .create_with_value_config( identity.id(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs index aa44110fbe4..38f2ec9e912 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs @@ -1,4 +1,4 @@ -// Generation 1 of the batch transformer, selected for protocol version 14 and +// Generation 1 of the batch transformer, selected for protocol version 15 and // later. It views the batch through the shell that sees every batch wire // format, so a format 2 batch carrying an erase of a keep-history document is // transformed like any other, and it produces batch action format 1, whose @@ -44,6 +44,7 @@ use dpp::state_transition::batch_transition::batched_transition::{ }; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; @@ -185,10 +186,9 @@ trait BatchTransitionInternalTransformerV1 { original_document: &Document, ) -> SimpleConsensusValidationResult; fn failed_per_transition_action( - base_transition: &dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition, + base_transition: &DocumentBaseTransition, owner_id: Identifier, errors: Vec, - platform_version: &PlatformVersion, ) -> Result, Error>; } @@ -733,7 +733,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { "document value depth {actual_depth} exceeds system maximum {max_depth}" )), ))], - platform_version, ); } } @@ -785,7 +784,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_replace_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -804,7 +802,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_replace_transition.base(), owner_id, validation_result.errors, - platform_version, ); } } @@ -863,7 +860,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_transfer_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -880,7 +876,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_transfer_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -899,7 +894,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_transfer_transition.base(), owner_id, validation_result.errors, - platform_version, ); } } @@ -928,7 +922,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_update_price_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -945,7 +938,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_update_price_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -964,7 +956,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_update_price_transition.base(), owner_id, validation_result.errors, - platform_version, ); } } @@ -993,7 +984,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_purchase_transition.base(), owner_id, validation_result.errors, - platform_version, ); } @@ -1012,7 +1002,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { )) .into(), ], - platform_version, ); }; @@ -1028,7 +1017,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { ), ) .into()], - platform_version, ); } @@ -1047,7 +1035,6 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { document_purchase_transition.base(), owner_id, validation_result.errors, - platform_version, ); } } @@ -1141,10 +1128,9 @@ impl BatchTransitionInternalTransformerV1 for BatchTransition { } fn failed_per_transition_action( - base_transition: &dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition, + base_transition: &DocumentBaseTransition, owner_id: Identifier, errors: Vec, - _platform_version: &PlatformVersion, ) -> Result, Error> { let bump_action = BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( diff --git a/packages/rs-drive-abci/src/query/document_history/mod.rs b/packages/rs-drive-abci/src/query/document_history/mod.rs index 489aa551761..6ff4612d4c8 100644 --- a/packages/rs-drive-abci/src/query/document_history/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/mod.rs @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -56,13 +57,11 @@ impl Platform { self.query_document_history_v1(request, platform_state, platform_version)? } processing_version => { - return Err(Error::Execution( - crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: "query_document_history".to_string(), - known_versions: vec![0, 1], - received: processing_version, - }, - )); + return Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "query_document_history".to_string(), + known_versions: vec![0, 1], + received: processing_version, + })); } }; Ok(result.map(|response| GetDocumentHistoryResponse { diff --git a/packages/rs-drive-abci/src/query/document_history/v1/mod.rs b/packages/rs-drive-abci/src/query/document_history/v1/mod.rs index 5d9abafa73f..aeb2ed0a571 100644 --- a/packages/rs-drive-abci/src/query/document_history/v1/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/v1/mod.rs @@ -98,7 +98,7 @@ impl Platform { Err(error) => return Err(error.into()), }; // The proof travels in the layout the protocol version stores, - // signed once; from protocol version 14 that is one proof object + // signed once; from protocol version 15 that is one proof object // carrying both GroveDB proofs. let proof = self .response_proof_v0(platform_state, proof, GroveDBToUse::Current)? diff --git a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs index f2bba6a8ef0..4cca756061d 100644 --- a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs @@ -527,7 +527,7 @@ fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { let chunk = version .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the erase chunk") as u64; + .expect("protocol 15 bounds the erase chunk") as u64; for revision in 1..=chunk + 2 { document.set_revision(Some(revision)); platform diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs index f6d5802f25c..99a14319d3f 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/upgrade_fork_tests.rs @@ -1343,77 +1343,6 @@ mod tests { .unwrap(), root ); - - // The lifecycle works over migrated storage, not only over history - // written after activation. A contract registered before protocol 14 - // cannot declare `canBeErased` — the grammar that admits the keyword - // did not exist then — so this exercises the storage layer directly: - // whether a delete and an erase read and rewrite revision keys the - // migration produced. - use drive::drive::document::lifecycle::DocumentLifecycleState; - use drive::util::batch::{DocumentOperationType, DriveOperation}; - use drive::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; - - let document_id = dpp::identifier::Identifier::from_bytes(&document_id).unwrap(); - let document_type = contract.document_type_for_name("note").unwrap(); - let contract_info = || DataContractInfo::BorrowedDataContract(&contract); - let document_type_info = || DocumentTypeInfo::DocumentTypeName("note".to_string()); - let apply = |operation: DriveOperation, time_ms: u64| { - restarted - .drive - .apply_drive_operations( - vec![operation], - true, - &dpp::block::block_info::BlockInfo::default_with_time(time_ms), - None, - new_version, - None, - ) - .expect("expected to apply the operation") - }; - let lifecycle = || { - restarted - .drive - .fetch_document_lifecycle( - &contract, - document_type, - document_id, - None, - None, - new_version, - ) - .expect("expected to read the lifecycle") - .0 - }; - - apply( - DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { - document_id, - deleter_id: Some(dpp::identifier::Identifier::new([3u8; 32])), - contract_info: contract_info(), - document_type_info: document_type_info(), - }), - 1_681_094_400_000, - ); - let state = lifecycle(); - assert!( - matches!(state, DocumentLifecycleState::Deleted(_)), - "a migrated document must be deletable, got {state:?}" - ); - - apply( - DriveOperation::DocumentOperation(DocumentOperationType::EraseDocument { - document_id, - contract_info: contract_info(), - document_type_info: document_type_info(), - }), - 1_681_094_500_000, - ); - let state = lifecycle(); - assert!( - matches!(state, DocumentLifecycleState::Absent), - "erasing every migrated revision must free the id, got {state:?}" - ); } #[stack_size(4 * 1024 * 1024)] 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 b651ac2bda8..fb3dc180bb3 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 @@ -7,6 +7,7 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use dpp::block::block_info::BlockInfo; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; @@ -99,6 +100,17 @@ impl Drive { transaction, platform_version, ), + // This signature carries only the block time. Generation 1 records + // a lifecycle entry for a keep-history document with the block and + // deleter that authored the delete, which only + // `delete_document_for_contract_operations_with_lifecycle` receives, + // so it serves the types that keep no history and refuses the rest + // instead of recording a fabricated block. + 1 if document_type.documents_keep_history() => { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "deleting a keep-history document needs the block and deleter that authored it", + ))) + } 1 => self.delete_document_for_contract_operations_v1( document_id, contract, @@ -260,6 +272,13 @@ impl Drive { transaction, platform_version, ), + // As above: without the authoring block and deleter, generation 1 + // serves only the types that keep no history. + 1 if document_type.documents_keep_history() => { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "deleting a keep-history document needs the block and deleter that authored it", + ))) + } 1 => self.force_delete_document_for_contract_operations_v1( document_id, contract, diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 890bf9407ca..1eb714ffac8 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1263,17 +1263,17 @@ mod tests { ); } - /// Protocol 13 refuses the delete outright, and protocol 14 carries it out: + /// Protocol 14 refuses the delete outright, and protocol 15 carries it out: /// the document leaves every ordinary read while its revisions stay where /// they are. #[test] - fn test_delete_document_keeps_history_returns_error_at_protocol_13() { - run_keep_history_delete_at_protocol_version(13); + fn test_delete_document_keeps_history_returns_error_at_protocol_14() { + run_keep_history_delete_at_protocol_version(14); } #[test] - fn should_delete_a_keep_history_document_without_touching_its_revisions_at_protocol_14() { - run_keep_history_delete_at_protocol_version(14); + fn should_delete_a_keep_history_document_without_touching_its_revisions_at_protocol_15() { + run_keep_history_delete_at_protocol_version(15); } fn run_keep_history_delete_at_protocol_version(protocol_version: u32) { @@ -1356,7 +1356,7 @@ mod tests { Some(&EPOCH_CHANGE_FEE_VERSION_TEST), ); - if protocol_version < 14 { + if protocol_version < 15 { assert!(matches!( outcome.expect_err("expected deleting a history-keeping document to fail"), Error::Drive(DriveError::InvalidDeletionOfDocumentThatKeepsHistory(_)) diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index 80a8c8f8700..b7204c4feb0 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -108,7 +108,7 @@ impl DocumentLifecycleRecord { /// deleted revision without a gap, which is what lets a by-revision read /// map a revision onto a position in the history. /// - /// A gap can only come from a history written before protocol 14, where + /// A gap can only come from a history written before protocol 15, where /// two writes in one block overwrote each other's revision; an erase /// removes the newest revisions first and so never opens one. pub fn revisions_are_contiguous(&self) -> bool { diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 7e2295817fe..f661688f884 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -44,7 +44,7 @@ fn chunk_size() -> u64 { latest() .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the erase chunk") as u64 + .expect("protocol 15 bounds the erase chunk") as u64 } /// Applies the family contract and writes `revisions` revisions of one person, @@ -1042,7 +1042,7 @@ fn should_reject_an_unsupported_erase_estimation_version() { .delete .add_estimation_costs_for_erase_document, Some(0), - "protocol 14 selects the only implementation there is" + "protocol 15 selects the only implementation there is" ); version .drive @@ -1663,14 +1663,14 @@ fn should_read_revisions_by_position_after_a_partial_erasure() { ); } -/// A history that protocol 13 left gapped keeps refusing by-revision reads -/// after the document is deleted: a revision no longer maps onto a position, -/// and an empty page for a revision that exists would otherwise verify as an -/// authenticated absence. +/// A history that the legacy layout left gapped keeps refusing by-revision +/// reads after the document is deleted: a revision no longer maps onto a +/// position, and an empty page for a revision that exists would otherwise +/// verify as an authenticated absence. #[test] fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { - let old = PlatformVersion::get(13).unwrap(); - let new = PlatformVersion::get(14).unwrap(); + let old = PlatformVersion::get(14).unwrap(); + let new = PlatformVersion::get(15).unwrap(); let drive = setup_drive_with_initial_state_structure(None); let contract = json_document_to_contract( "tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json", @@ -1690,7 +1690,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { old, ) .unwrap(); - // Two writes in one block under protocol 13 overwrite each other's + // Two writes in one block under the legacy layout overwrite each other's // revision, so revision 2 is lost and the retained history is [1, 3]. for (revision, time) in [(1, 1_000), (2, 2_000), (3, 2_000)] { document.set_revision(Some(revision)); @@ -1792,7 +1792,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { .expect("time pagination still reads a deleted gapped history"); let lifecycle = page .lifecycle - .expect("protocol 14 authenticates lifecycle metadata"); + .expect("protocol 15 authenticates lifecycle metadata"); assert_eq!(lifecycle.state, DocumentHistoryState::Deleted); assert_eq!(lifecycle.remaining_revisions, 2); assert_eq!( @@ -1838,7 +1838,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { .expect("an unused id reads as an authenticated absence"); assert_eq!( page.lifecycle - .expect("protocol 14 authenticates lifecycle metadata") + .expect("protocol 15 authenticates lifecycle metadata") .state, DocumentHistoryState::Absent ); diff --git a/packages/rs-drive/src/drive/document/migration/tests.rs b/packages/rs-drive/src/drive/document/migration/tests.rs index 8a23bda8061..bb791d23741 100644 --- a/packages/rs-drive/src/drive/document/migration/tests.rs +++ b/packages/rs-drive/src/drive/document/migration/tests.rs @@ -640,7 +640,7 @@ fn should_halt_migration_when_an_inventoried_index_reference_was_not_rewritten() } /// A keep-history type with a contested index could be registered before -/// protocol 14. Activation loads every stored contract through the structural +/// protocol 15. Activation loads every stored contract through the structural /// parse, which must keep reading that combination, or the migration would /// abort on the first such contract before touching any history. #[test] @@ -649,8 +649,8 @@ fn should_migrate_a_legacy_contested_keep_history_contract() { use dpp::document::document_factory::DocumentFactory; use dpp::platform_value::platform_value; - let old = PlatformVersion::get(13).unwrap(); - let new = PlatformVersion::get(14).unwrap(); + let old = PlatformVersion::get(14).unwrap(); + let new = PlatformVersion::get(15).unwrap(); let schema = platform_value!({ "type": "object", "documentsKeepHistory": true, @@ -671,7 +671,7 @@ fn should_migrate_a_legacy_contested_keep_history_contract() { }, }], }); - let contract = DataContractFactory::new(13) + let contract = DataContractFactory::new(14) .unwrap() .create_with_value_config( [7; 32].into(), @@ -680,14 +680,14 @@ fn should_migrate_a_legacy_contested_keep_history_contract() { None, None, ) - .expect("protocol 13 admits a contested keep-history type") + .expect("protocol 14 admits a contested keep-history type") .data_contract_owned(); let drive = setup_drive_with_initial_state_structure(None); drive .apply_contract(&contract, BlockInfo::default(), true, None, None, old) .unwrap(); let document_type = contract.document_type_for_name("name").unwrap(); - let mut document = DocumentFactory::new(13) + let mut document = DocumentFactory::new(14) .unwrap() .create_document( &contract, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs index 83dc0bcb858..fc8cda86fcc 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs @@ -124,7 +124,7 @@ mod tests { let result = action.into_high_level_drive_operations( &epoch, - PlatformVersion::get(13).expect("protocol 13"), + PlatformVersion::get(14).expect("protocol 14"), ); assert!(matches!( diff --git a/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs b/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs index 461d32456de..2fc94f740cb 100644 --- a/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs @@ -18,6 +18,9 @@ use dpp::identity::SecurityLevel; use dpp::prelude::UserFeeIncrease; use dpp::ProtocolError; +#[cfg(test)] +mod tests; + /// One item of a batch action in format 1. #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, From)] diff --git a/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs b/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs new file mode 100644 index 00000000000..20207a570cb --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs @@ -0,0 +1,316 @@ +//! Batch action format 1 must agree with format 0 on every item format 0 can +//! express, and treat an erase as a document action that moves no credits. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::DocumentType; +use dpp::document::{Document, DocumentV0}; +use dpp::identifier::Identifier; +use dpp::identity::SecurityLevel; +use dpp::platform_value::platform_value; +use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; +use dpp::version::PlatformVersion; +use grovedb_costs::OperationCost; + +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{ + DocumentBaseTransitionAction, DocumentBaseTransitionActionV0, +}; +use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{ + DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionV0, +}; +use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionV0, +}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::{ + TokenDirectPurchaseTransitionAction, TokenDirectPurchaseTransitionActionV0, +}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; +use crate::state_transition_action::batch::v0::BatchTransitionActionV0; +use crate::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; +use crate::state_transition_action::batch::BatchTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{ + BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0, +}; + +const OWNER_ID: [u8; 32] = [0x11; 32]; +const DOCUMENT_ID: [u8; 32] = [0xAA; 32]; +const PURCHASE_PRICE: u64 = 5_000; +const TOKEN_PRICE: u64 = 10_000; + +fn dpns_contract_info() -> Arc { + Arc::new(DataContractFetchInfo::dpns_contract_fixture( + PlatformVersion::latest().protocol_version, + )) +} + +/// The DPNS contract with one extra document type whose signature must come +/// from a master key. +fn contract_info_with_master_level_type() -> Arc { + let platform_version = PlatformVersion::latest(); + let mut contract = dpns_contract_info().contract.clone(); + let config = *contract.config(); + let schema = platform_value!({ + "type": "object", + "properties": { + "note": { + "type": "string", + "position": 0, + } + }, + "signatureSecurityLevelRequirement": SecurityLevel::MASTER as u8, + "additionalProperties": false, + }); + let document_type = DocumentType::try_from_schema( + contract.id(), + 1, + config.version(), + "vault", + schema, + None, + &BTreeMap::new(), + &config, + false, + &mut Vec::new(), + platform_version, + ) + .expect("a document type with a master-level signature requirement"); + contract + .document_types_mut() + .insert("vault".to_string(), document_type); + Arc::new(DataContractFetchInfo { + contract, + storage_flags: None, + cost: OperationCost::default(), + fee: None, + }) +} + +fn document_base( + document_type_name: &str, + data_contract: Arc, +) -> DocumentBaseTransitionAction { + DocumentBaseTransitionAction::V0(DocumentBaseTransitionActionV0 { + id: Identifier::from(DOCUMENT_ID), + identity_contract_nonce: 1, + document_type_name: document_type_name.to_string(), + data_contract, + token_cost: None, + gas_fees_paid_by: GasFeesPaidBy::default(), + }) +} + +fn erase( + document_type_name: &str, + data_contract: Arc, +) -> BatchedTransitionActionV1 { + BatchedTransitionActionV1::DocumentErase(DocumentEraseTransitionAction::V0( + DocumentEraseTransitionActionV0 { + base: document_base(document_type_name, data_contract), + }, + )) +} + +fn domain_delete() -> BatchedTransitionAction { + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::DeleteAction( + DocumentDeleteTransitionAction::V0(DocumentDeleteTransitionActionV0 { + base: document_base("domain", dpns_contract_info()), + }), + )) +} + +fn domain_purchase() -> BatchedTransitionAction { + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::PurchaseAction( + DocumentPurchaseTransitionAction::V0(DocumentPurchaseTransitionActionV0 { + base: document_base("domain", dpns_contract_info()), + document: Document::V0(DocumentV0::default()), + original_owner_id: Identifier::from([0xDD; 32]), + price: PURCHASE_PRICE, + }), + )) +} + +fn token_purchase() -> BatchedTransitionAction { + BatchedTransitionAction::TokenAction(TokenTransitionAction::DirectPurchaseAction( + TokenDirectPurchaseTransitionAction::V0(TokenDirectPurchaseTransitionActionV0 { + base: TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { + token_id: Identifier::from([0xBB; 32]), + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract: dpns_contract_info(), + store_in_group: None, + perform_action: true, + }), + token_count: 50, + total_agreed_price: TOKEN_PRICE, + }), + )) +} + +fn nonce_bump() -> BatchedTransitionAction { + BatchedTransitionAction::BumpIdentityDataContractNonce(BumpIdentityDataContractNonceAction::V0( + BumpIdentityDataContractNonceActionV0 { + identity_id: Identifier::from(OWNER_ID), + data_contract_id: Identifier::from([0x22; 32]), + identity_contract_nonce: 5, + user_fee_increase: 0, + }, + )) +} + +fn format_0_batch(transitions: Vec) -> BatchTransitionAction { + BatchTransitionAction::V0(BatchTransitionActionV0 { + owner_id: Identifier::from(OWNER_ID), + transitions, + user_fee_increase: 10, + }) +} + +fn format_1_batch(transitions: Vec) -> BatchTransitionActionV1 { + BatchTransitionActionV1 { + owner_id: Identifier::from(OWNER_ID), + transitions, + user_fee_increase: 10, + } +} + +#[test] +fn should_upcast_a_format_0_batch_keeping_its_items_in_order() { + let upcast: BatchTransitionActionV1 = + format_0_batch(vec![domain_delete(), token_purchase(), nonce_bump()]).into(); + + assert_eq!(upcast.owner_id(), Identifier::from(OWNER_ID)); + assert_eq!(upcast.user_fee_increase(), 10); + let kinds: Vec<_> = upcast + .transitions() + .iter() + .map(|item| match item { + BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction(_)) => { + "document" + } + BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction(_)) => "token", + BatchedTransitionActionV1::Batched( + BatchedTransitionAction::BumpIdentityDataContractNonce(_), + ) => "bump", + BatchedTransitionActionV1::DocumentErase(_) => "erase", + }) + .collect(); + assert_eq!(kinds, ["document", "token", "bump"]); +} + +/// The credits a batch commits up front decide whether the signer can afford +/// it; an erase commits none, so its presence must not change the sums. +#[test] +fn should_sum_the_same_credits_as_format_0_with_an_erase_alongside() { + let format_0 = format_0_batch(vec![domain_purchase(), token_purchase()]); + let with_erase = format_1_batch(vec![ + erase("domain", dpns_contract_info()), + BatchedTransitionActionV1::Batched(domain_purchase()), + BatchedTransitionActionV1::Batched(token_purchase()), + ]); + + assert_eq!( + with_erase.all_purchases_amount().unwrap(), + format_0.all_purchases_amount().unwrap() + ); + assert_eq!( + with_erase.all_purchases_amount().unwrap(), + Some(PURCHASE_PRICE + TOKEN_PRICE) + ); + assert_eq!( + with_erase + .all_conflicting_index_collateral_voting_funds() + .unwrap(), + format_0 + .all_conflicting_index_collateral_voting_funds() + .unwrap() + ); + assert_eq!( + with_erase.all_used_balances().unwrap(), + format_0.all_used_balances().unwrap() + ); + + let erase_only = format_1_batch(vec![erase("domain", dpns_contract_info())]); + assert_eq!(erase_only.all_purchases_amount().unwrap(), None); + assert_eq!( + erase_only + .all_conflicting_index_collateral_voting_funds() + .unwrap(), + None + ); + assert_eq!(erase_only.all_used_balances().unwrap(), None); +} + +/// An erase is signed like any other action on its document type. +#[test] +fn should_require_the_erased_types_security_level() { + let domain_level = dpns_contract_info() + .contract + .document_type_for_name("domain") + .expect("the DPNS contract has a domain type") + .security_level_requirement(); + assert_ne!( + domain_level, + SecurityLevel::MASTER, + "the test needs a type whose requirement spans more than one level" + ); + + let erase_only = format_1_batch(vec![erase("domain", dpns_contract_info())]); + let delete_only = format_0_batch(vec![domain_delete()]); + + let expected: Vec = (SecurityLevel::CRITICAL as u8..=domain_level as u8) + .map(|level| SecurityLevel::try_from(level).unwrap()) + .collect(); + assert_eq!( + erase_only.combined_security_level_requirement().unwrap(), + expected + ); + assert_eq!( + erase_only.combined_security_level_requirement().unwrap(), + delete_only.combined_security_level_requirement().unwrap(), + "an erase and a delete of the same type demand the same key" + ); +} + +#[test] +fn should_require_a_critical_key_for_a_token_action() { + let batch = format_1_batch(vec![ + erase("domain", dpns_contract_info()), + BatchedTransitionActionV1::Batched(token_purchase()), + ]); + assert_eq!( + batch.combined_security_level_requirement().unwrap(), + vec![SecurityLevel::CRITICAL] + ); +} + +#[test] +fn should_require_a_master_key_alone_when_an_erased_type_demands_it() { + let batch = format_1_batch(vec![ + BatchedTransitionActionV1::Batched(domain_delete()), + erase("vault", contract_info_with_master_level_type()), + ]); + assert_eq!( + batch.combined_security_level_requirement().unwrap(), + vec![SecurityLevel::MASTER] + ); +} + +#[test] +fn should_agree_with_format_0_on_a_batch_of_nonce_bumps() { + let format_0 = format_0_batch(vec![nonce_bump(), nonce_bump()]); + let format_1: BatchTransitionActionV1 = format_0.clone().into(); + assert_eq!( + format_1.combined_security_level_requirement().unwrap(), + format_0.combined_security_level_requirement().unwrap() + ); +} 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 1f8b48cee1f..6edf840e518 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 @@ -278,9 +278,10 @@ impl DocumentOperationType<'_> { Ok(()) } // These write to system contracts, which have no TTL indexes. - Self::AddWithdrawalDocument { .. } - | Self::DocumentHistory { .. } - | Self::EraseDocument { .. } => Ok(()), + Self::AddWithdrawalDocument { .. } | Self::DocumentHistory { .. } => Ok(()), + // An erase acts on a document whose delete already removed every + // index reference, so there is nothing left to drain. + Self::EraseDocument { .. } => Ok(()), } } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index ec80ba92ca8..9f061b41434 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -5,6 +5,7 @@ pub mod v3; pub mod v4; pub mod v5; pub mod v6; +pub mod v7; #[derive(Clone, Debug, Default)] pub struct DPPContractVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs new file mode 100644 index 00000000000..c0f20249551 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs @@ -0,0 +1,36 @@ +use crate::version::dpp_versions::dpp_contract_versions::v6::CONTRACT_VERSIONS_V6; +use crate::version::dpp_versions::dpp_contract_versions::{ + DPPContractVersions, DocumentTypeClassMethodVersions, DocumentTypeSchemaVersions, + DocumentTypeVersions, +}; + +// Introduced in protocol version 15 for the delete and erase lifecycle of +// keep-history documents. Uses the v4 document meta-schema, which is v3 plus +// the `canBeErased` keyword. +// +// `try_from_schema` moves to 4, selecting a new document-type parser +// generation (`try_from_schema/v4`). Generation 4 admits `documentsKeepHistory` +// together with `canBeDeleted` (a delete leaves the retained revisions +// readable), parses `canBeErased` (a deleted document's revisions may be +// purged), and refuses a keep-history type that carries a contested index. +// Generation 3 keeps rejecting deletable keep-history types, so replaying a +// protocol 14 block validates contracts exactly as it did. +// +// `document_type_schema` moves to 4 in the same step: generation 4 and +// meta-schema v4 are introduced together and pair by construction. +pub const CONTRACT_VERSIONS_V7: DPPContractVersions = DPPContractVersions { + document_type_versions: DocumentTypeVersions { + class_method_versions: DocumentTypeClassMethodVersions { + try_from_schema: 4, + ..CONTRACT_VERSIONS_V6 + .document_type_versions + .class_method_versions + }, + schema: DocumentTypeSchemaVersions { + document_type_schema: 4, + ..CONTRACT_VERSIONS_V6.document_type_versions.schema + }, + ..CONTRACT_VERSIONS_V6.document_type_versions + }, + ..CONTRACT_VERSIONS_V6 +}; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs index e4ae4bfd3a8..e3819244196 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs @@ -3,6 +3,7 @@ use versioned_feature_core::FeatureVersionBounds; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DPPStateTransitionSerializationVersions { @@ -29,7 +30,7 @@ pub struct DPPStateTransitionSerializationVersions { /// gate rejects the variant wherever this is `None`. pub document_index_only_delete_state_transition: Option, /// The erase kind, which purges the retained revisions of a deleted - /// keep-history document. `None` below protocol version 14 — the kind does + /// keep-history document. `None` below protocol version 15 — the kind does /// not exist on the wire there, and the batch basic-structure wire gate /// rejects the variant wherever this is `None`. pub document_erase_state_transition: Option, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs index 770faf5bc76..db17cbcd7b5 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs @@ -98,7 +98,7 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V1: DPPStateTransitionSerializ }, // The indexOnly delete kind joins the wire at PV14. document_index_only_delete_state_transition: None, - // The erase kind joins the wire at PV14. + // The erase kind joins the wire at protocol version 15. document_erase_state_transition: None, document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs index 1b05bb649cc..373db1f0c13 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs @@ -98,7 +98,7 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V2: DPPStateTransitionSerializ }, // The indexOnly delete kind joins the wire at PV14. document_index_only_delete_state_transition: None, - // The erase kind joins the wire at PV14. + // The erase kind joins the wire at protocol version 15. document_erase_state_transition: None, document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs index 53dab036184..383723f26eb 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -1,15 +1,9 @@ -//! V3 (PV14): identical to V2 except for two new kinds, both `Some` with V0 -//! bounds only. -//! -//! `document_index_only_delete_state_transition` is the indexOnly -//! delete-by-values transition (`DocumentIndexOnlyDeleteTransition`). -//! `document_erase_state_transition` is the erase transition -//! (`DocumentEraseTransition`), which purges the retained revisions of a -//! deleted keep-history document. -//! -//! Each is its own kind, not a version of the delete transition: -//! `document_delete_state_transition` keeps V2's bounds and keeps evolving -//! independently for stored types. +//! V3 (PV14): identical to V2 except the new +//! `document_index_only_delete_state_transition` kind exists (`Some`, V0 +//! only) — the indexOnly delete-by-values transition +//! (`DocumentIndexOnlyDeleteTransition`). It is its own kind, not a +//! version of the delete transition: `document_delete_state_transition` +//! keeps V2's bounds and keeps evolving independently for stored types. use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ DPPStateTransitionSerializationVersions, DocumentFeatureVersionBounds, @@ -80,8 +74,8 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ }, batch_state_transition: FeatureVersionBounds { min_version: 0, - max_version: 2, - default_current_version: 2, + max_version: 1, + default_current_version: 1, }, document_base_state_transition: FeatureVersionBounds { min_version: 0, @@ -116,13 +110,8 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ default_current_version: 0, }, }), - document_erase_state_transition: Some(DocumentFeatureVersionBounds { - bounds: FeatureVersionBounds { - min_version: 0, - max_version: 0, - default_current_version: 0, - }, - }), + // The erase kind joins the wire at protocol version 15. + document_erase_state_transition: None, document_transfer_state_transition: DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs new file mode 100644 index 00000000000..f8ea147b407 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs @@ -0,0 +1,30 @@ +//! V4 (PV15): V3 plus batch transition format 2 and the erase kind. +//! +//! Format 2 carries a document transition shell that knows the erase kind +//! (`DocumentEraseTransition`), which purges the retained revisions of a +//! deleted keep-history document. Formats 0 and 1 keep their shells, so the +//! kind cannot appear in them; `document_erase_state_transition` bounds the +//! kind's own generation and stays `None` in every earlier table. + +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ + DPPStateTransitionSerializationVersions, DocumentFeatureVersionBounds, +}; +use versioned_feature_core::FeatureVersionBounds; + +pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V4: DPPStateTransitionSerializationVersions = + DPPStateTransitionSerializationVersions { + batch_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 2, + default_current_version: 2, + }, + document_erase_state_transition: Some(DocumentFeatureVersionBounds { + bounds: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + }), + ..STATE_TRANSITION_SERIALIZATION_VERSIONS_V3 + }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs index a670bc78b8b..b63679cdd24 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs @@ -4,7 +4,7 @@ use crate::version::dpp_versions::dpp_state_transition_versions::{ DocumentsBatchTransitionValidationVersions, DocumentsBatchTransitionVersions, }; -/// Protocol 14 selects the batch validator that sees every batch wire format, +/// Protocol 15 selects the batch validator that sees every batch wire format, /// including format 2, whose document shell carries the erase kind. The /// generation selected by earlier protocol versions only sees the shell of /// formats 0 and 1 and remains unchanged. diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs index 1149cdaa32d..39de89da284 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs @@ -5,6 +5,7 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; #[derive(Clone, Debug, Default)] pub struct DPPValidationVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs new file mode 100644 index 00000000000..dbb8ce60616 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs @@ -0,0 +1,18 @@ +use crate::version::dpp_versions::dpp_validation_versions::{ + DPPValidationVersions, DocumentTypeValidationVersions, +}; + +use super::v5::DPP_VALIDATION_VERSIONS_V5; + +/// Protocol v15 validation versions. +/// +/// v2 document-type update validation knows the `canBeErased` keyword: it +/// keeps erasability immutable in both directions and lets a keep-history +/// type withdraw deletion only while it is not erasable. +pub const DPP_VALIDATION_VERSIONS_V6: DPPValidationVersions = DPPValidationVersions { + document_type: DocumentTypeValidationVersions { + validate_update: 2, + ..DPP_VALIDATION_VERSIONS_V5.document_type + }, + ..DPP_VALIDATION_VERSIONS_V5 +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs index e45ea1c4e70..a3de509ffbc 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs @@ -2,6 +2,7 @@ pub mod v0; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; use versioned_feature_core::{FeatureVersion, FeatureVersionBounds}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs index edd50cdc3f7..351c4889e11 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs @@ -41,7 +41,7 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V3: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, - document_history_processing: 1, + document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 2, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs new file mode 100644 index 00000000000..a982b6417c4 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs @@ -0,0 +1,13 @@ +use crate::version::drive_abci_versions::drive_abci_query_versions::v3::DRIVE_ABCI_QUERY_VERSIONS_V3; +use crate::version::drive_abci_versions::drive_abci_query_versions::DriveAbciQueryVersions; + +/// Version 4 of the Drive ABCI query versions. +/// +/// Differs from v3 in one slot: `document_history_processing` is 1, the +/// history handler that reports the deleted and erasing lifecycle states of a +/// keep-history document with their times. The wire version of the request and +/// response is unchanged. +pub const DRIVE_ABCI_QUERY_VERSIONS_V4: DriveAbciQueryVersions = DriveAbciQueryVersions { + document_history_processing: 1, + ..DRIVE_ABCI_QUERY_VERSIONS_V3 +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index aa876081619..80b5a065c28 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -1,5 +1,6 @@ pub mod v1; pub mod v10; +pub mod v11; pub mod v2; pub mod v3; pub mod v4; @@ -214,6 +215,14 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// (PROTOCOL_VERSION_12+) passes `Some(epoch)` and bills via /// `execution_context.add_operation`. pub fetch_documents_for_transitions_knowing_contract_and_document_type: FeatureVersion, + /// Versions + /// `fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type`, + /// the sibling helper that reads transitions through the shell that knows + /// every batch wire format. Active from protocol version 15; earlier + /// versions only see the shell of wire formats 0 and 1 and use the helper + /// above. + pub fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + OptionalFeatureVersion, /// Versions the `fetch_document_with_id` helper. Same v0 vs v1 /// semantics as /// `fetch_documents_for_transitions_knowing_contract_and_document_type`. @@ -225,7 +234,7 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_structure_validation: FeatureVersion, - /// The erase kind, active from protocol version 14. + /// The erase kind, active from protocol version 15. pub document_erase_transition_structure_validation: OptionalFeatureVersion, pub document_replace_transition_structure_validation: FeatureVersion, pub document_transfer_transition_structure_validation: FeatureVersion, @@ -237,11 +246,11 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// The indexOnly delete-by-values kind (PV14+); 0 in every earlier /// version table, where the kind cannot appear. pub document_index_only_delete_transition_state_validation: FeatureVersion, - /// The erase kind, active from protocol version 14. + /// The erase kind, active from protocol version 15. pub document_erase_transition_state_validation: OptionalFeatureVersion, /// Versions `fetch_keep_history_document_lifecycle`, the stateful read /// that classifies a keep-history document as active, deleted, erasing or - /// absent. The helper is active from protocol version 14. + /// absent. The helper is active from protocol version 15. pub fetch_keep_history_document_lifecycle: OptionalFeatureVersion, pub document_replace_transition_state_validation: FeatureVersion, pub document_transfer_transition_state_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index a5faa5a3acb..ae11c96b747 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -109,6 +109,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index a831222eba9..e0415355848 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -117,10 +117,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { basic_structure: 0, - advanced_structure: 1, - state: 1, - // Generation 1 reads the nonces of a batch of any wire format. - revision: 1, + advanced_structure: 0, + state: 0, + revision: 0, // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition // fee accounting fixes. This single field gates multiple // related billing changes so they all activate together at @@ -144,7 +143,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // * T3 — DashPay data trigger recipient identity-balance // fetch cost (switched to `fetch_identity_balance_with_costs`). // * T4 — withdrawals data trigger `query_documents` cost. - transform_into_action: 2, + transform_into_action: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit // a `BumpIdentityDataContractNonce` action so the user pays @@ -157,9 +156,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // their query_documents calls. v0 stays for PV11 chain // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). - // Generation 2 keeps generation 1's billing and reads the - // transitions of any batch wire format. - fetch_documents_for_transitions_knowing_contract_and_document_type: 2, + fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { // PROTOCOL_VERSION_14: v2 adds DashPay `profile` @@ -186,29 +185,23 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = reject_data_trigger: 0, }, }, - // Generation 1 reads a batch of any wire format. - is_allowed: 1, + is_allowed: 0, document_create_transition_structure_validation: 1, - // Keep-history types take part in the document lifecycle: a delete - // removes the current pointer and its index references while the - // retained revisions stay readable. Protocols through 13 retain the - // original internal-error outcome. + // Reject deletes on legacy keep-history types as paid consensus errors. + // Protocols through 13 retain the original internal-error outcome. document_delete_transition_structure_validation: 1, document_index_only_delete_transition_structure_validation: 0, - document_erase_transition_structure_validation: Some(0), + document_erase_transition_structure_validation: None, document_replace_transition_structure_validation: 0, document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, document_update_price_transition_structure_validation: 0, document_base_transition_state_validation: 0, document_create_transition_state_validation: 2, - // Consults the keep-history lifecycle before deleting, so a delete - // of an already deleted or erasing document is a paid consensus - // error rather than a second removal. - document_delete_transition_state_validation: 1, + document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, - document_erase_transition_state_validation: Some(0), - fetch_keep_history_document_lifecycle: Some(0), + document_erase_transition_state_validation: None, + fetch_keep_history_document_lifecycle: None, document_replace_transition_state_validation: 1, document_transfer_transition_state_validation: 0, document_purchase_transition_state_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs new file mode 100644 index 00000000000..0dc6391c704 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -0,0 +1,53 @@ +use crate::version::drive_abci_versions::drive_abci_validation_versions::v10::DRIVE_ABCI_VALIDATION_VERSIONS_V10; +use crate::version::drive_abci_versions::drive_abci_validation_versions::{ + DriveAbciDocumentsStateTransitionValidationVersions, + DriveAbciStateTransitionValidationVersions, DriveAbciValidationVersions, +}; + +/// Protocol v15 validation versions: the delete and erase lifecycle of +/// keep-history documents, carried by batch transition wire format 2. +/// +/// Every batch generation bumped here reads a batch through the shell that +/// knows the erase kind and produces or consumes batch action format 1, whose +/// items may include an erase. The generations selected by earlier protocol +/// versions keep the shell and action format of wire formats 0 and 1. +/// +/// * `transform_into_action: 2` runs the transformer that sees every wire +/// format and emits action format 1; +/// * `advanced_structure: 1` and `state: 1` validate action format 1; +/// * `revision: 1` and `is_allowed: 1` read nonces and contested creates from +/// a batch of any wire format; +/// * `fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: +/// Some(0)` bills like generation 1 of the sibling helper for transitions +/// viewed through the shell of any wire format; +/// * delete structure v2 admits a delete of a keep-history document and +/// refuses one whose type carries a contested index; delete state v1 refuses +/// a delete of a document that is already deleted or erasing as a paid +/// consensus error; create state v3 refuses the id of such a document; the +/// erase generations validate the erase itself and read the lifecycle it +/// acts on. +pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = + DriveAbciValidationVersions { + state_transitions: DriveAbciStateTransitionValidationVersions { + batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { + advanced_structure: 1, + state: 1, + revision: 1, + transform_into_action: 2, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + Some(0), + is_allowed: 1, + document_delete_transition_structure_validation: 2, + document_create_transition_state_validation: 3, + document_erase_transition_structure_validation: Some(0), + document_delete_transition_state_validation: 1, + document_erase_transition_state_validation: Some(0), + fetch_keep_history_document_lifecycle: Some(0), + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .batch_state_transition + }, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10.state_transitions + }, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index 46edb3178c1..dd3e3d44f32 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -109,6 +109,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 95ef1fcccda..e975e2f85db 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -109,6 +109,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 8f962d14a87..e226bf423fb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -112,6 +112,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index dcbf2db5503..7eb5c759cb8 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -113,6 +113,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 33ead66ce64..8ec6cdf98d5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -116,6 +116,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index c9befa9af48..9844beb851e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -110,6 +110,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 83a0b29efc2..c71e1e6c33f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -158,6 +158,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 1de62df75b3..028798e9db4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -151,6 +151,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: + None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { // PROTOCOL_VERSION_13: v1 drops the reject bindings for 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 82429ab676e..23a05aec606 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 @@ -40,7 +40,7 @@ pub struct DriveDocumentQueryMethodVersions { pub primary_key_path_query: FeatureVersion, /// Reads the lifecycle record of one keep-history document and, when the /// record says the document is deleted, its newest retained revision. - /// Absent before protocol version 14, which introduces the lifecycle tree. + /// Absent before protocol version 15, which introduces the lifecycle tree. pub fetch_document_lifecycle: OptionalFeatureVersion, /// Mode-detection routing table for `SELECT COUNT` queries. /// Versioned because the routing table is consensus-relevant on @@ -161,7 +161,7 @@ pub struct DriveDocumentDeleteMethodVersions { pub delete_index_only_document_for_contract: FeatureVersion, /// Removes a bounded chunk of the retained revisions of a deleted /// keep-history document, dropping the history subtree and the lifecycle - /// record with the terminal chunk. Absent before protocol version 14. + /// record with the terminal chunk. Absent before protocol version 15. pub erase_document_for_contract_operations: OptionalFeatureVersion, /// Estimation layers for the erase chunk. Same dormancy as /// `erase_document_for_contract_operations`. 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 1f856f683b0..0a1878137bc 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 @@ -82,9 +82,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = fetch_document_history: 0, prove_document_history: 0, primary_key_path_query: 0, - // The lifecycle tree is introduced with the delete and erase - // lifecycle; the read exists only from protocol version 14. - fetch_document_lifecycle: Some(0), + fetch_document_lifecycle: None, detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, @@ -95,32 +93,24 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = where_clause_grouping: 1, }, delete: DriveDocumentDeleteMethodVersions { - // v1 at protocol v14: a keep-history primary layer holds current - // pointers, not document items, so its layer sizes come from the - // reference shape. - add_estimation_costs_for_remove_document_to_primary_storage: 1, - delete_document_for_contract: 1, - delete_document_for_contract_id: 1, - delete_document_for_contract_apply_and_add_to_operations: 1, - // v1 at protocol v14: the stateless delete of a keep-history - // primary entry is sized as a reference. - remove_document_from_primary_storage: 1, + add_estimation_costs_for_remove_document_to_primary_storage: 0, + delete_document_for_contract: 0, + delete_document_for_contract_id: 0, + delete_document_for_contract_apply_and_add_to_operations: 0, + remove_document_from_primary_storage: 0, // v1 at protocol v14: the empty-tree pruning climb stops at the // member level on `preallocated` indexOnly indexes, keeping the // trees the referenced document's insert paid for. remove_reference_for_index_level_for_contract_operations: 1, remove_indices_for_index_level_for_contract_operations: 2, remove_indices_for_top_index_level_for_contract_operations: 2, - delete_document_for_contract_id_with_named_type_operations: 1, - delete_document_for_contract_with_named_type_operations: 1, - // v1 at protocol v14: keep-history documents lose their current - // pointer and index references and gain a lifecycle record; the - // non-history path is delegated to v0 unchanged. - delete_document_for_contract_operations: 1, + delete_document_for_contract_id_with_named_type_operations: 0, + delete_document_for_contract_with_named_type_operations: 0, + delete_document_for_contract_operations: 0, delete_index_only_document_for_contract_operations: 0, delete_index_only_document_for_contract: 0, - erase_document_for_contract_operations: Some(0), - add_estimation_costs_for_erase_document: Some(0), + erase_document_for_contract_operations: None, + add_estimation_costs_for_erase_document: None, }, 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 index faec462bd69..fdc952ce265 100644 --- 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 @@ -5,7 +5,14 @@ use super::{ DriveDocumentQueryMethodVersions, DriveDocumentUpdateMethodVersions, }; -/// Protocol v15 document methods for per-type keep-history storage. +/// Protocol v15 document methods for per-type keep-history storage and the +/// delete and erase lifecycle of keep-history documents. +/// +/// A delete of a keep-history document removes its current pointer and index +/// references and records a lifecycle entry while the retained revisions stay +/// readable; an erase removes those revisions a chunk at a time. The delete +/// wrappers, the primary-storage removal estimate and the lifecycle read are +/// the generations that know that layout. pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = DriveDocumentMethodVersions { query: DriveDocumentQueryMethodVersions { @@ -13,10 +20,21 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = fetch_document_history: 1, prove_document_history: 1, primary_key_path_query: 1, + fetch_document_lifecycle: Some(0), ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.query }, delete: DriveDocumentDeleteMethodVersions { remove_reference_for_index_level_for_contract_operations: 2, + add_estimation_costs_for_remove_document_to_primary_storage: 1, + delete_document_for_contract: 1, + delete_document_for_contract_id: 1, + delete_document_for_contract_apply_and_add_to_operations: 1, + remove_document_from_primary_storage: 1, + delete_document_for_contract_id_with_named_type_operations: 1, + delete_document_for_contract_with_named_type_operations: 1, + delete_document_for_contract_operations: 1, + erase_document_for_contract_operations: Some(0), + add_estimation_costs_for_erase_document: Some(0), ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.delete }, insert: DriveDocumentInsertMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index 0b8e86a7774..ecb63864207 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -2,6 +2,7 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; use crate::version::drive_versions::DriveDataContractOperationMethodVersions; use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; @@ -32,7 +33,7 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub document_create_transition: FeatureVersion, pub document_delete_transition: FeatureVersion, pub document_index_only_delete_transition: FeatureVersion, - /// The erase kind, absent before protocol version 14. + /// The erase kind, absent before protocol version 15. pub document_erase_transition: OptionalFeatureVersion, pub document_purchase_transition: FeatureVersion, pub document_replace_transition: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index 10f7ce05c4a..547d6d450d7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -19,9 +19,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV data_contract_create_transition: 0, data_contract_update_transition: 0, document_create_transition: 0, - document_delete_transition: 1, + document_delete_transition: 0, document_index_only_delete_transition: 0, - document_erase_transition: Some(0), + document_erase_transition: None, // PROTOCOL_VERSION_13: v1 rewrites a transferred or purchased // DPNS domain document's `records.identity` to the new owner // so the username resolves to the buyer. v0 stays for @@ -33,9 +33,7 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV token_burn_transition: 0, token_mint_transition: 0, token_transfer_transition: 0, - // Generation 1 consumes batch action format 1, whose items - // may include the erase of a keep-history document. - documents_batch_transition: 1, + documents_batch_transition: 0, identity_create_transition: 0, identity_create_from_addresses_transition: 0, identity_credit_transfer_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs new file mode 100644 index 00000000000..40ec620cd05 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs @@ -0,0 +1,25 @@ +use crate::version::drive_versions::drive_state_transition_method_versions::v4::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_state_transition_method_versions::{ + DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions, + DriveStateTransitionMethodVersions, +}; + +/// Protocol v15 state transition methods for the keep-history document +/// lifecycle. +/// +/// Delete conversion generation 1 records a lifecycle entry for a keep-history +/// document instead of removing its retained revisions; erase conversion +/// removes them a chunk at a time; batch conversion generation 1 consumes +/// batch action format 1, whose items may include an erase, and emits the +/// operations of every item. +pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5: DriveStateTransitionMethodVersions = + DriveStateTransitionMethodVersions { + convert_to_high_level_operations: + DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions { + document_delete_transition: 1, + document_erase_transition: Some(0), + documents_batch_transition: 1, + ..DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4.convert_to_high_level_operations + }, + ..DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4 + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs index 34de2e862db..d622addb544 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs @@ -5,7 +5,8 @@ use super::{ }; /// Verification of composite document history and its independent metadata proof, -/// and the cursor lookup of a proved page in the protocol 15 keep-history layout. +/// the cursor lookup of a proved page in the protocol 15 keep-history layout, and +/// state transition execution proofs for batch wire format 2. pub const DRIVE_VERIFY_METHOD_VERSIONS_V3: DriveVerifyMethodVersions = DriveVerifyMethodVersions { document: DriveVerifyDocumentMethodVersions { verify_document_history: 1, 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 9784199401f..21f8c074d5b 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,18 +1,28 @@ use super::drive_contract_method_versions::v5::DRIVE_CONTRACT_METHOD_VERSIONS_V5; use super::drive_document_method_versions::v5::DRIVE_DOCUMENT_METHOD_VERSIONS_V5; +use super::drive_state_transition_method_versions::v5::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5; use super::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; use super::v9::DRIVE_VERSION_V9; -use super::{DriveMethodVersions, DriveVersion}; +use super::{DriveMethodVersions, DriveProveMethodVersions, DriveVersion}; /// Drive version 10, introduced in protocol v15. /// /// Moves keep-history documents to per-type history trees and selects the /// matching contract writers, document operations, queries, and proof verifier. +/// It also selects the delete and erase lifecycle of keep-history documents: +/// the delete wrappers and action conversion that record and consume a +/// lifecycle entry, the batch conversion that knows action format 1, and the +/// state transition prover and verifier that know batch wire format 2. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { methods: DriveMethodVersions { document: DRIVE_DOCUMENT_METHOD_VERSIONS_V5, contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, verify: DRIVE_VERIFY_METHOD_VERSIONS_V3, + state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5, + prove: DriveProveMethodVersions { + prove_state_transition: 1, + ..DRIVE_VERSION_V9.methods.prove + }, ..DRIVE_VERSION_V9.methods }, ..DRIVE_VERSION_V9 diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index 9da17e7e4e8..fdb7aa5aaca 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -63,9 +63,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { prove: DriveProveMethodVersions { prove_elements: 0, prove_multiple_state_transition_results: 0, - // Generation 1 proves a batch of any wire format, including - // format 2, whose document shell carries the erase kind. - prove_state_transition: 1, + prove_state_transition: 0, }, balances: DriveBalancesMethodVersions { add_to_system_credits: 0, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 94ad6f57b47..7d75630d4fa 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -2,6 +2,7 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; #[derive(Clone, Debug, Default)] pub struct SystemLimits { diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 3729cac5c56..5f4afc741af 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -66,5 +66,5 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_time_range_ttl_seconds: Some(604_800), // one week min_time_range_ttl_drop_operations_per_write: Some(32), minimum_grovedb_proof_envelope_version: 1, // clients reject legacy V0 GroveDB proof envelopes from v14 - max_document_revisions_erased_per_transition: Some(100), + max_document_revisions_erased_per_transition: None, }; diff --git a/packages/rs-platform-version/src/version/system_limits/v5.rs b/packages/rs-platform-version/src/version/system_limits/v5.rs new file mode 100644 index 00000000000..f8f0350f723 --- /dev/null +++ b/packages/rs-platform-version/src/version/system_limits/v5.rs @@ -0,0 +1,11 @@ +use crate::version::system_limits::SystemLimits; + +/// System limits for protocol version 15 and above. Relative to V4 this adds +/// the erase chunk: `max_document_revisions_erased_per_transition` bounds how +/// many retained revisions of a deleted keep-history document one erase +/// transition removes, so the work of erasing a long history is spread over +/// several transitions of bounded cost. +pub const SYSTEM_LIMITS_V5: SystemLimits = SystemLimits { + max_document_revisions_erased_per_transition: Some(100), + ..super::v4::SYSTEM_LIMITS_V4 +}; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 273962ac0df..a321869ce94 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -9,7 +9,7 @@ use crate::version::dpp_versions::dpp_method_versions::v3::DPP_METHOD_VERSIONS_V use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2; use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; +use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; @@ -298,7 +298,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, - state_transitions: STATE_TRANSITION_VERSIONS_V4, + state_transitions: STATE_TRANSITION_VERSIONS_V3, contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords; validate_structure_interval v1 rejects a zero epoch interval document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties identity_versions: IDENTITY_VERSIONS_V1, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 787228facc9..874bbfb0da4 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -1,25 +1,66 @@ +use crate::version::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v4::STATE_TRANSITION_SERIALIZATION_VERSIONS_V4; +use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; +use crate::version::dpp_versions::dpp_validation_versions::v6::DPP_VALIDATION_VERSIONS_V6; +use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; +use crate::version::drive_abci_versions::drive_abci_query_versions::v4::DRIVE_ABCI_QUERY_VERSIONS_V4; +use crate::version::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; use crate::version::drive_abci_versions::DriveAbciVersion; use crate::version::drive_versions::v10::DRIVE_VERSION_V10; use crate::version::protocol_version::PlatformVersion; +use crate::version::system_limits::v5::SYSTEM_LIMITS_V5; use crate::version::v14::PLATFORM_V14; use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; -/// Protocol v15 moves keep-history documents to per-type history trees. +/// Protocol v15 moves keep-history documents to per-type history trees and +/// gives them a delete and erase lifecycle. /// -/// The primary-key entry stores the current document while retained revisions -/// live in a separate provable count tree keyed by block time and revision. -/// The first v15 block migrates existing v14 histories and index references -/// before v15 state transitions execute. History queries and proofs select the -/// matching layout through Drive's version tables. +/// 1. **Per-type history trees.** The primary-key entry stores the current +/// document while retained revisions live in a separate provable count tree +/// keyed by block time and revision. The first v15 block migrates existing +/// v14 histories and index references before v15 state transitions execute. +/// History queries and proofs select the matching layout through Drive's +/// version tables. +/// 2. **Keep-history delete.** A keep-history document type may now allow +/// deletion. A delete removes the document's current pointer and index +/// references and records a lifecycle entry; the retained revisions stay +/// readable through the history query, which reports the deleted state and +/// its time. Creating a document under a deleted id is refused until its +/// history is gone. +/// 3. **Erase.** A document type that keeps history and can be deleted may +/// declare `canBeErased` (meta-schema v4, parser generation 4; the keyword +/// is immutable across contract updates). An erase transition removes the +/// retained revisions of a deleted document a chunk of +/// `max_document_revisions_erased_per_transition` at a time: the owner +/// authorizes the first chunk, any identity may submit a continuation, and +/// the last chunk removes the lifecycle entry, after which the id reads as +/// absent. The history query reports the erasing state, its start time and +/// the remaining revision count. +/// 4. **Batch transition format 2.** The erase kind is carried by a new batch +/// wire format whose document transition shell knows it; formats 0 and 1 +/// keep their shells and cannot carry an erase. Format 2 becomes the +/// default the SDKs emit. Inside Drive ABCI a batch becomes action format 1, +/// and the generations that transform, validate, convert, prove and verify +/// a batch were bumped to read every wire format. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: document v5 and contract v5 write the per-type history tree; verify v3 authenticates history pages and lifecycle counts + drive: DRIVE_VERSION_V10, // changed: document v5 and contract v5 write the per-type history tree and the delete/erase lifecycle; verify v3 authenticates history pages, lifecycle counts and format 2 execution proofs; state transition methods v5 convert the lifecycle and batch action format 1; prover v1 proves format 2 drive_abci: DriveAbciVersion { methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the protocol-change hook v2 migrates retained histories on the first v15 block + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: batch generations read wire format 2 and action format 1; delete consults the lifecycle; erase structure and state validation + query: DRIVE_ABCI_QUERY_VERSIONS_V4, // changed: the history handler reports the deleted and erasing lifecycle states with their times ..PLATFORM_V14.drive_abci }, + dpp: DPPVersion { + validation: DPP_VALIDATION_VERSIONS_V6, // changed: document type update validation v2 keeps `canBeErased` immutable + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V4, // changed: batch transition format 2 and the erase kind join the wire + state_transitions: STATE_TRANSITION_VERSIONS_V4, // changed: batch basic structure v1 reads every wire format + contract_versions: CONTRACT_VERSIONS_V7, // changed: v4 document meta-schema and parser generation 4 admit deletable keep-history types and `canBeErased` + ..PLATFORM_V14.dpp + }, + system_limits: SYSTEM_LIMITS_V5, // changed: erase chunk of 100 revisions per transition ..PLATFORM_V14 }; diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index 736c490eca8..c0de8d7a4ce 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -7,13 +7,14 @@ use platform_version::version::PlatformVersion; -/// The released protocol versions parse contracts with the generation that +/// The released protocol versions parse contracts with a generation that /// predates the lifecycle grammar and refuse a keep-history delete before it /// reaches storage. Both must replay identically forever. #[test] fn should_preserve_released_keep_history_validation_versions() { - for protocol in [12, 13] { + for protocol in [12, 13, 14] { let version = PlatformVersion::get(protocol).unwrap(); + let released_parser_generation = if protocol == 14 { 3 } else { 2 }; assert_eq!( version .dpp @@ -21,9 +22,13 @@ fn should_preserve_released_keep_history_validation_versions() { .document_type_versions .class_method_versions .try_from_schema, - 2, + released_parser_generation, "parser at protocol {protocol}" ); + // Protocol 14 refuses a delete of a keep-history document as a paid + // consensus error instead of the earlier internal error; neither + // generation carries one out. + let released_delete_structure_generation = if protocol == 14 { 1 } else { 0 }; assert_eq!( version .drive_abci @@ -31,7 +36,7 @@ fn should_preserve_released_keep_history_validation_versions() { .state_transitions .batch_state_transition .document_delete_transition_structure_validation, - 0, + released_delete_structure_generation, "delete structure validation at protocol {protocol}" ); assert_eq!( @@ -122,12 +127,12 @@ fn should_preserve_released_keep_history_validation_versions() { } } -/// Protocol 14 selects the lifecycle: the parser generation that admits the new +/// Protocol 15 selects the lifecycle: the parser generation that admits the new /// keywords, the delete that consults the lifecycle, and the erase kind with /// the bound on how much one transition may remove. #[test] -fn should_activate_keep_history_validation_at_protocol_14() { - let version = PlatformVersion::get(14).unwrap(); +fn should_activate_keep_history_validation_at_protocol_15() { + let version = PlatformVersion::get(15).unwrap(); assert_eq!( version .dpp @@ -135,7 +140,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .document_type_versions .class_method_versions .try_from_schema, - 3 + 4 ); assert_eq!( version @@ -144,7 +149,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .state_transitions .batch_state_transition .document_delete_transition_structure_validation, - 1 + 2 ); assert_eq!( version @@ -155,6 +160,16 @@ fn should_activate_keep_history_validation_at_protocol_14() { .document_delete_transition_state_validation, 1 ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .document_create_transition_state_validation, + 3, + "protocol 15 refuses to create over a deleted or erasing keep-history document" + ); assert_eq!( version .drive @@ -163,7 +178,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .delete .delete_document_for_contract_operations, 1, - "protocol 14 selects the keep-history delete branch" + "protocol 15 selects the keep-history delete branch" ); assert_eq!( version @@ -172,7 +187,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .batch_state_transition .max_version, 2, - "protocol 14 admits BatchTransitionV2" + "protocol 15 admits BatchTransitionV2" ); assert_eq!( version @@ -181,7 +196,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .batch_state_transition .default_current_version, 2, - "protocol 14 constructs BatchTransitionV2" + "protocol 15 constructs BatchTransitionV2" ); assert_eq!( version @@ -192,7 +207,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .validation .validate_base_structure, 1, - "protocol 14 selects structure validation that admits erase only in V2" + "protocol 15 selects structure validation that admits erase only in V2" ); assert_eq!( version @@ -202,7 +217,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .convert_to_high_level_operations .document_delete_transition, 1, - "protocol 14 selects the lifecycle-specific delete operation" + "protocol 15 selects the lifecycle-specific delete operation" ); assert_eq!( version @@ -212,7 +227,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .state_transition .verify_state_transition_was_executed_with_proof, 1, - "protocol 14 selects erase-aware proof verification" + "protocol 15 selects erase-aware proof verification" ); assert_eq!( version @@ -222,7 +237,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .batch_state_transition .transform_into_action, 2, - "protocol 14 selects the erase-aware transformer" + "protocol 15 selects the erase-aware transformer" ); assert_eq!( version @@ -232,7 +247,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .batch_state_transition .advanced_structure, 1, - "protocol 14 selects erase-aware advanced validation" + "protocol 15 selects erase-aware advanced validation" ); assert_eq!( version @@ -242,7 +257,18 @@ fn should_activate_keep_history_validation_at_protocol_14() { .batch_state_transition .state, 1, - "protocol 14 selects erase-aware state validation" + "protocol 15 selects erase-aware state validation" + ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_documents_for_transitions_knowing_contract_and_document_type, + 1, + "the fetch through the shell of wire formats 0 and 1 is unchanged; the \ + shell of any format has its own slot" ); let bounds = version @@ -250,7 +276,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { .state_transition_serialization_versions .document_erase_state_transition .as_ref() - .expect("the erase kind joins the wire at protocol 14"); + .expect("the erase kind joins the wire at protocol 15"); assert_eq!(bounds.bounds.min_version, 0); assert_eq!(bounds.bounds.max_version, 0); assert_eq!(bounds.bounds.default_current_version, 0); @@ -258,7 +284,7 @@ fn should_activate_keep_history_validation_at_protocol_14() { let chunk = version .system_limits .max_document_revisions_erased_per_transition - .expect("protocol 14 bounds the erase chunk"); + .expect("protocol 15 bounds the erase chunk"); assert_eq!( chunk, 100, "the chunk bounds the work one erase can demand and the balance every \ @@ -274,7 +300,7 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { let Ok(version) = PlatformVersion::get(protocol) else { continue; }; - let expected_erase_version = (protocol >= 14).then_some(0); + let expected_erase_version = (protocol >= 15).then_some(0); assert_eq!( version .drive @@ -345,5 +371,15 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { expected_erase_version, "lifecycle state read at protocol {protocol}" ); + assert_eq!( + version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type, + expected_erase_version, + "document fetch through the shell of any wire format at protocol {protocol}" + ); } } diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index e8f8955bf0f..2a4b6619eda 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -360,7 +360,7 @@ mod document_history_mock_tests { #[test] fn should_round_trip_every_lifecycle_state_with_its_times() { let mut sdk = crate::SdkBuilder::default() - .with_version(dpp::version::PlatformVersion::get(14).unwrap()) + .with_version(dpp::version::PlatformVersion::get(15).unwrap()) .build() .unwrap(); let mock = sdk.mock(); diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index 4dbc6130810..102fff3f9e7 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -167,7 +167,7 @@ impl DocumentEraseTransitionBuilder { /// Refuses, before any nonce is reserved, an erase the platform version /// cannot construct: the transition kind joined the wire at protocol - /// version 14, and the batch may only be built at a version it knows. + /// version 15, and the batch may only be built at a version it knows. /// /// The same rejection happens inside the transition constructor, but by /// then the SDK has already advanced its cached contract nonce for a @@ -446,15 +446,15 @@ mod tests { /// that cannot construct an erase is refused without reserving a nonce. #[test] fn should_refuse_an_erase_the_platform_version_cannot_construct() { - let too_old = PlatformVersion::get(13).unwrap(); + let too_old = PlatformVersion::get(14).unwrap(); let error = DocumentEraseTransitionBuilder::check_erase_is_constructible(None, too_old) - .expect_err("protocol 13 has no erase transition"); + .expect_err("protocol 14 has no erase transition"); assert!(error .to_string() .contains("erase transitions do not exist at this platform version")); let current = PlatformVersion::latest(); DocumentEraseTransitionBuilder::check_erase_is_constructible(None, current) - .expect("protocol 14 constructs erases"); + .expect("protocol 15 constructs erases"); for unsupported in [0, 1] { let options = StateTransitionCreationOptions { batch_feature_version: Some(unsupported), diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs index 803ace4dab0..18b4d6b736a 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs @@ -119,7 +119,7 @@ impl BatchTransitionWasm { let owner_id = owner_id.try_into()?; // Batch format 1 carries every transition but an erase; an erase needs - // format 2, which only protocol version 14 and later accept. + // format 2, which only protocol version 15 and later accept. let batch = match downcast_batched_transitions(transitions) { Ok(transitions) => BatchTransition::V1(BatchTransitionV1 { owner_id, From a537d257be86fbf5f62b51a92b430a248d7cc874 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 18 Sep 2026 22:19:18 +0700 Subject: [PATCH 35/48] fix(drive): refuse a keep-history delete through every entry point that lacks the block Three delete entry points that carry only the block time still reached the lifecycle generation with a fabricated block and no deleter, so a keep-history delete through them would have recorded a lifecycle entry at height 0. They now refuse such a type like `delete_document_for_contract_operations` already did, and the fee-applying wrappers reach the lifecycle generation through new `_with_lifecycle` dispatchers instead of calling a sibling directory's generation body directly, so every slot governs its own path again. The id-based named-type dispatcher loses a generation 1 nothing called; its generation 0 delegates to the guarded dispatcher. Basic structure v1 regains the per-kind version-bounds gate that generation 0 applies to the indexOnly delete kind, and applies it to the erase kind as well: software that knows a newer generation of a kind decodes it whatever protocol version is active, and this check is where it agrees with software that does not. Also: the v4 meta-schema freeze notice describes v4 and protocol 15 instead of copying v3's; generation-4 tests pin the latest platform version instead of the literal 15 (boundary tests keep their numbers); the erasability test asserts the immutability message rather than any invalid result; `STATE_TRANSITION_VERSIONS_V4` spells its validation table as a delta; two comments that named only the generation-3 driver name its successors too. Tests: `should_refuse_a_keep_history_delete_through_entry_points_without_a_block` fails on the previous dispatchers (the named-type entry point deleted the document) and passes now; `should_gate_an_erase_on_the_bounds_the_protocol_version_publishes` pins the regained gate. Co-Authored-By: Claude Opus 5 (1M context) --- .../document/v4/document-meta.json | 2 +- .../try_from_schema/common/mod.rs | 6 +- .../try_from_schema/v4/keep_history_tests.rs | 77 ++++----- .../validate_basic_structure/v1/mod.rs | 113 +++++++++++++ .../batch/tests/document/erase.rs | 4 +- .../tests/document/lifecycle_contracts.rs | 12 +- .../delete_document_for_contract/v1/mod.rs | 2 +- .../mod.rs | 75 ++++++++- .../v1/mod.rs | 27 ++-- .../delete_document_for_contract_id/v1/mod.rs | 2 +- .../mod.rs | 15 +- .../v1/mod.rs | 66 -------- .../v1/mod.rs | 2 +- .../mod.rs | 74 ++++++++- .../v1/mod.rs | 2 +- .../rs-drive/src/drive/document/delete/mod.rs | 153 ++++++++++++++++++ .../dpp_state_transition_versions/v4.rs | 5 +- .../drive_document_method_versions/v5.rs | 1 - packages/rs-sdk/src/mock/requests.rs | 2 +- 19 files changed, 490 insertions(+), 150 deletions(-) delete mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs diff --git a/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json b/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json index d3cdbd4a265..89538910301 100644 --- a/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json +++ b/packages/rs-dpp/schema/meta_schemas/document/v4/document-meta.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/dashpay/platform/blob/master/packages/rs-dpp/schema/meta_schemas/document/v1/document-meta.json", - "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V14 SHIPS — FROZEN AFTER. This v3 document meta-schema activates with protocol v14 (CONTRACT_VERSIONS_V6). It is v2 plus the ranked index keywords (rankedCountable, rankedSummable, rankedAverageable), the refersTo reference keyword on identifier properties, the requiredSince property keyword (the contract version a property is required from), and the timeRange index transform, and admits every v14+ contract written to disk. v2 stays in place for protocol v13, where those keys still fail an index entry's `additionalProperties: false`. Once the release carrying protocol v14 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v4+). The $id above deliberately still names the v1 path: v1, v2 and v3 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", + "$comment": "EDITABLE UNTIL THE RELEASE CARRYING PROTOCOL V15 SHIPS — FROZEN AFTER. This v4 document meta-schema activates with protocol v15 (CONTRACT_VERSIONS_V7). It is v3 plus the doctype-level canBeErased keyword, which lets a keep-history document type that allows deletion also allow the erasure of a deleted document's retained revisions, and admits every v15+ contract written to disk. v3 stays in place for protocol v14, where canBeErased still fails the document type's `additionalProperties: false`. Once the release carrying protocol v15 ships, mutating it would change historical validation results and break consensus replay. After release, any new top-level property or rule MUST go in a newer meta-schema version (v5+). The $id above deliberately still names the v1 path: v1, v2, v3 and v4 all share that identity, and it is the exact string `enrich_with_base_schema` injects as every PV12+ document schema's `$schema`, so bumping it here would be a wire-visible change rather than a documentation fix.", "type": "object", "$defs": { "documentProperties": { diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs index 49e4f2b7fca..f6e532c6823 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs @@ -206,8 +206,8 @@ pub(super) struct ParserGeneration { /// document types). Forwarded to [`Index::try_from_value_map`] exactly /// like `admit_ranked` and `admit_time_range`. The doc-type-level /// `indexOnly` keyword needs no admission flag of its own: it is read - /// only by the generation-3 driver (`parse_index_only_keyword`), so - /// earlier generations ignore it exactly as they ignore every other + /// only by the generation-3 and later drivers (`parse_index_only_keyword`), + /// so earlier generations ignore it exactly as they ignore every other /// doctype-level keyword they predate. pub admit_index_terminal: bool, /// Whether the index grammar admits the `preallocated` keyword @@ -2054,7 +2054,7 @@ pub(super) fn reject_contested_keep_history( /// /// Runs before the core parse because the core takes `schema` by value — /// same shape as [`parse_doctype_aggregate_keywords`]. Only the generation-3 -/// driver calls this; earlier generations ignore the keyword exactly as they +/// and later drivers call this; earlier generations ignore the keyword exactly as they /// ignore every doctype-level keyword they predate (their meta-schemas still /// reject it under `full_validation`). pub(super) fn parse_index_only_keyword(schema: &Value) -> Result { diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs index fc65f129100..5f637d54b5c 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v4/keep_history_tests.rs @@ -36,7 +36,11 @@ fn parse_at_version( } fn parse(schema: Value) -> Result { - parse_at_version(schema, 15, true) + parse_at_version(schema, PlatformVersion::latest().protocol_version, true) +} + +fn parse_without_validation(schema: Value) -> Result { + parse_at_version(schema, PlatformVersion::latest().protocol_version, false) } fn keep_history_deletable_schema() -> Value { @@ -62,7 +66,7 @@ fn keep_history_deletable_schema() -> Value { #[test] fn should_accept_a_keep_history_type_that_allows_deletion() { let document_type = parse(keep_history_deletable_schema()) - .expect("a keep-history type may allow deletion from protocol version 15"); + .expect("a keep-history type may allow deletion at the latest protocol version"); assert!(document_type.documents_keep_history()); assert!(document_type.documents_can_be_deleted()); assert!( @@ -282,7 +286,7 @@ fn should_still_load_a_legacy_contested_keep_history_type_without_validation() { "documentsKeepHistory": true, "canBeDeleted": false, }); - let document_type = parse_at_version(schema, 15, false) + let document_type = parse_without_validation(schema) .expect("a stored contract with this combination must still load"); assert!(document_type.documents_keep_history()); } @@ -328,12 +332,12 @@ fn repair_schema(keep_history: bool, can_be_deleted: bool) -> Value { /// A keep-history type may withdraw deletion, and only in that direction. #[test] -fn should_allow_a_keep_history_type_to_withdraw_deletion_at_protocol_15() { +fn should_allow_a_keep_history_type_to_withdraw_deletion() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); - let new = parse_at_version(repair_schema(true, false), 15, true).unwrap(); + let new = parse(repair_schema(true, false)).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .expect("the update must reach a consensus result"); assert!(result.is_valid(), "rejected: {:?}", result.errors); } @@ -355,7 +359,7 @@ fn should_preserve_the_immutable_delete_flag_through_protocol_13() { } #[test] -fn should_reject_other_delete_and_history_flag_changes_at_protocol_15() { +fn should_reject_other_delete_and_history_flag_changes() { for (old_flags, new_flags) in [ ((false, true), (false, false)), ((false, false), (false, true)), @@ -366,10 +370,10 @@ fn should_reject_other_delete_and_history_flag_changes_at_protocol_15() { let old = parse_at_version(repair_schema(old_flags.0, old_flags.1), 13, true).unwrap(); // A caller may already have a parsed contract; update validation must // enforce immutability even without the full-validation parser guard. - let new = parse_at_version(repair_schema(new_flags.0, new_flags.1), 15, false).unwrap(); + let new = parse_without_validation(repair_schema(new_flags.0, new_flags.1)).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); assert!( !result.is_valid(), @@ -384,15 +388,20 @@ fn should_reject_other_delete_and_history_flag_changes_at_protocol_15() { #[test] fn should_reject_every_change_to_the_erasure_flag() { for (before, after) in [(false, true), (true, false)] { - let old = parse_at_version(erasable_schema(true, true, before), 15, false).unwrap(); - let new = parse_at_version(erasable_schema(true, true, after), 15, false).unwrap(); + let old = parse_without_validation(erasable_schema(true, true, before)).unwrap(); + let new = parse_without_validation(erasable_schema(true, true, after)).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); + let message = result + .errors + .first() + .unwrap_or_else(|| panic!("unexpectedly accepted canBeErased {before} -> {after}")) + .to_string(); assert!( - !result.is_valid(), - "unexpectedly accepted canBeErased {before} -> {after}" + message.contains("can not change whether its documents can be erased"), + "canBeErased {before} -> {after} must be refused for being immutable, got {message}" ); } } @@ -405,15 +414,15 @@ fn should_reject_every_change_to_the_erasure_flag() { #[test] fn should_refuse_to_withdraw_deletion_from_an_erasable_type() { assert!( - parse_at_version(erasable_schema(true, false, true), 15, false).is_err(), + parse_without_validation(erasable_schema(true, false, true)).is_err(), "an erasable type that forbids deletion is not a valid type at all" ); - let old = parse_at_version(erasable_schema(true, true, true), 15, false).unwrap(); - let new = parse_at_version(repair_schema(true, false), 15, false).unwrap(); + let old = parse_without_validation(erasable_schema(true, true, true)).unwrap(); + let new = parse_without_validation(repair_schema(true, false)).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); assert!(!result.is_valid(), "an erasable type kept its delete flag"); } @@ -421,23 +430,19 @@ fn should_refuse_to_withdraw_deletion_from_an_erasable_type() { #[test] fn should_reject_incompatible_properties_during_a_delete_flag_withdrawal() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); - let new = parse_at_version( - platform_value!({ - "type": "object", - "properties": { - "label": {"type": "integer", "position": 0}, - }, - "additionalProperties": false, - "documentsKeepHistory": true, - "canBeDeleted": false, - }), - 15, - true, - ) + let new = parse(platform_value!({ + "type": "object", + "properties": { + "label": {"type": "integer", "position": 0}, + }, + "additionalProperties": false, + "documentsKeepHistory": true, + "canBeDeleted": false, + })) .unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); assert!( !result.is_valid(), @@ -450,10 +455,10 @@ fn should_reject_mutability_change_during_a_delete_flag_withdrawal() { let old = parse_at_version(repair_schema(true, true), 13, true).unwrap(); let mut schema = repair_schema(true, false); schema.set_value("documentsMutable", false.into()).unwrap(); - let new = parse_at_version(schema, 15, true).unwrap(); + let new = parse(schema).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); assert!( !result.is_valid(), @@ -483,10 +488,10 @@ fn should_still_validate_a_property_named_can_be_deleted_during_a_withdrawal() { ) .unwrap(); let old = parse_at_version(old_schema, 13, true).unwrap(); - let new = parse_at_version(new_schema, 15, true).unwrap(); + let new = parse(new_schema).unwrap(); let result = old .as_ref() - .validate_update(new.as_ref(), 2, PlatformVersion::get(15).unwrap()) + .validate_update(new.as_ref(), 2, PlatformVersion::latest()) .unwrap(); assert!( !result.is_valid(), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs index f88846eaed5..c52ec1b29ec 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs @@ -2,7 +2,11 @@ use crate::consensus::basic::document::{ DocumentTransitionsAreAbsentError, DuplicateDocumentTransitionsWithIdsError, MaxDocumentsTransitionsExceededError, NonceOutOfBoundsError, }; +use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; use crate::consensus::basic::BasicError; +use crate::state_transition::batch_transition::batched_transition::{ + DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, +}; use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; @@ -11,6 +15,7 @@ use crate::state_transition::batch_transition::BatchTransition; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; use platform_value::Identifier; +use platform_version::version::dpp_versions::dpp_state_transition_serialization_versions::DocumentFeatureVersionBounds; use platform_version::version::PlatformVersion; use std::collections::btree_map::Entry; use std::collections::BTreeMap; @@ -104,6 +109,43 @@ impl BatchTransition { NonceOutOfBoundsError::new(transition.identity_contract_nonce()), )); } + + // The kinds that joined the wire after the first batch formats + // are gated on the version bounds the active protocol version + // publishes for them. Software that knows a newer generation of + // a kind decodes it whatever the active protocol version, so + // this is where it agrees with software that does not. + match transition { + DocumentTransitionRefV1::IndexOnlyDelete(index_only_delete) => { + let feature_version = match index_only_delete { + DocumentIndexOnlyDeleteTransition::V0(_) => 0, + }; + check_kind_version_bounds( + feature_version, + platform_version + .dpp + .state_transition_serialization_versions + .document_index_only_delete_state_transition + .as_ref(), + &mut result, + ); + } + DocumentTransitionRefV1::Erase(erase) => { + let feature_version = match erase { + DocumentEraseTransition::V0(_) => 0, + }; + check_kind_version_bounds( + feature_version, + platform_version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .as_ref(), + &mut result, + ); + } + _ => {} + } } // Make sure we don't have duplicate transitions @@ -242,10 +284,39 @@ impl<'a> From> for TransitionFingerprint<'a> { } } +/// Records an error when the active protocol version does not publish +/// `feature_version` of a document transition kind: an absent entry means the +/// kind does not exist at all at that version, which the empty range (min 1, +/// max 0) states. +fn check_kind_version_bounds( + feature_version: u16, + bounds: Option<&DocumentFeatureVersionBounds>, + result: &mut SimpleConsensusValidationResult, +) { + match bounds { + None => result.add_error(BasicError::UnsupportedVersionError( + UnsupportedVersionError::new(feature_version, 1, 0), + )), + Some(bounds) if !bounds.bounds.check_version(feature_version) => result.add_error( + BasicError::UnsupportedVersionError(UnsupportedVersionError::new( + feature_version, + bounds.bounds.min_version, + bounds.bounds.max_version, + )), + ), + Some(_) => {} + } +} + /// Finds the duplicates among the document transitions of one contract. Two /// transitions are duplicates when they name the same document type and /// document id, whatever their kinds. Every duplicated transition is reported, /// the first one once per later duplicate. +/// +/// The shared `find_duplicates_by_id` helper and its version slot take the +/// shell of batch formats 0 and 1, which cannot hold an erase, so this +/// generation carries its own finder over the shell of every format. A later +/// generation that needs a different rule forks this module. fn find_duplicates_by_id<'a>( document_transitions: &[DocumentTransitionRefV1<'a>], ) -> Vec> { @@ -423,6 +494,48 @@ mod tests { )); } + /// Software that knows the erase kind decodes it whatever protocol version + /// is active, so the structure check has to refuse the kind wherever the + /// active version publishes no bounds for it; that is how such software + /// agrees with software that cannot decode it at all. + #[test] + fn should_gate_an_erase_on_the_bounds_the_protocol_version_publishes() { + let batch = make_batch_v2(vec![make_erase(1, 1)]); + let is_unsupported_version = |error: &ConsensusError| { + matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + ) + }; + + let without_erase = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert!( + without_erase + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .is_none(), + "the test needs a version that publishes no erase bounds" + ); + let result = batch + .validate_base_structure_v1(without_erase) + .expect("no protocol err"); + assert!( + result.errors.iter().any(is_unsupported_version), + "a version without erase bounds must refuse the kind, got {:?}", + result.errors + ); + + let result = batch + .validate_base_structure_v1(PlatformVersion::latest()) + .expect("no protocol err"); + assert!( + !result.errors.iter().any(is_unsupported_version), + "the latest version publishes erase bounds, got {:?}", + result.errors + ); + } + /// A format 2 batch never reaches the released structure generation in /// production, because decoding rejects it by wire version first. Should /// one arrive anyway, the shell that generation reads through cannot see diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 5bba4af4f67..89ad23eedda 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -92,12 +92,12 @@ impl Fixture { async fn new(document_type_name: &str) -> Self { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(15) + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_initial_state_structure(); let contract = json_document_to_contract(ERASABLE_CONTRACT, true, platform_version) - .expect("the erasable note contract must pass full validation at protocol 15"); + .expect("the erasable note contract must pass full validation"); platform .drive .apply_contract( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs index da96f4be488..22636c59201 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lifecycle_contracts.rs @@ -87,15 +87,15 @@ fn contested_keep_history() -> Value { async fn refused_create( schema: Value, ) -> (TempPlatform, Identifier, IdentityNonce) { - let platform_version = PlatformVersion::get(15).expect("protocol 15 exists"); + let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(15) + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); let platform_state = platform.state.load(); let (identity, signer, key) = setup_identity(&mut platform, 4001, dash_to_credits!(10.0)); - let contract = DataContractFactory::new(15) + let contract = DataContractFactory::new(platform_version.protocol_version) .expect("expected a contract factory") .create_with_value_config( identity.id(), @@ -195,15 +195,15 @@ async fn should_charge_for_a_signed_contested_keep_history_contract() { /// The same refusal on the update path, which has its own error-category split. #[tokio::test] async fn should_charge_for_a_signed_contract_update_asking_for_erasure_without_history() { - let platform_version = PlatformVersion::get(15).expect("protocol 15 exists"); + let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() - .with_initial_protocol_version(15) + .with_initial_protocol_version(platform_version.protocol_version) .build_with_mock_rpc() .set_genesis_state(); let platform_state = platform.state.load(); let (identity, signer, key) = setup_identity(&mut platform, 4002, dash_to_credits!(1.0)); - let mut contract = DataContractFactory::new(15) + let mut contract = DataContractFactory::new(platform_version.protocol_version) .expect("expected a contract factory") .create_with_value_config( identity.id(), diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs index 5cf14f06148..9da1db0d4d2 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs @@ -33,7 +33,7 @@ impl Drive { } else { Some(HashMap::new()) }; - self.delete_document_for_contract_apply_and_add_to_operations_v1( + self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( document_id, contract, document_type_name, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs index 343bea486c0..fdbfbda0c92 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs @@ -5,6 +5,9 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -60,11 +63,25 @@ impl Drive { drive_operations, platform_version, ), + // This signature carries only the block time. Generation 1 records + // a lifecycle entry for a keep-history document with the block and + // deleter that authored the delete, which only the + // `_with_lifecycle` entry point receives, so it serves the types + // that keep no history and refuses the rest instead of recording a + // fabricated block. + 1 if contract + .document_type_for_name(document_type_name)? + .documents_keep_history() => + { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "deleting a keep-history document needs the block and deleter that authored it", + ))) + } 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( document_id, contract, document_type_name, - &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), + &BlockInfo::default_with_time(block_time_ms), None, estimated_costs_only_with_layer_info, block_time_ms, @@ -79,4 +96,60 @@ impl Drive { })), } } + + /// Deletes a document and adds the operations to the given list, recording + /// the lifecycle entry of a keep-history document with the block and deleter + /// that authored the delete. + #[allow(clippy::too_many_arguments)] + pub fn delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, + estimated_costs_only_with_layer_info: Option< + HashMap, + >, + block_time_ms: u64, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .document + .delete + .delete_document_for_contract_apply_and_add_to_operations + { + 0 => self.delete_document_for_contract_apply_and_add_to_operations_v0( + document_id, + contract, + document_type_name, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + drive_operations, + platform_version, + ), + 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( + document_id, + contract, + document_type_name, + block_info, + deleter_id, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + drive_operations, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "delete_document_for_contract_apply_and_add_to_operations".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } + } } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs index ebf8ce8dd41..2de3672955d 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs @@ -14,7 +14,7 @@ impl Drive { /// Deletes a document. #[inline(always)] #[allow(clippy::too_many_arguments)] - pub(in crate::drive::document::delete) fn delete_document_for_contract_apply_and_add_to_operations_v1( + pub(super) fn delete_document_for_contract_apply_and_add_to_operations_v1( &self, document_id: Identifier, contract: &DataContract, @@ -36,18 +36,19 @@ impl Drive { && transaction.is_none()) .then(|| self.grove.start_transaction()); let transaction = owned_transaction.as_ref().or(transaction); - let batch_operations = self.delete_document_for_contract_with_named_type_operations_v1( - document_id, - contract, - document_type_name, - block_info, - deleter_id, - None, - &mut estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - )?; + let batch_operations = self + .delete_document_for_contract_with_named_type_operations_with_lifecycle( + document_id, + contract, + document_type_name, + block_info, + deleter_id, + None, + &mut estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + )?; self.apply_batch_low_level_drive_operations( estimated_costs_only_with_layer_info, transaction, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs index 1285ef6cd07..e2b056d54ad 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs @@ -52,7 +52,7 @@ impl Drive { let contract = &contract_fetch_info.contract; - self.delete_document_for_contract_apply_and_add_to_operations_v1( + self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( document_id, contract, document_type_name, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs index 023da2bdd18..10d1297bdf0 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -63,21 +62,9 @@ impl Drive { transaction, platform_version, ), - 1 => self.delete_document_for_contract_id_with_named_type_operations_v1( - document_id, - contract_id, - document_type_name, - &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), - None, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_id_with_named_type_operations".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs deleted file mode 100644 index 60e154fc40c..00000000000 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v1/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -use grovedb::batch::KeyInfoPath; - -use grovedb::{EstimatedLayerInformation, TransactionArg}; - -use std::collections::HashMap; - -use crate::drive::Drive; -use crate::error::document::DocumentError; - -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; - -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; - -use dpp::version::PlatformVersion; - -impl Drive { - /// Prepares the operations for deleting a document. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_id_with_named_type_operations_v1( - &self, - document_id: Identifier, - contract_id: Identifier, - document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, - 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> { - let mut operations = vec![]; - let Some(contract_fetch_info) = self.get_contract_with_fetch_info_and_add_to_operations( - contract_id.to_buffer(), - Some(&block_info.epoch), - true, - transaction, - &mut operations, - platform_version, - )? - else { - return Err(Error::Document(DocumentError::DataContractNotFound)); - }; - - let contract = &contract_fetch_info.contract; - let document_type = contract.document_type_for_name(document_type_name)?; - self.delete_document_for_contract_operations_with_lifecycle( - document_id, - contract, - document_type, - block_info, - deleter_id, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ) - } -} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 462fb56d686..0f1a4d29573 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -182,7 +182,7 @@ impl Drive { // The record remembers how many revisions the history retained next to // the revision being deleted, so a later by-revision read can tell a - // contiguous history from one a pre-protocol-14 overwrite left gapped. + // contiguous history from one a pre-protocol-15 overwrite left gapped. // That count is the history tree's own aggregate, read off its element // in the type's history tree. let mut history_type_path = document_history_path( diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs index 5a1a5ece047..a64c6228917 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs @@ -5,6 +5,9 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -59,11 +62,25 @@ impl Drive { transaction, platform_version, ), + // This signature carries only the block time. Generation 1 records + // a lifecycle entry for a keep-history document with the block and + // deleter that authored the delete, which only the + // `_with_lifecycle` entry point receives, so it serves the types + // that keep no history and refuses the rest instead of recording a + // fabricated block. + 1 if contract + .document_type_for_name(document_type_name)? + .documents_keep_history() => + { + Err(Error::Drive(DriveError::CorruptedCodeExecution( + "deleting a keep-history document needs the block and deleter that authored it", + ))) + } 1 => self.delete_document_for_contract_with_named_type_operations_v1( document_id, contract, document_type_name, - &dpp::block::block_info::BlockInfo::default_with_time(block_time_ms), + &BlockInfo::default_with_time(block_time_ms), None, previous_batch_operations, estimated_costs_only_with_layer_info, @@ -78,4 +95,59 @@ impl Drive { })), } } + + /// Prepares a lifecycle-aware delete of a document named by its type, using + /// the block and deleter that authored the state transition. + #[allow(clippy::too_many_arguments)] + pub(crate) fn delete_document_for_contract_with_named_type_operations_with_lifecycle( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, + 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_document_for_contract_with_named_type_operations + { + 0 => self.delete_document_for_contract_with_named_type_operations_v0( + document_id, + contract, + document_type_name, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_with_named_type_operations_v1( + document_id, + contract, + document_type_name, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "delete_document_for_contract_with_named_type_operations".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } + } } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs index 9d2b9cfcac7..f87fc8b5890 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs @@ -21,7 +21,7 @@ impl Drive { /// Prepares the operations for deleting a document. #[inline(always)] #[allow(clippy::too_many_arguments)] - pub(in crate::drive::document::delete) fn delete_document_for_contract_with_named_type_operations_v1( + pub(super) fn delete_document_for_contract_with_named_type_operations_v1( &self, document_id: Identifier, contract: &DataContract, diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 1eb714ffac8..53a476a8217 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1395,6 +1395,159 @@ mod tests { ); } + /// A delete entry point that carries only the block time cannot author the + /// lifecycle entry of a keep-history document, so at the lifecycle + /// generation it must refuse such a document loudly instead of recording a + /// fabricated block, while the entry points that carry the block still + /// delete it. + #[test] + fn should_refuse_a_keep_history_delete_through_entry_points_without_a_block() { + use crate::drive::document::lifecycle::DocumentLifecycleState; + use dpp::document::DocumentV0Getters; + + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let contract = setup_contract( + &drive, + "tests/supporting_files/contract/family/family-contract-with-history.json", + None, + None, + None::, + None, + None, + ); + let document_type = contract + .document_type_for_name("person") + .expect("expected to get document type"); + let owner_id = rand::thread_rng().gen::<[u8; 32]>(); + let person_document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some(owner_id.into()), + document_type, + platform_version, + ) + .expect("expected to get document"); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &person_document, + Some(Cow::Owned(StorageFlags::SingleEpoch(0))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("expected to insert a document successfully"); + let document_id = person_document.id(); + + let is_refusal = |error: Error| { + assert!( + matches!( + &error, + Error::Drive(DriveError::CorruptedCodeExecution(message)) + if message.contains("needs the block and deleter") + ), + "expected the keep-history refusal, got {error:?}" + ); + }; + is_refusal( + drive + .delete_document_for_contract_operations( + document_id, + &contract, + document_type, + None, + &mut None, + 1_000, + None, + platform_version, + ) + .expect_err("a delete without a block must refuse a keep-history document"), + ); + is_refusal( + drive + .delete_document_for_contract_with_named_type_operations( + document_id, + &contract, + "person", + None, + &mut None, + 1_000, + None, + platform_version, + ) + .expect_err("a delete without a block must refuse a keep-history document"), + ); + is_refusal( + drive + .delete_document_for_contract_apply_and_add_to_operations( + document_id, + &contract, + "person", + None, + 1_000, + None, + &mut vec![], + platform_version, + ) + .expect_err("a delete without a block must refuse a keep-history document"), + ); + assert!( + matches!( + drive + .fetch_document_lifecycle( + &contract, + document_type, + document_id, + None, + None, + platform_version, + ) + .expect("expected to read the lifecycle") + .0, + DocumentLifecycleState::Active(_) + ), + "a refused delete leaves the document untouched" + ); + + drive + .delete_document_for_contract( + document_id, + &contract, + "person", + BlockInfo::default_with_time(2_000), + true, + None, + platform_version, + Some(&EPOCH_CHANGE_FEE_VERSION_TEST), + ) + .expect("the entry point that carries the block deletes the document"); + assert!(matches!( + drive + .fetch_document_lifecycle( + &contract, + document_type, + document_id, + None, + None, + platform_version, + ) + .expect("expected to read the lifecycle") + .0, + DocumentLifecycleState::Deleted(_) + )); + } + // ---------- Error-path tests (added for coverage) ---------- #[test] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs index b63679cdd24..5d40182e37b 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs @@ -12,8 +12,11 @@ pub const STATE_TRANSITION_VERSIONS_V4: DPPStateTransitionVersions = DPPStateTra documents: DocumentTransitionVersions { documents_batch_transition: DocumentsBatchTransitionVersions { validation: DocumentsBatchTransitionValidationVersions { - find_duplicates_by_id: 0, validate_base_structure: 1, + ..STATE_TRANSITION_VERSIONS_V3 + .documents + .documents_batch_transition + .validation }, }, }, 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 index fdc952ce265..58e60776cc4 100644 --- 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 @@ -30,7 +30,6 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = delete_document_for_contract_id: 1, delete_document_for_contract_apply_and_add_to_operations: 1, remove_document_from_primary_storage: 1, - delete_document_for_contract_id_with_named_type_operations: 1, delete_document_for_contract_with_named_type_operations: 1, delete_document_for_contract_operations: 1, erase_document_for_contract_operations: Some(0), diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 2a4b6619eda..64beccb5b45 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -360,7 +360,7 @@ mod document_history_mock_tests { #[test] fn should_round_trip_every_lifecycle_state_with_its_times() { let mut sdk = crate::SdkBuilder::default() - .with_version(dpp::version::PlatformVersion::get(15).unwrap()) + .with_version(dpp::version::PlatformVersion::latest()) .build() .unwrap(); let mock = sdk.mock(); From f88edc585f01c58afee126d1cf321539f7ea903d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 19 Sep 2026 16:55:19 +0700 Subject: [PATCH 36/48] fix(drive): never write a new document over a deleted one's retained revisions The primary-storage writer refuses to create a keep-history document whose id still retains revisions, but it deferred that refusal to the caller's `override_document` claim, which the shipped insert generation forwards as "skip the checks" once the document turns out not to exist. A deleted document has no current entry and still retains its revisions, so a create with the override merged the new document into the deleted one's history while its lifecycle record stood. Consensus creates never pass the override, so no chain path reached this; the public Drive API did. Document insert generation 2, selected from protocol 15, forwards the override to primary storage only for types that keep no history, whose missing entry hides nothing; a keep-history type always lets the writer check. Types without history keep generation 1's cheaper write, so token history and other system-contract documents are billed as before. Also names the trust boundary of the raw erase operation (the type's `canBeErased` and the submitter's authorization are decided in transition validation, like a delete's ownership), states the shipped rule a delete's proof classification inherits instead of overclaiming it, and says why the batch constructor traits read through the format-aware accessor view. Test: `should_refuse_to_create_over_retained_revisions_even_when_the_caller_overrides` merges the document on the previous generation and is refused now. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch_transition/methods/v0/mod.rs | 5 + .../mod.rs | 15 +- .../v2/mod.rs | 205 ++++++++++++++++++ .../add_document_to_primary_storage/v1/mod.rs | 3 + .../src/drive/document/lifecycle/tests.rs | 50 +++++ .../src/util/batch/drive_op_batch/document.rs | 4 +- .../v1/mod.rs | 5 +- .../drive_document_method_versions/v5.rs | 4 +- .../tests/keep_history_delete_versions.rs | 21 ++ 9 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v2/mod.rs diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs index 76bf918daa4..2e1caa9d08d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs @@ -28,6 +28,11 @@ use platform_value::Identifier; use platform_version::version::PlatformVersion; use std::convert::TryFrom; +/// Client-side constructors and signing helpers of a batch transition. The +/// trait is not selected by the version tables; it is the API every batch +/// wire format shares, so it reads a batch through the accessor view that +/// knows every format: a format 2 batch must see its own transitions to +/// compute the key its signature needs. pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 { #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/mod.rs index ba4d552f4be..9c9908a6a56 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/mod.rs @@ -1,5 +1,6 @@ mod v0; mod v1; +mod v2; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -91,9 +92,21 @@ impl Drive { transaction, platform_version, ), + // v2: a fresh insert of a keep-history document never lets the + // caller's override skip the primary-storage checks, so a deleted + // document's retained revisions cannot be written over. + 2 => self.add_document_for_contract_operations_v2( + document_and_contract_info, + override_document, + block_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "add_document_for_contract_operations".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v2/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v2/mod.rs new file mode 100644 index 00000000000..05be13227e7 --- /dev/null +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract_operations/v2/mod.rs @@ -0,0 +1,205 @@ +use crate::drive::document::paths::contract_documents_primary_key_path; +use crate::drive::document::primary_key_tree_type::DocumentTypePrimaryKeyTreeType; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::DirectQueryType::{StatefulDirectQuery, StatelessDirectQuery}; +use crate::util::grove_operations::QueryTarget::QueryTargetValue; +use crate::util::object_size_info::{DocumentAndContractInfo, DocumentInfoV0Methods}; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; + +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Gathers the operations to add a document to a contract. + /// + /// v2 is v1 with one difference in how a fresh insert of a keep-history + /// document reaches primary storage: `override_document` no longer lets + /// the primary-storage writer skip its own checks once the document turned + /// out not to exist. Under the keep-history layout a deleted document has + /// no current entry but still retains its revisions, and the writer + /// refuses to create over them only when it is allowed to check; a + /// caller's override speaks for the current entry alone, so the insert + /// path does not forward it for such a type. Types that keep no history + /// have nothing behind a missing entry and keep v1's cheaper write. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn add_document_for_contract_operations_v2( + &self, + document_and_contract_info: DocumentAndContractInfo, + override_document: bool, + block_info: &BlockInfo, + previous_batch_operations: &mut Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut batch_operations: Vec = vec![]; + + // indexOnly document types have no primary-storage row and no + // primary-key tree at all — the index entries are the rows, and + // updates are impossible by construction (immutable, no revision). + // `index_only()` can only be true on a PV14+ contract (the grammar + // rejects the keyword below meta-schema v3), so this branch is + // unreachable for every historical document. + if document_and_contract_info.document_type.index_only() { + 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( + document_and_contract_info.contract, + estimated_costs_only_with_layer_info, + &platform_version.drive, + )?; + } + + self.add_indices_for_top_index_level_for_contract_operations( + &document_and_contract_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + &mut batch_operations, + platform_version, + )?; + + self.add_preallocated_index_tree_operations_for_referring_types( + &document_and_contract_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + return Ok(batch_operations); + } + + let primary_key_path = contract_documents_primary_key_path( + document_and_contract_info.contract.id_ref().as_bytes(), + document_and_contract_info.document_type.name().as_str(), + ); + + let primary_key_tree_type = document_and_contract_info + .document_type + .primary_key_tree_type(platform_version)?; + + // Apply means stateful query + let query_type = if estimated_costs_only_with_layer_info.is_none() { + StatefulDirectQuery + } else { + StatelessDirectQuery { + in_tree_type: primary_key_tree_type, + query_target: QueryTargetValue( + document_and_contract_info + .document_type + .estimated_size(platform_version)? as u32, + ), + } + }; + + // To update but not create: + + // 1. Override should be allowed + let could_be_update = override_document; + + // 2. Is not a dry run + let could_be_update = could_be_update + && !document_and_contract_info + .owned_document_info + .document_info + .is_document_size(); + + // 3. Document exists in storage + let is_update = could_be_update + && self.grove_has_raw( + primary_key_path.as_ref().into(), + document_and_contract_info + .owned_document_info + .document_info + .id_key_value_info() + .as_key_ref_request()?, + query_type, + transaction, + &mut batch_operations, + &platform_version.drive, + )?; + + if is_update { + let update_operations = self + .update_document_for_contract_operations_without_ttl_drain( + document_and_contract_info, + block_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + batch_operations.extend(update_operations); + + return Ok(batch_operations); + } + + // if we are trying to get estimated costs we need to add the upper levels + 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( + document_and_contract_info.contract, + estimated_costs_only_with_layer_info, + &platform_version.drive, + )?; + } + + // The document has no current entry, whatever the caller assumed. For + // a keep-history type that entry is not the whole story, so the writer + // performs its own checks, including the refusal to create over a + // deleted document's retained revisions. + let insert_without_check = override_document + && !document_and_contract_info + .document_type + .documents_keep_history(); + self.add_document_to_primary_storage( + &document_and_contract_info, + block_info, + insert_without_check, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + self.add_indices_for_top_index_level_for_contract_operations( + &document_and_contract_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + &mut batch_operations, + platform_version, + )?; + + // If any indexOnly document type of this contract carries a + // `preallocated` index bound to this document type through a + // refersTo declaration, create that index's trees for entries + // referencing this document now — see + // `add_preallocated_index_tree_operations`. A no-op for every + // contract without the (PV14+, indexOnly-only) flag. + self.add_preallocated_index_tree_operations_for_referring_types( + &document_and_contract_info, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + Ok(batch_operations) + } +} diff --git a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs index 181e822c5ee..d9ab8dbcf63 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_to_primary_storage/v1/mod.rs @@ -115,6 +115,9 @@ impl Drive { // history would silently merge a new document into a deleted one's // record, so it is refused here rather than only in transition // validation: this is the guard that covers writers outside it. + // Only the update path asks to write without checks, and its + // document holds the id already, so its history is meant to exist; + // the insert path always lets this check run. // A dry run skips the probe, which reports the tree as absent, and // pays for it as a fixed cost so estimation and execution agree. if !created && !insert_without_check { diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index f661688f884..68e027fd7de 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -478,6 +478,56 @@ fn should_refuse_to_create_a_document_whose_revisions_are_still_retained() { assert_eq!(visible_document_count(&drive, &contract), 1); } +/// `override_document` tells the writer the caller has already checked the +/// primary-key entry, so it may skip that check on the current pointer. A +/// deleted document has no current pointer but still retains its revisions, +/// and nothing the caller checked says so; the retained-history refusal must +/// therefore hold whatever the caller claims about the pointer. +#[test] +fn should_refuse_to_create_over_retained_revisions_even_when_the_caller_overrides() { + let owner = [14u8; 32]; + let (drive, contract, id) = setup_history(2, owner); + delete(&drive, &contract, id, Identifier::new(owner), 5_000); + + let version = latest(); + let document_type = document_type_of(&contract); + let mut document = + json_document_to_document(PERSON, Some(owner.into()), document_type, version).unwrap(); + document.set_revision(Some(1)); + + let error = drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + true, + BlockInfo::default_with_time(6_000), + true, + None, + version, + None, + ) + .expect_err("an override must not merge a new document into a deleted one's history"); + assert!(matches!( + error, + Error::Drive(DriveError::CorruptedDocumentAlreadyExists(_)) + )); + assert_eq!( + history_metadata(&drive, &contract, id).remaining_revisions, + 2, + "the refused write must leave the retained history untouched" + ); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Deleted(_) + )); +} + #[test] fn should_refuse_to_drop_a_history_subtree_while_a_revision_survives() { let owner = [13u8; 32]; 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 6edf840e518..5fde1093077 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 @@ -117,7 +117,9 @@ pub enum DocumentOperationType<'a> { }, /// Removes a bounded chunk of the retained revisions of an already deleted /// keep-history document, dropping the history subtree and the lifecycle - /// record with the terminal chunk. + /// record with the terminal chunk. Whether the type allows erasure and who + /// may start one are decided in transition validation, like a delete's + /// ownership; this operation assumes both. EraseDocument { /// The document id document_id: Identifier, diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs index e90b07c857f..9b3e594bb47 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs @@ -3444,8 +3444,9 @@ mod tests { /// An erase acts on a document that was already deleted, so the by-id /// proof shows the same absence whether or not it ran. The classifier must /// report that as affected state, while a delete over the very same proof - /// stays execution-proved: presence before and absence after do bind a - /// delete's execution. + /// keeps the classification generation 0 gives a delete: a document's + /// absence by id is read as the delete's outcome, which is what the SDK's + /// strict wait relies on. #[test] fn verify_batch_document_erase_is_affected_state_not_execution_proved() { use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; 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 index 58e60776cc4..ddff0075319 100644 --- 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 @@ -12,7 +12,8 @@ use super::{ /// references and records a lifecycle entry while the retained revisions stay /// readable; an erase removes those revisions a chunk at a time. The delete /// wrappers, the primary-storage removal estimate and the lifecycle read are -/// the generations that know that layout. +/// the generations that know that layout, and the insert generation never lets +/// a caller's override write over a deleted document's retained revisions. pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = DriveDocumentMethodVersions { query: DriveDocumentQueryMethodVersions { @@ -37,6 +38,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.delete }, insert: DriveDocumentInsertMethodVersions { + add_document_for_contract_operations: 2, add_document_to_primary_storage: 1, add_reference_for_index_level_for_contract_operations: 1, ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.insert diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index c0de8d7a4ce..e7e5195e568 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -74,6 +74,17 @@ fn should_preserve_released_keep_history_validation_versions() { 0, "the keep-history delete branch must not be selected at protocol {protocol}" ); + let released_insert_generation = if protocol == 14 { 1 } else { 0 }; + assert_eq!( + version + .drive + .methods + .document + .insert + .add_document_for_contract_operations, + released_insert_generation, + "document insert at protocol {protocol}" + ); assert_eq!( version .dpp @@ -180,6 +191,16 @@ fn should_activate_keep_history_validation_at_protocol_15() { 1, "protocol 15 selects the keep-history delete branch" ); + assert_eq!( + version + .drive + .methods + .document + .insert + .add_document_for_contract_operations, + 2, + "protocol 15 selects the insert that never writes over retained revisions" + ); assert_eq!( version .dpp From a5a3d20450bf78df6d25a91fba0658038fcd7f3d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 19 Sep 2026 23:22:19 +0700 Subject: [PATCH 37/48] refactor(platform)!: carry the erase kind on the shipped batch transition Append `DocumentTransition::Erase` and `DocumentTransitionAction::EraseAction` to the shipped enums, the way the indexOnly delete kind was added, instead of introducing batch wire format 2, batch action format 1 and a generation of every batch-level validator, transformer, converter, prover and verifier that could read them. Admission is gated per kind: `validate_basic_structure/v0` refuses an erase wherever the active protocol version publishes no bounds for it, so software that decodes the kind agrees with software that cannot while protocol version 14 is still active. The arms added to the shipped transformer, structure, state, converter and verifier generations are unreachable below protocol version 15 and byte-identical for every historical input; data triggers are matched by action type, so an erase reaches none. Version tables: `STATE_TRANSITION_SERIALIZATION_VERSIONS_V4` keeps only the erase bounds (batch wire bounds stay `0..=1`, default 1); `STATE_TRANSITION_VERSIONS_V4` is removed and protocol 15 keeps the shipped batch structure generation; the batch conversion, prover, verifier, transformer, advanced structure, state, nonce and admission slots at protocol 15 are the same as at protocol 14; the format-aware document fetch slot is gone from every table. Released constants differ by dormant `None`/`0` slots only. Deleted: the `DocumentTransitionV1`/`BatchedTransitionV1` shells, `BatchTransitionV2`, the second accessor view, batch basic structure v1, `BatchTransitionActionV1`, `StateTransitionAction::BatchActionV1`, converter `batch_transition_v1`, prover v1, verifier v1, abci transformer v1, state v2, state validation v1, advanced structure v1, nonce v1 and is-allowed v1. Clients build a format 1 batch: the rs-sdk erase builder accepts batch formats 0 and 1, wasm-dpp2 wraps the shipped shells and keeps the `erase` action type, wallet-ffi and wasm-sdk read the shipped accessors. Tests: the boundary pin `validate_base_structure_v0_gates_erase_by_protocol_version` refuses a format 1 batch with an erase at protocol 14 with `UnsupportedVersionError` and admits it at the latest version; a platform-version pin holds every batch-level slot at protocol 15 equal to protocol 14; the verifier classification test and the batch action key-level test are ported onto the shipped types. Co-Authored-By: Claude Fable 5.1 --- .../src/serialization/json/safe_fields.rs | 8 - packages/rs-dpp/src/state_transition/mod.rs | 181 +- .../batch_transition/accessors/mod.rs | 25 - .../document/batch_transition/accessors/v1.rs | 232 - .../batched_transition/document_transition.rs | 38 +- .../document_transition_action_type.rs | 27 +- .../batched_transition/mod.rs | 5 - .../batched_transition/resolvers.rs | 17 +- .../batched_transition/token_transition.rs | 6 +- .../batch_transition/batched_transition/v1.rs | 944 --- .../batch_transition/identity_signed.rs | 4 - .../document/batch_transition/methods/mod.rs | 496 +- .../batch_transition/methods/v0/mod.rs | 53 +- .../batch_transition/methods/v1/mod.rs | 6 +- .../batch_transition/methods/v2/mod.rs | 35 - .../document/batch_transition/mod.rs | 4 - .../batch_transition/resolvers/v0/mod.rs | 3 +- ...ate_transition_estimated_fee_validation.rs | 4 +- .../batch_transition/state_transition_like.rs | 10 - .../document/batch_transition/tests.rs | 166 +- .../batch_transition/v0/v0_methods.rs | 51 +- .../batch_transition/v1/v0_methods.rs | 51 +- .../batch_transition/v2/identity_signed.rs | 42 - .../document/batch_transition/v2/mod.rs | 38 - .../v2/state_transition_like.rs | 100 - .../document/batch_transition/v2/types.rs | 18 - .../batch_transition/v2/v0_methods.rs | 459 -- .../batch_transition/v2/v1_methods.rs | 929 --- .../document/batch_transition/v2/version.rs | 9 - .../validate_basic_structure/mod.rs | 4 +- .../validate_basic_structure/v0/mod.rs | 139 +- .../validate_basic_structure/v1/mod.rs | 728 -- .../document/batch_transition/version.rs | 1 - .../execution/types/execution_event/mod.rs | 21 - .../batch/advanced_structure/mod.rs | 1 - .../batch/advanced_structure/v0/mod.rs | 15 + .../batch/advanced_structure/v1/mod.rs | 284 - .../batch/identity_contract_nonce/mod.rs | 1 - .../batch/identity_contract_nonce/v1/mod.rs | 67 - .../state_transitions/batch/is_allowed/mod.rs | 4 +- .../batch/is_allowed/v1/mod.rs | 53 - .../state_transitions/batch/mod.rs | 68 +- .../state_transitions/batch/state/mod.rs | 2 - .../batch/state/v0/fetch_documents.rs | 119 - .../state_transitions/batch/state/v0/mod.rs | 10 + .../state_transitions/batch/state/v2/mod.rs | 47 - .../batch/state/validation_v1/mod.rs | 348 - .../batch/tests/document/creation.rs | 2 +- .../batch/tests/document/erase.rs | 20 +- .../distribution/perpetual/block_based.rs | 19 +- .../batch/tests/token/transfer/mod.rs | 8 +- .../batch/transformer/mod.rs | 1 - .../batch/transformer/v0/mod.rs | 11 + .../batch/transformer/v1/mod.rs | 1148 --- .../verify_state_transitions.rs | 53 +- .../src/prove/prove_state_transition/mod.rs | 4 +- .../prove/prove_state_transition/v1/mod.rs | 555 -- packages/rs-drive/src/query/filter.rs | 4 + .../batch/batch_transition_v1.rs | 138 - .../batch/document/document_transition.rs | 7 + .../action_convert_to_operations/batch/mod.rs | 1 - .../action_convert_to_operations/mod.rs | 3 - .../transformer.rs | 4 +- .../v0/transformer.rs | 17 +- .../document_transition_action_type.rs | 1 + .../document_transition/mod.rs | 7 + .../src/state_transition_action/batch/mod.rs | 2 - .../state_transition_action/batch/tests.rs | 87 + .../state_transition_action/batch/v1/mod.rs | 275 - .../state_transition_action/batch/v1/tests.rs | 316 - .../src/state_transition_action/mod.rs | 6 - .../transformer.rs | 50 +- .../mod.rs | 10 +- .../v0/mod.rs | 187 +- .../v1/mod.rs | 6410 ----------------- .../v4.rs | 17 +- .../dpp_state_transition_versions/mod.rs | 1 - .../dpp_state_transition_versions/v4.rs | 24 - .../drive_abci_validation_versions/mod.rs | 8 - .../drive_abci_validation_versions/v1.rs | 2 - .../drive_abci_validation_versions/v10.rs | 2 - .../drive_abci_validation_versions/v11.rs | 31 +- .../drive_abci_validation_versions/v2.rs | 2 - .../drive_abci_validation_versions/v3.rs | 2 - .../drive_abci_validation_versions/v4.rs | 2 - .../drive_abci_validation_versions/v5.rs | 2 - .../drive_abci_validation_versions/v6.rs | 2 - .../drive_abci_validation_versions/v7.rs | 2 - .../drive_abci_validation_versions/v8.rs | 2 - .../drive_abci_validation_versions/v9.rs | 2 - .../v5.rs | 5 +- .../drive_verify_method_versions/v3.rs | 11 +- .../src/version/drive_versions/v10.rs | 9 +- .../rs-platform-version/src/version/v15.rs | 19 +- .../tests/keep_history_delete_versions.rs | 224 +- .../src/parse_state_transition.rs | 10 +- .../platform/documents/transitions/erase.rs | 16 +- .../document_transition/mod.rs | 3 + .../base/state_transition.rs | 17 +- .../batch/batch_transition.rs | 66 +- .../batch/batched_transition.rs | 45 +- .../batch/document_transition.rs | 28 +- .../batch/document_transitions/create.rs | 4 +- .../batch/document_transitions/delete.rs | 4 +- .../batch/document_transitions/purchase.rs | 4 +- .../batch/document_transitions/replace.rs | 4 +- .../batch/document_transitions/transfer.rs | 4 +- .../document_transitions/update_price.rs | 4 +- .../src/state_transitions/broadcast.rs | 4 +- 109 files changed, 1013 insertions(+), 14788 deletions(-) delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs delete mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs delete mode 100644 packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs delete mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs delete mode 100644 packages/rs-drive/src/state_transition_action/batch/v1/mod.rs delete mode 100644 packages/rs-drive/src/state_transition_action/batch/v1/tests.rs delete mode 100644 packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs delete mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs diff --git a/packages/rs-dpp/src/serialization/json/safe_fields.rs b/packages/rs-dpp/src/serialization/json/safe_fields.rs index 1816204b8d2..4b3c27a89f7 100644 --- a/packages/rs-dpp/src/serialization/json/safe_fields.rs +++ b/packages/rs-dpp/src/serialization/json/safe_fields.rs @@ -137,14 +137,6 @@ impl JsonSafeFields for crate::state_transition::batch_transition::batched_transition::BatchedTransition { } -impl JsonSafeFields - for crate::state_transition::batch_transition::batched_transition::DocumentTransitionV1 -{ -} -impl JsonSafeFields - for crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1 -{ -} impl JsonSafeFields for crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice {} impl JsonSafeFields for crate::group::action_event::GroupActionEvent {} // TokenEvent contains u64 aliases (TokenAmount, Credits) in tuple variants that diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 3a80a422b3c..cff847f8ecf 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1,6 +1,7 @@ use derive_more::From; #[cfg(feature = "serde-conversion")] use serde::{Deserialize, Serialize}; +use state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; use std::collections::BTreeMap; use std::ops::RangeInclusive; @@ -84,10 +85,8 @@ use crate::state_transition::address_funding_from_asset_lock_transition::{ use crate::state_transition::address_funds_transfer_transition::{ AddressFundsTransferTransition, AddressFundsTransferTransitionSignable, }; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransitionRefV1, DocumentTransitionRefV1, -}; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionSignable}; @@ -897,7 +896,6 @@ impl StateTransition { StateTransition::Batch(batch_transition) => match batch_transition { BatchTransition::V0(_) => ALL_VERSIONS, BatchTransition::V1(_) => 9..=LATEST_VERSION, - BatchTransition::V2(_) => 15..=LATEST_VERSION, }, StateTransition::IdentityCreate(_) | StateTransition::IdentityTopUp(_) @@ -975,55 +973,47 @@ impl StateTransition { Self::DataContractUpdate(_) => "DataContractUpdate".to_string(), Self::Batch(batch_transition) => { let mut document_transition_types = vec![]; - for transition in batch_transition.transitions_iter_v1() { + for transition in batch_transition.transitions_iter() { let document_transition_name = match transition { - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Create(_)) => { - "Create" - } - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Replace(_)) => { - "Replace" - } - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Delete(_)) => { - "Delete" - } - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Transfer(_)) => { + BatchedTransitionRef::Document(DocumentTransition::Create(_)) => "Create", + BatchedTransitionRef::Document(DocumentTransition::Replace(_)) => "Replace", + BatchedTransitionRef::Document(DocumentTransition::Delete(_)) => "Delete", + BatchedTransitionRef::Document(DocumentTransition::Transfer(_)) => { "Transfer" } - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::UpdatePrice( - _, - )) => "UpdatePrice", - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Purchase(_)) => { + BatchedTransitionRef::Document(DocumentTransition::UpdatePrice(_)) => { + "UpdatePrice" + } + BatchedTransitionRef::Document(DocumentTransition::Purchase(_)) => { "Purchase" } - BatchedTransitionRefV1::Document( - DocumentTransitionRefV1::IndexOnlyDelete(_), - ) => "IndexOnlyDelete", - BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Erase(_)) => { - "Erase" + BatchedTransitionRef::Document(DocumentTransition::IndexOnlyDelete(_)) => { + "IndexOnlyDelete" } - BatchedTransitionRefV1::Token(TokenTransition::Transfer(_)) => { + BatchedTransitionRef::Document(DocumentTransition::Erase(_)) => "Erase", + BatchedTransitionRef::Token(TokenTransition::Transfer(_)) => { "TokenTransfer" } - BatchedTransitionRefV1::Token(TokenTransition::Mint(_)) => "TokenMint", - BatchedTransitionRefV1::Token(TokenTransition::Burn(_)) => "TokenBurn", - BatchedTransitionRefV1::Token(TokenTransition::Freeze(_)) => "TokenFreeze", - BatchedTransitionRefV1::Token(TokenTransition::Unfreeze(_)) => { + BatchedTransitionRef::Token(TokenTransition::Mint(_)) => "TokenMint", + BatchedTransitionRef::Token(TokenTransition::Burn(_)) => "TokenBurn", + BatchedTransitionRef::Token(TokenTransition::Freeze(_)) => "TokenFreeze", + BatchedTransitionRef::Token(TokenTransition::Unfreeze(_)) => { "TokenUnfreeze" } - BatchedTransitionRefV1::Token(TokenTransition::DestroyFrozenFunds(_)) => { + BatchedTransitionRef::Token(TokenTransition::DestroyFrozenFunds(_)) => { "TokenDestroyFrozenFunds" } - BatchedTransitionRefV1::Token(TokenTransition::EmergencyAction(_)) => { + BatchedTransitionRef::Token(TokenTransition::EmergencyAction(_)) => { "TokenEmergencyAction" } - BatchedTransitionRefV1::Token(TokenTransition::ConfigUpdate(_)) => { + BatchedTransitionRef::Token(TokenTransition::ConfigUpdate(_)) => { "TokenConfigUpdate" } - BatchedTransitionRefV1::Token(TokenTransition::Claim(_)) => "TokenClaim", - BatchedTransitionRefV1::Token(TokenTransition::DirectPurchase(_)) => { + BatchedTransitionRef::Token(TokenTransition::Claim(_)) => "TokenClaim", + BatchedTransitionRef::Token(TokenTransition::DirectPurchase(_)) => { "TokenDirectPurchase" } - BatchedTransitionRefV1::Token( + BatchedTransitionRef::Token( TokenTransition::SetPriceForDirectPurchase(_), ) => "SetPriceForDirectPurchase", }; @@ -1399,14 +1389,14 @@ impl StateTransition { st.verify_public_key_is_enabled(identity_public_key)?; } StateTransition::Batch(st) => { - let allow_token_transfer_keys = st.transitions_len_v1() == 1 + let allow_token_transfer_keys = st.transitions_len() == 1 && (st - .first_transition_v1() + .first_transition() .expect("expected first transition with len 1") .as_transition_token_claim() .is_some() || st - .first_transition_v1() + .first_transition() .expect("expected first transition with len 1") .as_transition_token_transfer() .is_some()); @@ -2065,7 +2055,6 @@ impl StateTransitionStructureValidation for StateTransition { #[cfg(test)] mod tests { use super::*; - use crate::state_transition::batch_transition::batched_transition::DocumentTransition; // ----------------------------------------------------------------------- // StateTransitionSigningOptions tests @@ -3308,118 +3297,6 @@ mod tests { } } - // --- Batch wire formats. Format 2 is the first shell that can carry an - // erase, so it becomes active with the erase kind at protocol version 15 - // while formats 0 and 1 keep the ranges they shipped with. A format 2 - // batch must therefore never decode under a released protocol version, - // whatever it carries. --- - fn sample_batch_v2_st_with_erase() -> StateTransition { - use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; - use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransitionV1, DocumentEraseTransition, DocumentTransitionV1, - }; - use crate::state_transition::batch_transition::BatchTransitionV2; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Identifier::from([1u8; 32]), - identity_contract_nonce: 3, - document_type_name: "note".to_string(), - data_contract_id: Identifier::from([2u8; 32]), - }); - let erase = - DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { - base, - })); - StateTransition::Batch(BatchTransition::V2(BatchTransitionV2 { - owner_id: Identifier::from([8u8; 32]), - transitions: vec![BatchedTransitionV1::Document(erase)], - user_fee_increase: 2, - signature_public_key_id: 7, - signature: BinaryData::new(vec![0xEE; 65]), - })) - } - - #[test] - fn test_active_version_range_batch_wire_formats() { - use crate::state_transition::batch_transition::batched_transition::BatchedTransition; - use crate::state_transition::batch_transition::BatchTransitionV1; - - assert_eq!( - sample_batch_st_with_delete().active_version_range(), - ALL_VERSIONS, - "format 0 predates protocol version ranges" - ); - - let StateTransition::Batch(BatchTransition::V0(format_0)) = sample_batch_st_with_delete() - else { - unreachable!("the sample is a format 0 batch") - }; - let format_1 = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: format_0.owner_id, - transitions: format_0 - .transitions - .into_iter() - .map(BatchedTransition::Document) - .collect(), - user_fee_increase: format_0.user_fee_increase, - signature_public_key_id: format_0.signature_public_key_id, - signature: format_0.signature, - })); - assert_eq!( - format_1.active_version_range(), - 9..=LATEST_VERSION, - "format 1 arrived with tokens at protocol version 9" - ); - - assert_eq!( - sample_batch_v2_st_with_erase().active_version_range(), - 15..=LATEST_VERSION, - "format 2 arrived with the erase kind at protocol version 15" - ); - } - - #[cfg(all(feature = "state-transitions", feature = "validation"))] - #[test] - fn test_deserialize_format_2_batch_in_version_rejects_released_protocols() { - use crate::serialization::PlatformSerializable; - use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; - - let bytes = PlatformSerializable::serialize_to_bytes(&sample_batch_v2_st_with_erase()) - .expect("serialize succeeds"); - - let released = PlatformVersion::get(14).expect("protocol version 14 exists"); - let err = StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, released) - .expect_err("a format 2 batch must not decode under protocol version 14"); - match err { - ProtocolError::StateTransitionError(StateTransitionIsNotActiveError { - state_transition_type, - active_version_range, - current_protocol_version, - }) => { - assert_eq!(state_transition_type, "DocumentsBatch([Erase])"); - assert_eq!(active_version_range, 15..=LATEST_VERSION); - assert_eq!(current_protocol_version, 14); - } - other => panic!("expected StateTransitionIsNotActiveError, got {other:?}"), - } - - let decoded = StateTransition::deserialize_from_bytes_untrusted_in_version( - &bytes, - PlatformVersion::latest(), - ) - .expect("a format 2 batch decodes under the latest protocol version"); - let StateTransition::Batch(batch) = decoded else { - panic!("expected a batch, got {decoded:?}"); - }; - assert!(matches!(batch, BatchTransition::V2(_))); - assert_eq!(batch.transitions_len_v1(), 1); - assert_eq!( - batch.transitions_len(), - 0, - "the shell of formats 0 and 1 cannot see an erase and reports the batch as empty" - ); - } - // ----------------------------------------------------------------------- // Additional coverage: variants not yet exercised. // diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs index 2e691425711..f18d109bb24 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/mod.rs @@ -1,21 +1,15 @@ mod v0; -mod v1; -use std::iter::Empty; use std::slice::Iter; use crate::state_transition::batch_transition::batched_transition::{BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef}; use crate::state_transition::batch_transition::BatchTransition; pub use v0::*; -pub use v1::*; use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; /// Iterator enum for `BatchTransition` that can handle both V0 and V1. pub enum DocumentBatchIterator<'a> { V0(Iter<'a, DocumentTransition>), V1(DocumentBatchV1Iterator<'a>), - /// Batch format 2 has no view through the format 0 and 1 accessors; see - /// the `DocumentsBatchTransitionAccessorsV0` implementation below. - V2(Empty>), } /// Iterator for version 1, yielding `BatchedTransitionRef<'a>` items. @@ -43,22 +37,10 @@ impl<'a> Iterator for DocumentBatchIterator<'a> { match self { DocumentBatchIterator::V0(iter) => iter.next().map(BatchedTransitionRef::Document), DocumentBatchIterator::V1(iter) => iter.next(), - DocumentBatchIterator::V2(iter) => iter.next(), } } } -/// The view of a batch through the transition shells of batch formats 0 and 1. -/// -/// Batch format 2 carries a document transition shell with an erase kind that -/// [`BatchedTransitionRef`] cannot represent, so a format 2 batch has no view -/// here: every accessor reports an empty batch for it. Code that must see every -/// format uses [`DocumentsBatchTransitionAccessorsV1`]. The generations that -/// still read this view (structure, nonce, admission and state validation -/// alike) are the ones selected for protocol versions that reject a format 2 -/// batch by its wire version while decoding it, before any of them runs; and -/// the structure generation among them rejects an empty batch, so the empty -/// view fails closed rather than silently dropping transitions. impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { type IterType<'a> = DocumentBatchIterator<'a> @@ -72,7 +54,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { BatchTransition::V1(v1) => DocumentBatchIterator::V1(DocumentBatchV1Iterator { inner: v1.transitions.iter(), }), - BatchTransition::V2(_) => DocumentBatchIterator::V2(std::iter::empty()), } } @@ -80,7 +61,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.transitions.len(), BatchTransition::V1(v1) => v1.transitions.len(), - BatchTransition::V2(_) => 0, } } @@ -88,7 +68,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.transitions.is_empty(), BatchTransition::V1(v1) => v1.transitions.is_empty(), - BatchTransition::V2(_) => true, } } @@ -99,7 +78,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .first() .map(|batch_transition| batch_transition.borrow_as_ref()), - BatchTransition::V2(_) => None, } } @@ -113,7 +91,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .first_mut() .map(|batch_transition| batch_transition.borrow_as_mut()), - BatchTransition::V2(_) => None, } } @@ -124,7 +101,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .iter() .any(|transition| matches!(transition, BatchedTransition::Document(_))), - BatchTransition::V2(_) => false, } } @@ -135,7 +111,6 @@ impl DocumentsBatchTransitionAccessorsV0 for BatchTransition { .transitions .iter() .any(|transition| matches!(transition, BatchedTransition::Token(_))), - BatchTransition::V2(_) => false, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs deleted file mode 100644 index 55f2af5d0a9..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/accessors/v1.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::slice::Iter; - -use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionMutRefV1, BatchedTransitionRefV1, BatchedTransitionV1, - DocumentTransition, -}; -use crate::state_transition::batch_transition::{ - BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, -}; - -/// The view of a batch that sees every batch format. -/// -/// Items are borrowed through the transition shells introduced with batch -/// format 2, which know every kind the older formats carry plus the erase -/// kind. Formats 0 and 1 are viewed through a lossless conversion of their own -/// shells. -pub trait DocumentsBatchTransitionAccessorsV1 { - /// Associated type for the iterator. - type IterType<'a>: Iterator> - where - Self: 'a; - - /// Returns an iterator over the `BatchedTransitionRefV1` items. - fn transitions_iter_v1(&self) -> Self::IterType<'_>; - - fn transitions_len_v1(&self) -> usize; - fn transitions_are_empty_v1(&self) -> bool; - - fn first_transition_v1(&self) -> Option>; - - fn first_transition_mut_v1(&mut self) -> Option>; - fn contains_document_transition_v1(&self) -> bool; - fn contains_token_transition_v1(&self) -> bool; -} - -/// Iterator over the items of any batch format, yielding -/// `BatchedTransitionRefV1<'a>` items. -pub enum DocumentBatchIteratorV1<'a> { - V0(Iter<'a, DocumentTransition>), - V1(Iter<'a, BatchedTransition>), - V2(Iter<'a, BatchedTransitionV1>), -} - -impl<'a> Iterator for DocumentBatchIteratorV1<'a> { - type Item = BatchedTransitionRefV1<'a>; - - fn next(&mut self) -> Option { - match self { - Self::V0(iter) => iter - .next() - .map(|transition| BatchedTransitionRefV1::Document(transition.into())), - Self::V1(iter) => iter.next().map(Into::into), - Self::V2(iter) => iter.next().map(BatchedTransitionV1::borrow_as_ref), - } - } -} - -impl DocumentsBatchTransitionAccessorsV1 for BatchTransition { - type IterType<'a> - = DocumentBatchIteratorV1<'a> - where - Self: 'a; - - fn transitions_iter_v1(&self) -> Self::IterType<'_> { - match self { - Self::V0(transition) => DocumentBatchIteratorV1::V0(transition.transitions.iter()), - Self::V1(transition) => DocumentBatchIteratorV1::V1(transition.transitions.iter()), - Self::V2(transition) => DocumentBatchIteratorV1::V2(transition.transitions.iter()), - } - } - - fn transitions_len_v1(&self) -> usize { - match self { - Self::V0(transition) => transition.transitions.len(), - Self::V1(transition) => transition.transitions.len(), - Self::V2(transition) => transition.transitions.len(), - } - } - - fn transitions_are_empty_v1(&self) -> bool { - self.transitions_len_v1() == 0 - } - - fn first_transition_v1(&self) -> Option> { - self.transitions_iter_v1().next() - } - - fn first_transition_mut_v1(&mut self) -> Option> { - match self { - Self::V0(transition) => transition - .transitions - .first_mut() - .map(|transition| BatchedTransitionMutRefV1::Document(transition.into())), - Self::V1(transition) => transition.transitions.first_mut().map(Into::into), - Self::V2(transition) => transition - .transitions - .first_mut() - .map(BatchedTransitionV1::borrow_as_mut), - } - } - - fn contains_document_transition_v1(&self) -> bool { - self.transitions_iter_v1() - .any(|transition| matches!(transition, BatchedTransitionRefV1::Document(_))) - } - - fn contains_token_transition_v1(&self) -> bool { - self.transitions_iter_v1() - .any(|transition| matches!(transition, BatchedTransitionRefV1::Token(_))) - } -} - -impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV2 { - type IterType<'a> - = DocumentBatchIteratorV1<'a> - where - Self: 'a; - - fn transitions_iter_v1(&self) -> Self::IterType<'_> { - DocumentBatchIteratorV1::V2(self.transitions.iter()) - } - - fn transitions_len_v1(&self) -> usize { - self.transitions.len() - } - - fn transitions_are_empty_v1(&self) -> bool { - self.transitions.is_empty() - } - - fn first_transition_v1(&self) -> Option> { - self.transitions - .first() - .map(BatchedTransitionV1::borrow_as_ref) - } - - fn first_transition_mut_v1(&mut self) -> Option> { - self.transitions - .first_mut() - .map(BatchedTransitionV1::borrow_as_mut) - } - - fn contains_document_transition_v1(&self) -> bool { - self.transitions - .iter() - .any(|transition| matches!(transition, BatchedTransitionV1::Document(_))) - } - - fn contains_token_transition_v1(&self) -> bool { - self.transitions - .iter() - .any(|transition| matches!(transition, BatchedTransitionV1::Token(_))) - } -} - -impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV0 { - type IterType<'a> - = DocumentBatchIteratorV1<'a> - where - Self: 'a; - - fn transitions_iter_v1(&self) -> Self::IterType<'_> { - DocumentBatchIteratorV1::V0(self.transitions.iter()) - } - - fn transitions_len_v1(&self) -> usize { - self.transitions.len() - } - - fn transitions_are_empty_v1(&self) -> bool { - self.transitions.is_empty() - } - - fn first_transition_v1(&self) -> Option> { - self.transitions - .first() - .map(|transition| BatchedTransitionRefV1::Document(transition.into())) - } - - fn first_transition_mut_v1(&mut self) -> Option> { - self.transitions - .first_mut() - .map(|transition| BatchedTransitionMutRefV1::Document(transition.into())) - } - - fn contains_document_transition_v1(&self) -> bool { - !self.transitions.is_empty() - } - - fn contains_token_transition_v1(&self) -> bool { - false - } -} - -impl DocumentsBatchTransitionAccessorsV1 for BatchTransitionV1 { - type IterType<'a> - = DocumentBatchIteratorV1<'a> - where - Self: 'a; - - fn transitions_iter_v1(&self) -> Self::IterType<'_> { - DocumentBatchIteratorV1::V1(self.transitions.iter()) - } - - fn transitions_len_v1(&self) -> usize { - self.transitions.len() - } - - fn transitions_are_empty_v1(&self) -> bool { - self.transitions.is_empty() - } - - fn first_transition_v1(&self) -> Option> { - self.transitions.first().map(Into::into) - } - - fn first_transition_mut_v1(&mut self) -> Option> { - self.transitions.first_mut().map(Into::into) - } - - fn contains_document_transition_v1(&self) -> bool { - self.transitions - .iter() - .any(|transition| matches!(transition, BatchedTransition::Document(_))) - } - - fn contains_token_transition_v1(&self) -> bool { - self.transitions - .iter() - .any(|transition| matches!(transition, BatchedTransition::Token(_))) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index 1b9c13ddea9..ca6510dc0ca 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use bincode::{Encode, Decode, DecodeUntrusted}; use crate::prelude::{IdentityNonce, Revision}; use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; -use crate::state_transition::batch_transition::batched_transition::{DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; +use crate::state_transition::batch_transition::batched_transition::{DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; @@ -57,6 +57,13 @@ pub enum DocumentTransition { /// PV14+ (see the wire gate in `validate_base_structure_v0`). #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] IndexOnlyDelete(DocumentIndexOnlyDeleteTransition), + + /// The erase kind, which removes the retained revisions of a deleted + /// keep-history document — appended at the end so every existing variant + /// keeps its bincode discriminant. Only exists at protocol version 15 and + /// later (see the wire gate in `validate_base_structure_v0`). + #[display("EraseDocumentTransition({})", "_0")] + Erase(DocumentEraseTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -74,7 +81,7 @@ impl crate::serialization::ValueConvertible for DocumentTransition {} pub(crate) mod json_convertible_tests { use super::*; use crate::state_transition::batch_transition::batched_transition::{ - document_create_transition, document_delete_transition, + document_create_transition, document_delete_transition, document_erase_transition, document_index_only_delete_transition, document_purchase_transition, document_replace_transition, document_transfer_transition, document_update_price_transition, @@ -183,6 +190,14 @@ pub(crate) mod json_convertible_tests { "indexOnlyDelete", ); } + + #[test] + fn umbrella_erase() { + assert_umbrella_round_trip( + DocumentTransition::Erase(document_erase_transition::json_convertible_tests::fixture()), + "erase", + ); + } } impl BatchTransitionResolversV0 for DocumentTransition { @@ -225,6 +240,14 @@ impl BatchTransitionResolversV0 for DocumentTransition { } } + fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { + if let Self::Erase(ref t) = self { + Some(t) + } else { + None + } + } + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { None } @@ -325,6 +348,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base(), DocumentTransition::Purchase(t) => t.base(), DocumentTransition::IndexOnlyDelete(t) => t.base(), + DocumentTransition::Erase(t) => t.base(), } } @@ -337,6 +361,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => t.data().get(path), + DocumentTransition::Erase(_) => None, } } @@ -357,6 +382,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -373,6 +399,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data()), + DocumentTransition::Erase(_) => None, } } @@ -391,6 +418,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => Some(t.revision()), DocumentTransition::Purchase(t) => Some(t.revision()), DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -403,6 +431,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base().identity_contract_nonce(), DocumentTransition::Purchase(t) => t.base().identity_contract_nonce(), DocumentTransition::IndexOnlyDelete(t) => t.base().identity_contract_nonce(), + DocumentTransition::Erase(t) => t.base().identity_contract_nonce(), } } @@ -426,6 +455,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.data_mut().insert(property_name, value); } + DocumentTransition::Erase(_) => {} } } @@ -442,6 +472,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(t) => t.base_mut(), DocumentTransition::Purchase(t) => t.base_mut(), DocumentTransition::IndexOnlyDelete(t) => t.base_mut(), + DocumentTransition::Erase(t) => t.base_mut(), } } @@ -454,6 +485,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(t) => Some(t.data_mut()), + DocumentTransition::Erase(_) => None, } } @@ -466,6 +498,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::UpdatePrice(ref mut t) => t.set_revision(revision), DocumentTransition::Purchase(ref mut t) => t.set_revision(revision), DocumentTransition::IndexOnlyDelete(_) => {} + DocumentTransition::Erase(_) => {} } } @@ -480,6 +513,7 @@ impl DocumentTransitionV0Methods for DocumentTransition { DocumentTransition::IndexOnlyDelete(t) => { t.base_mut().set_identity_contract_nonce(nonce) } + DocumentTransition::Erase(t) => t.base_mut().set_identity_contract_nonce(nonce), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs index 74691b73421..b2a946706ee 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition_action_type.rs @@ -1,6 +1,3 @@ -use crate::state_transition::batch_transition::batched_transition::{ - DocumentTransitionRefV1, DocumentTransitionV1, -}; use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition; use crate::ProtocolError; @@ -32,29 +29,7 @@ impl DocumentTransitionActionTypeGetter for DocumentTransition { DocumentTransition::UpdatePrice(_) => DocumentTransitionActionType::UpdatePrice, DocumentTransition::Purchase(_) => DocumentTransitionActionType::Purchase, DocumentTransition::IndexOnlyDelete(_) => DocumentTransitionActionType::IndexOnlyDelete, - } - } -} - -impl DocumentTransitionActionTypeGetter for DocumentTransitionV1 { - fn action_type(&self) -> DocumentTransitionActionType { - self.borrow_as_ref().action_type() - } -} - -impl DocumentTransitionActionTypeGetter for DocumentTransitionRefV1<'_> { - fn action_type(&self) -> DocumentTransitionActionType { - match self { - DocumentTransitionRefV1::Create(_) => DocumentTransitionActionType::Create, - DocumentTransitionRefV1::Delete(_) => DocumentTransitionActionType::Delete, - DocumentTransitionRefV1::Replace(_) => DocumentTransitionActionType::Replace, - DocumentTransitionRefV1::Transfer(_) => DocumentTransitionActionType::Transfer, - DocumentTransitionRefV1::UpdatePrice(_) => DocumentTransitionActionType::UpdatePrice, - DocumentTransitionRefV1::Purchase(_) => DocumentTransitionActionType::Purchase, - DocumentTransitionRefV1::IndexOnlyDelete(_) => { - DocumentTransitionActionType::IndexOnlyDelete - } - DocumentTransitionRefV1::Erase(_) => DocumentTransitionActionType::Erase, + DocumentTransition::Erase(_) => DocumentTransitionActionType::Erase, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index 1dc2eb21c33..d2a0fa480d1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -30,7 +30,6 @@ pub mod token_transfer_transition; pub mod token_transition; pub mod token_transition_action_type; pub mod token_unfreeze_transition; -pub mod v1; use crate::prelude::IdentityNonce; use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; @@ -47,10 +46,6 @@ pub use document_transition::DocumentTransition; pub use document_update_price_transition::DocumentUpdatePriceTransition; use platform_value::Identifier; pub use token_transition::TokenTransition; -pub use v1::{ - BatchedTransitionMutRefV1, BatchedTransitionRefV1, BatchedTransitionV1, - DocumentTransitionMutRefV1, DocumentTransitionRefV1, DocumentTransitionV1, -}; pub const PROPERTY_ACTION: &str = "$action"; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs index d8dabc14ca0..423bce1c51a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs @@ -1,5 +1,6 @@ use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionRef, DocumentPurchaseTransition, DocumentTransferTransition, + BatchedTransition, BatchedTransitionRef, DocumentEraseTransition, DocumentPurchaseTransition, + DocumentTransferTransition, }; use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{ @@ -46,6 +47,13 @@ impl BatchTransitionResolversV0 for BatchedTransition { } } + fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { + match self { + BatchedTransition::Document(document) => document.as_transition_erase(), + BatchedTransition::Token(_) => None, + } + } + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { match self { BatchedTransition::Document(_) => None, @@ -166,6 +174,13 @@ impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { } } + fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { + match self { + BatchedTransitionRef::Document(document) => document.as_transition_erase(), + BatchedTransitionRef::Token(_) => None, + } + } + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { match self { BatchedTransitionRef::Document(_) => None, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index 0269ea0b610..4f119c32f42 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -19,7 +19,7 @@ use crate::document::Document; use crate::prelude::IdentityNonce; use crate::ProtocolError; use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenSetPriceForDirectPurchaseTransition}; -use crate::state_transition::batch_transition::batched_transition::{DocumentPurchaseTransition, DocumentTransferTransition}; +use crate::state_transition::batch_transition::batched_transition::{DocumentEraseTransition, DocumentPurchaseTransition, DocumentTransferTransition}; use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; @@ -264,6 +264,10 @@ impl BatchTransitionResolversV0 for TokenTransition { None } + fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { + None + } + fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { if let Self::Burn(ref t) = self { Some(t) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs deleted file mode 100644 index 215bbcc34fe..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/v1.rs +++ /dev/null @@ -1,944 +0,0 @@ -use bincode::{Decode, DecodeUntrusted, Encode}; -use derive_more::{Display, From}; -use platform_value::{Identifier, Value}; -#[cfg(feature = "serde-conversion")] -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; - -use crate::prelude::{IdentityNonce, Revision}; -use crate::ProtocolError; -use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, DocumentCreateTransition, DocumentDeleteTransition, - DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, - DocumentReplaceTransition, DocumentTransferTransition, DocumentTransition, - DocumentUpdatePriceTransition, TokenTransition, -}; -use crate::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; -use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; -use crate::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use crate::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; -use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; -use crate::state_transition::batch_transition::{ - TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, - TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, - TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, - TokenSetPriceForDirectPurchaseTransition, TokenTransferTransition, TokenUnfreezeTransition, -}; - -/// The document transition shell carried by batch transition format 2. -/// -/// Batch formats 0 and 1 carry [`DocumentTransition`], whose wire discriminants -/// end at the indexOnly delete kind. Format 2 owns this shell so that the erase -/// kind can exist without changing what a format 0 or 1 batch can decode: -/// software that only knows the older formats keeps rejecting an erase at -/// decode time, exactly as software that predates the kind does. The concrete -/// per-kind payloads are shared with [`DocumentTransition`]; only the two shell -/// enums differ. -#[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display, DecodeUntrusted)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - // The same `$action` discriminator and variant names as - // `DocumentTransition`, so a format 2 batch has the same external shape - // for every kind the older formats already carried. - serde(tag = "$action", rename_all = "camelCase") -)] -pub enum DocumentTransitionV1 { - #[display("CreateDocumentTransition({})", "_0")] - Create(DocumentCreateTransition), - - #[display("ReplaceDocumentTransition({})", "_0")] - Replace(DocumentReplaceTransition), - - #[display("DeleteDocumentTransition({})", "_0")] - Delete(DocumentDeleteTransition), - - #[display("TransferDocumentTransition({})", "_0")] - Transfer(DocumentTransferTransition), - - #[display("UpdatePriceDocumentTransition({})", "_0")] - UpdatePrice(DocumentUpdatePriceTransition), - - #[display("PurchaseDocumentTransition({})", "_0")] - Purchase(DocumentPurchaseTransition), - - #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] - IndexOnlyDelete(DocumentIndexOnlyDeleteTransition), - - /// The erase kind, which purges the retained revisions of a deleted - /// keep-history document. It only exists in this shell. - #[display("EraseDocumentTransition({})", "_0")] - Erase(DocumentEraseTransition), -} - -#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] -impl crate::serialization::JsonConvertible for DocumentTransitionV1 {} - -#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] -impl crate::serialization::ValueConvertible for DocumentTransitionV1 {} - -impl From for DocumentTransitionV1 { - fn from(value: DocumentTransition) -> Self { - match value { - DocumentTransition::Create(transition) => Self::Create(transition), - DocumentTransition::Replace(transition) => Self::Replace(transition), - DocumentTransition::Delete(transition) => Self::Delete(transition), - DocumentTransition::Transfer(transition) => Self::Transfer(transition), - DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), - DocumentTransition::Purchase(transition) => Self::Purchase(transition), - DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), - } - } -} - -impl<'a> From<&'a DocumentTransition> for DocumentTransitionRefV1<'a> { - fn from(value: &'a DocumentTransition) -> Self { - match value { - DocumentTransition::Create(transition) => Self::Create(transition), - DocumentTransition::Replace(transition) => Self::Replace(transition), - DocumentTransition::Delete(transition) => Self::Delete(transition), - DocumentTransition::Transfer(transition) => Self::Transfer(transition), - DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), - DocumentTransition::Purchase(transition) => Self::Purchase(transition), - DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), - } - } -} - -impl<'a> From<&'a mut DocumentTransition> for DocumentTransitionMutRefV1<'a> { - fn from(value: &'a mut DocumentTransition) -> Self { - match value { - DocumentTransition::Create(transition) => Self::Create(transition), - DocumentTransition::Replace(transition) => Self::Replace(transition), - DocumentTransition::Delete(transition) => Self::Delete(transition), - DocumentTransition::Transfer(transition) => Self::Transfer(transition), - DocumentTransition::UpdatePrice(transition) => Self::UpdatePrice(transition), - DocumentTransition::Purchase(transition) => Self::Purchase(transition), - DocumentTransition::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), - } - } -} - -impl TryFrom for DocumentTransition { - type Error = ProtocolError; - - /// Fails for an erase: no batch format before 2 can carry one. - fn try_from(value: DocumentTransitionV1) -> Result { - Ok(match value { - DocumentTransitionV1::Create(transition) => Self::Create(transition), - DocumentTransitionV1::Replace(transition) => Self::Replace(transition), - DocumentTransitionV1::Delete(transition) => Self::Delete(transition), - DocumentTransitionV1::Transfer(transition) => Self::Transfer(transition), - DocumentTransitionV1::UpdatePrice(transition) => Self::UpdatePrice(transition), - DocumentTransitionV1::Purchase(transition) => Self::Purchase(transition), - DocumentTransitionV1::IndexOnlyDelete(transition) => Self::IndexOnlyDelete(transition), - DocumentTransitionV1::Erase(_) => { - return Err(ProtocolError::Generic( - "an erase transition only exists in batch transition format 2".to_string(), - )) - } - }) - } -} - -/// The batched transition shell carried by batch transition format 2. -/// -/// It differs from [`BatchedTransition`] only in the document shell it wraps, -/// which is the one that knows the erase kind. -#[derive(Debug, Clone, Encode, Decode, From, PartialEq, Display, DecodeUntrusted)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - // The same `$transition` discriminator as `BatchedTransition`, so the - // external shape of a format 2 batch matches the older formats. - serde(tag = "$transition", rename_all = "camelCase") -)] -pub enum BatchedTransitionV1 { - #[display("DocumentTransition({})", "_0")] - Document(DocumentTransitionV1), - #[display("TokenTransition({})", "_0")] - Token(TokenTransition), -} - -#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] -impl crate::serialization::JsonConvertible for BatchedTransitionV1 {} - -#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] -impl crate::serialization::ValueConvertible for BatchedTransitionV1 {} - -impl From for BatchedTransitionV1 { - fn from(value: BatchedTransition) -> Self { - match value { - BatchedTransition::Document(transition) => Self::Document(transition.into()), - BatchedTransition::Token(transition) => Self::Token(transition), - } - } -} - -impl<'a> From<&'a BatchedTransition> for BatchedTransitionRefV1<'a> { - fn from(value: &'a BatchedTransition) -> Self { - match value { - BatchedTransition::Document(transition) => Self::Document(transition.into()), - BatchedTransition::Token(transition) => Self::Token(transition), - } - } -} - -impl<'a> From<&'a mut BatchedTransition> for BatchedTransitionMutRefV1<'a> { - fn from(value: &'a mut BatchedTransition) -> Self { - match value { - BatchedTransition::Document(transition) => Self::Document(transition.into()), - BatchedTransition::Token(transition) => Self::Token(transition), - } - } -} - -impl TryFrom for BatchedTransition { - type Error = ProtocolError; - - /// Fails for an erase: no batch format before 2 can carry one. - fn try_from(value: BatchedTransitionV1) -> Result { - Ok(match value { - BatchedTransitionV1::Document(transition) => Self::Document(transition.try_into()?), - BatchedTransitionV1::Token(transition) => Self::Token(transition), - }) - } -} - -/// A borrowed view of one document transition of any batch format. -/// -/// Every batch format can be viewed through it: formats 0 and 1 convert their -/// [`DocumentTransition`] items, format 2 borrows its own shell. -#[derive(Debug, Clone, Copy, PartialEq, Display)] -pub enum DocumentTransitionRefV1<'a> { - #[display("CreateDocumentTransition({})", "_0")] - Create(&'a DocumentCreateTransition), - #[display("ReplaceDocumentTransition({})", "_0")] - Replace(&'a DocumentReplaceTransition), - #[display("DeleteDocumentTransition({})", "_0")] - Delete(&'a DocumentDeleteTransition), - #[display("TransferDocumentTransition({})", "_0")] - Transfer(&'a DocumentTransferTransition), - #[display("UpdatePriceDocumentTransition({})", "_0")] - UpdatePrice(&'a DocumentUpdatePriceTransition), - #[display("PurchaseDocumentTransition({})", "_0")] - Purchase(&'a DocumentPurchaseTransition), - #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] - IndexOnlyDelete(&'a DocumentIndexOnlyDeleteTransition), - #[display("EraseDocumentTransition({})", "_0")] - Erase(&'a DocumentEraseTransition), -} - -/// A mutable view of one document transition of any batch format. -#[derive(Debug, PartialEq, Display)] -pub enum DocumentTransitionMutRefV1<'a> { - #[display("CreateDocumentTransition({})", "_0")] - Create(&'a mut DocumentCreateTransition), - #[display("ReplaceDocumentTransition({})", "_0")] - Replace(&'a mut DocumentReplaceTransition), - #[display("DeleteDocumentTransition({})", "_0")] - Delete(&'a mut DocumentDeleteTransition), - #[display("TransferDocumentTransition({})", "_0")] - Transfer(&'a mut DocumentTransferTransition), - #[display("UpdatePriceDocumentTransition({})", "_0")] - UpdatePrice(&'a mut DocumentUpdatePriceTransition), - #[display("PurchaseDocumentTransition({})", "_0")] - Purchase(&'a mut DocumentPurchaseTransition), - #[display("IndexOnlyDeleteDocumentTransition({})", "_0")] - IndexOnlyDelete(&'a mut DocumentIndexOnlyDeleteTransition), - #[display("EraseDocumentTransition({})", "_0")] - Erase(&'a mut DocumentEraseTransition), -} - -impl DocumentTransitionV1 { - pub fn borrow_as_ref(&self) -> DocumentTransitionRefV1<'_> { - match self { - Self::Create(transition) => DocumentTransitionRefV1::Create(transition), - Self::Replace(transition) => DocumentTransitionRefV1::Replace(transition), - Self::Delete(transition) => DocumentTransitionRefV1::Delete(transition), - Self::Transfer(transition) => DocumentTransitionRefV1::Transfer(transition), - Self::UpdatePrice(transition) => DocumentTransitionRefV1::UpdatePrice(transition), - Self::Purchase(transition) => DocumentTransitionRefV1::Purchase(transition), - Self::IndexOnlyDelete(transition) => { - DocumentTransitionRefV1::IndexOnlyDelete(transition) - } - Self::Erase(transition) => DocumentTransitionRefV1::Erase(transition), - } - } - - pub fn borrow_as_mut(&mut self) -> DocumentTransitionMutRefV1<'_> { - match self { - Self::Create(transition) => DocumentTransitionMutRefV1::Create(transition), - Self::Replace(transition) => DocumentTransitionMutRefV1::Replace(transition), - Self::Delete(transition) => DocumentTransitionMutRefV1::Delete(transition), - Self::Transfer(transition) => DocumentTransitionMutRefV1::Transfer(transition), - Self::UpdatePrice(transition) => DocumentTransitionMutRefV1::UpdatePrice(transition), - Self::Purchase(transition) => DocumentTransitionMutRefV1::Purchase(transition), - Self::IndexOnlyDelete(transition) => { - DocumentTransitionMutRefV1::IndexOnlyDelete(transition) - } - Self::Erase(transition) => DocumentTransitionMutRefV1::Erase(transition), - } - } - - /// The erase transition, when this is one. - pub fn as_transition_erase(&self) -> Option<&DocumentEraseTransition> { - match self { - Self::Erase(transition) => Some(transition), - _ => None, - } - } -} - -/// A borrowed view of one batched transition of any batch format. -#[derive(Debug, Clone, Copy, PartialEq, Display)] -pub enum BatchedTransitionRefV1<'a> { - #[display("DocumentTransition({})", "_0")] - Document(DocumentTransitionRefV1<'a>), - #[display("TokenTransition({})", "_0")] - Token(&'a TokenTransition), -} - -/// A mutable view of one batched transition of any batch format. -#[derive(Debug, PartialEq, Display)] -pub enum BatchedTransitionMutRefV1<'a> { - #[display("DocumentTransition({})", "_0")] - Document(DocumentTransitionMutRefV1<'a>), - #[display("TokenTransition({})", "_0")] - Token(&'a mut TokenTransition), -} - -impl BatchedTransitionV1 { - pub fn borrow_as_ref(&self) -> BatchedTransitionRefV1<'_> { - match self { - Self::Document(transition) => { - BatchedTransitionRefV1::Document(transition.borrow_as_ref()) - } - Self::Token(transition) => BatchedTransitionRefV1::Token(transition), - } - } - - pub fn borrow_as_mut(&mut self) -> BatchedTransitionMutRefV1<'_> { - match self { - Self::Document(transition) => { - BatchedTransitionMutRefV1::Document(transition.borrow_as_mut()) - } - Self::Token(transition) => BatchedTransitionMutRefV1::Token(transition), - } - } - - pub fn set_identity_contract_nonce(&mut self, identity_contract_nonce: IdentityNonce) { - match self { - Self::Document(transition) => { - transition.set_identity_contract_nonce(identity_contract_nonce) - } - Self::Token(transition) => { - transition.set_identity_contract_nonce(identity_contract_nonce) - } - } - } -} - -impl<'a> BatchedTransitionRefV1<'a> { - pub fn to_owned_transition(&self) -> BatchedTransitionV1 { - match self { - Self::Document(transition) => { - BatchedTransitionV1::Document(transition.to_owned_transition()) - } - Self::Token(transition) => BatchedTransitionV1::Token((*transition).clone()), - } - } - - pub fn identity_contract_nonce(&self) -> IdentityNonce { - match self { - Self::Document(transition) => transition.identity_contract_nonce(), - Self::Token(transition) => transition.identity_contract_nonce(), - } - } - - pub fn data_contract_id(&self) -> Identifier { - match self { - Self::Document(transition) => transition.data_contract_id(), - Self::Token(transition) => transition.data_contract_id(), - } - } - - /// The erase transition, when this is one. - pub fn as_transition_erase(&self) -> Option<&'a DocumentEraseTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Erase(transition)) => Some(transition), - _ => None, - } - } -} - -impl<'a> DocumentTransitionRefV1<'a> { - pub fn to_owned_transition(&self) -> DocumentTransitionV1 { - match self { - Self::Create(transition) => DocumentTransitionV1::Create((*transition).clone()), - Self::Replace(transition) => DocumentTransitionV1::Replace((*transition).clone()), - Self::Delete(transition) => DocumentTransitionV1::Delete((*transition).clone()), - Self::Transfer(transition) => DocumentTransitionV1::Transfer((*transition).clone()), - Self::UpdatePrice(transition) => { - DocumentTransitionV1::UpdatePrice((*transition).clone()) - } - Self::Purchase(transition) => DocumentTransitionV1::Purchase((*transition).clone()), - Self::IndexOnlyDelete(transition) => { - DocumentTransitionV1::IndexOnlyDelete((*transition).clone()) - } - Self::Erase(transition) => DocumentTransitionV1::Erase((*transition).clone()), - } - } - - pub fn base(&self) -> &'a DocumentBaseTransition { - match self { - Self::Create(transition) => transition.base(), - Self::Replace(transition) => transition.base(), - Self::Delete(transition) => transition.base(), - Self::Transfer(transition) => transition.base(), - Self::UpdatePrice(transition) => transition.base(), - Self::Purchase(transition) => transition.base(), - Self::IndexOnlyDelete(transition) => transition.base(), - Self::Erase(transition) => transition.base(), - } - } - - pub fn get_id(&self) -> Identifier { - self.base().id() - } - - pub fn document_type_name(&self) -> &'a String { - self.base().document_type_name() - } - - pub fn data_contract_id(&self) -> Identifier { - self.base().data_contract_id() - } - - pub fn identity_contract_nonce(&self) -> IdentityNonce { - self.base().identity_contract_nonce() - } - - pub fn entropy(&self) -> Option> { - match self { - Self::Create(transition) => Some(Vec::from(transition.entropy())), - _ => None, - } - } - - /// The document data the transition carries, when its kind carries any. - pub fn data(&self) -> Option<&'a BTreeMap> { - match self { - Self::Create(transition) => Some(transition.data()), - Self::Replace(transition) => Some(transition.data()), - Self::IndexOnlyDelete(transition) => Some(transition.data()), - Self::Delete(_) - | Self::Transfer(_) - | Self::UpdatePrice(_) - | Self::Purchase(_) - | Self::Erase(_) => None, - } - } - - pub fn get_dynamic_property(&self, path: &str) -> Option<&'a Value> { - self.data()?.get(path) - } - - pub fn first_data_depth_exceeding(&self, max_depth: usize) -> Option { - self.data()? - .values() - .find_map(|value| value.first_depth_exceeding(max_depth)) - } - - pub fn revision(&self) -> Option { - match self { - Self::Create(_) => Some(1), - Self::Replace(transition) => Some(transition.revision()), - Self::Transfer(transition) => Some(transition.revision()), - Self::UpdatePrice(transition) => Some(transition.revision()), - Self::Purchase(transition) => Some(transition.revision()), - Self::Delete(_) | Self::IndexOnlyDelete(_) | Self::Erase(_) => None, - } - } - - pub fn as_transition_create(&self) -> Option<&'a DocumentCreateTransition> { - match self { - Self::Create(transition) => Some(transition), - _ => None, - } - } - - pub fn as_transition_replace(&self) -> Option<&'a DocumentReplaceTransition> { - match self { - Self::Replace(transition) => Some(transition), - _ => None, - } - } - - pub fn as_transition_delete(&self) -> Option<&'a DocumentDeleteTransition> { - match self { - Self::Delete(transition) => Some(transition), - _ => None, - } - } - - pub fn as_transition_transfer(&self) -> Option<&'a DocumentTransferTransition> { - match self { - Self::Transfer(transition) => Some(transition), - _ => None, - } - } - - pub fn as_transition_purchase(&self) -> Option<&'a DocumentPurchaseTransition> { - match self { - Self::Purchase(transition) => Some(transition), - _ => None, - } - } - - pub fn as_transition_erase(&self) -> Option<&'a DocumentEraseTransition> { - match self { - Self::Erase(transition) => Some(transition), - _ => None, - } - } -} - -impl DocumentTransitionV0Methods for DocumentTransitionV1 { - fn base(&self) -> &DocumentBaseTransition { - match self { - Self::Create(transition) => transition.base(), - Self::Replace(transition) => transition.base(), - Self::Delete(transition) => transition.base(), - Self::Transfer(transition) => transition.base(), - Self::UpdatePrice(transition) => transition.base(), - Self::Purchase(transition) => transition.base(), - Self::IndexOnlyDelete(transition) => transition.base(), - Self::Erase(transition) => transition.base(), - } - } - - fn get_dynamic_property(&self, path: &str) -> Option<&Value> { - self.data()?.get(path) - } - - fn get_id(&self) -> Identifier { - self.base().id() - } - - fn entropy(&self) -> Option> { - match self { - Self::Create(transition) => Some(Vec::from(transition.entropy())), - _ => None, - } - } - - fn document_type_name(&self) -> &String { - self.base().document_type_name() - } - - fn data_contract_id(&self) -> Identifier { - self.base().data_contract_id() - } - - fn data(&self) -> Option<&BTreeMap> { - match self { - Self::Create(transition) => Some(transition.data()), - Self::Replace(transition) => Some(transition.data()), - Self::IndexOnlyDelete(transition) => Some(transition.data()), - Self::Delete(_) - | Self::Transfer(_) - | Self::UpdatePrice(_) - | Self::Purchase(_) - | Self::Erase(_) => None, - } - } - - fn first_data_depth_exceeding(&self, max_depth: usize) -> Option { - self.borrow_as_ref().first_data_depth_exceeding(max_depth) - } - - fn revision(&self) -> Option { - self.borrow_as_ref().revision() - } - - fn identity_contract_nonce(&self) -> IdentityNonce { - self.base().identity_contract_nonce() - } - - #[cfg(test)] - fn insert_dynamic_property(&mut self, property_name: String, value: Value) { - if let Some(data) = self.data_mut() { - data.insert(property_name, value); - } - } - - fn set_data_contract_id(&mut self, id: Identifier) { - self.base_mut().set_data_contract_id(id) - } - - fn base_mut(&mut self) -> &mut DocumentBaseTransition { - match self { - Self::Create(transition) => transition.base_mut(), - Self::Replace(transition) => transition.base_mut(), - Self::Delete(transition) => transition.base_mut(), - Self::Transfer(transition) => transition.base_mut(), - Self::UpdatePrice(transition) => transition.base_mut(), - Self::Purchase(transition) => transition.base_mut(), - Self::IndexOnlyDelete(transition) => transition.base_mut(), - Self::Erase(transition) => transition.base_mut(), - } - } - - fn data_mut(&mut self) -> Option<&mut BTreeMap> { - match self { - Self::Create(transition) => Some(transition.data_mut()), - Self::Replace(transition) => Some(transition.data_mut()), - Self::IndexOnlyDelete(transition) => Some(transition.data_mut()), - Self::Delete(_) - | Self::Transfer(_) - | Self::UpdatePrice(_) - | Self::Purchase(_) - | Self::Erase(_) => None, - } - } - - fn set_revision(&mut self, revision: Revision) { - match self { - Self::Replace(transition) => transition.set_revision(revision), - Self::Transfer(transition) => transition.set_revision(revision), - Self::UpdatePrice(transition) => transition.set_revision(revision), - Self::Purchase(transition) => transition.set_revision(revision), - Self::Create(_) | Self::Delete(_) | Self::IndexOnlyDelete(_) | Self::Erase(_) => {} - } - } - - fn set_identity_contract_nonce(&mut self, nonce: IdentityNonce) { - self.base_mut().set_identity_contract_nonce(nonce) - } -} - -impl BatchTransitionResolversV0 for BatchedTransitionV1 { - fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { - match self { - Self::Document(DocumentTransitionV1::Create(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_replace(&self) -> Option<&DocumentReplaceTransition> { - match self { - Self::Document(DocumentTransitionV1::Replace(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_delete(&self) -> Option<&DocumentDeleteTransition> { - match self { - Self::Document(DocumentTransitionV1::Delete(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_transfer(&self) -> Option<&DocumentTransferTransition> { - match self { - Self::Document(DocumentTransitionV1::Transfer(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_purchase(&self) -> Option<&DocumentPurchaseTransition> { - match self { - Self::Document(DocumentTransitionV1::Purchase(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_burn(), - } - } - - fn as_transition_token_mint(&self) -> Option<&TokenMintTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_mint(), - } - } - - fn as_transition_token_transfer(&self) -> Option<&TokenTransferTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_transfer(), - } - } - - fn as_transition_token_freeze(&self) -> Option<&TokenFreezeTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_freeze(), - } - } - - fn as_transition_token_unfreeze(&self) -> Option<&TokenUnfreezeTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_unfreeze(), - } - } - - fn as_transition_token_destroy_frozen_funds( - &self, - ) -> Option<&TokenDestroyFrozenFundsTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_destroy_frozen_funds(), - } - } - - fn as_transition_token_claim(&self) -> Option<&TokenClaimTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_claim(), - } - } - - fn as_transition_token_emergency_action(&self) -> Option<&TokenEmergencyActionTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_emergency_action(), - } - } - - fn as_transition_token_config_update(&self) -> Option<&TokenConfigUpdateTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_config_update(), - } - } - - fn as_transition_token_direct_purchase(&self) -> Option<&TokenDirectPurchaseTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_direct_purchase(), - } - } - - fn as_transition_token_set_price_for_direct_purchase( - &self, - ) -> Option<&TokenSetPriceForDirectPurchaseTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_set_price_for_direct_purchase(), - } - } -} - -impl BatchTransitionResolversV0 for BatchedTransitionRefV1<'_> { - fn as_transition_create(&self) -> Option<&DocumentCreateTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Create(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_replace(&self) -> Option<&DocumentReplaceTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Replace(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_delete(&self) -> Option<&DocumentDeleteTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Delete(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_transfer(&self) -> Option<&DocumentTransferTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Transfer(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_purchase(&self) -> Option<&DocumentPurchaseTransition> { - match self { - Self::Document(DocumentTransitionRefV1::Purchase(transition)) => Some(transition), - _ => None, - } - } - - fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_burn(), - } - } - - fn as_transition_token_mint(&self) -> Option<&TokenMintTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_mint(), - } - } - - fn as_transition_token_transfer(&self) -> Option<&TokenTransferTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_transfer(), - } - } - - fn as_transition_token_freeze(&self) -> Option<&TokenFreezeTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_freeze(), - } - } - - fn as_transition_token_unfreeze(&self) -> Option<&TokenUnfreezeTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_unfreeze(), - } - } - - fn as_transition_token_destroy_frozen_funds( - &self, - ) -> Option<&TokenDestroyFrozenFundsTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_destroy_frozen_funds(), - } - } - - fn as_transition_token_claim(&self) -> Option<&TokenClaimTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_claim(), - } - } - - fn as_transition_token_emergency_action(&self) -> Option<&TokenEmergencyActionTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_emergency_action(), - } - } - - fn as_transition_token_config_update(&self) -> Option<&TokenConfigUpdateTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_config_update(), - } - } - - fn as_transition_token_direct_purchase(&self) -> Option<&TokenDirectPurchaseTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_direct_purchase(), - } - } - - fn as_transition_token_set_price_for_direct_purchase( - &self, - ) -> Option<&TokenSetPriceForDirectPurchaseTransition> { - match self { - Self::Document(_) => None, - Self::Token(token) => token.as_transition_token_set_price_for_direct_purchase(), - } - } -} - -#[cfg(all( - test, - feature = "json-conversion", - feature = "value-conversion", - feature = "serde-conversion" -))] -mod json_convertible_tests { - use super::*; - use crate::serialization::{JsonConvertible, ValueConvertible}; - use crate::state_transition::batch_transition::batched_transition::{ - document_create_transition, document_erase_transition, - }; - - fn assert_document_round_trip(transition: DocumentTransitionV1, expected_action: &str) { - let json = transition.to_json().expect("to_json"); - let json_obj = json.as_object().expect("json object"); - assert_eq!( - json_obj.get("$action").and_then(|v| v.as_str()), - Some(expected_action), - "json `$action` discriminator mismatch" - ); - let recovered_json = DocumentTransitionV1::from_json(json).expect("from_json"); - assert_eq!(transition, recovered_json); - - let value = transition.to_object().expect("to_object"); - let recovered_value = DocumentTransitionV1::from_object(value).expect("from_object"); - assert_eq!(transition, recovered_value); - } - - #[test] - fn should_keep_the_external_shape_of_a_legacy_kind_in_the_format_2_shell() { - let create = document_create_transition::json_convertible_tests::fixture(); - let legacy = DocumentTransition::Create(create.clone()); - let format_2 = DocumentTransitionV1::Create(create); - - assert_eq!( - legacy.to_json().expect("legacy to_json"), - format_2.to_json().expect("format 2 to_json"), - "a kind shared by every batch format must serialize identically" - ); - assert_document_round_trip(format_2, "create"); - } - - #[test] - fn should_round_trip_an_erase_through_json_and_value() { - let erase = document_erase_transition::json_convertible_tests::fixture(); - assert_document_round_trip(DocumentTransitionV1::Erase(erase), "erase"); - } - - #[test] - fn should_tag_the_batched_shell_with_the_transition_kind() { - let erase = document_erase_transition::json_convertible_tests::fixture(); - let batched = BatchedTransitionV1::Document(DocumentTransitionV1::Erase(erase)); - - let json = batched.to_json().expect("to_json"); - let json_obj = json.as_object().expect("json object"); - assert_eq!( - json_obj.get("$transition").and_then(|v| v.as_str()), - Some("document") - ); - assert_eq!( - json_obj.get("$action").and_then(|v| v.as_str()), - Some("erase") - ); - let recovered = BatchedTransitionV1::from_json(json).expect("from_json"); - assert_eq!(batched, recovered); - } - - #[test] - fn should_refuse_an_erase_in_the_shell_of_the_older_batch_formats() { - let erase = document_erase_transition::json_convertible_tests::fixture(); - let json = DocumentTransitionV1::Erase(erase) - .to_json() - .expect("to_json"); - - assert!( - DocumentTransition::from_json(json).is_err(), - "the shell used by batch formats 0 and 1 has no erase kind" - ); - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs index 0061ce2f174..549bc5c955c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/identity_signed.rs @@ -7,7 +7,6 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.signature_public_key_id(), BatchTransition::V1(transition) => transition.signature_public_key_id(), - BatchTransition::V2(transition) => transition.signature_public_key_id(), } } @@ -15,7 +14,6 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature_public_key_id(key_id), BatchTransition::V1(transition) => transition.set_signature_public_key_id(key_id), - BatchTransition::V2(transition) => transition.set_signature_public_key_id(key_id), } } @@ -23,7 +21,6 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.security_level_requirement(purpose), BatchTransition::V1(transition) => transition.security_level_requirement(purpose), - BatchTransition::V2(transition) => transition.security_level_requirement(purpose), } } @@ -31,7 +28,6 @@ impl StateTransitionIdentitySigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.purpose_requirement(), BatchTransition::V1(transition) => transition.purpose_requirement(), - BatchTransition::V2(transition) => transition.purpose_requirement(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index 01e2b9872d9..5e9acff9b05 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -23,12 +23,9 @@ use crate::prelude::UserFeeIncrease; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; -use crate::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; use crate::state_transition::batch_transition::BatchTransition; #[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{ - BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, -}; +use crate::state_transition::batch_transition::{BatchTransitionV0, BatchTransitionV1}; #[cfg(feature = "state-transition-signing")] use crate::state_transition::StateTransition; use crate::state_transition::StateTransitionSigningOptions; @@ -49,7 +46,6 @@ use platform_version::version::PlatformVersion; pub mod v0; pub mod v1; -pub mod v2; #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] pub struct StateTransitionCreationOptions { @@ -65,7 +61,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.all_document_purchases_amount(), BatchTransition::V1(v1) => v1.all_document_purchases_amount(), - BatchTransition::V2(v2) => v2.all_document_purchases_amount(), } } @@ -75,7 +70,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.all_conflicting_index_collateral_voting_funds(), BatchTransition::V1(v1) => v1.all_conflicting_index_collateral_voting_funds(), - BatchTransition::V2(v2) => v2.all_conflicting_index_collateral_voting_funds(), } } @@ -83,7 +77,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.set_transitions(transitions), BatchTransition::V1(v1) => v1.set_transitions(transitions), - BatchTransition::V2(v2) => v2.set_transitions(transitions), } } @@ -91,7 +84,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { match self { BatchTransition::V0(v0) => v0.set_identity_contract_nonce(identity_contract_nonce), BatchTransition::V1(v1) => v1.set_identity_contract_nonce(identity_contract_nonce), - BatchTransition::V2(v2) => v2.set_identity_contract_nonce(identity_contract_nonce), } } @@ -131,7 +123,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_creation_transition_from_document( document, @@ -147,24 +138,9 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_creation_transition_from_document( - document, - document_type, - entropy, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - token_payment_info, - signer, - platform_version, - options, - ) - .await?, - ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_created_from_document".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, }), } @@ -204,7 +180,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_replacement_transition_from_document( document, @@ -219,14 +194,56 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_replacement_transition_from_document( + version => Err(ProtocolError::UnknownVersionMismatch { + method: + "DocumentsBatchTransition::new_document_replacement_transition_from_document" + .to_string(), + known_versions: vec![0, 1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 0 => Ok( + BatchTransitionV0::new_document_erase_transition_from_document( + document, + document_type, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await?, + ), + 1 => Ok( + BatchTransitionV1::new_document_erase_transition_from_document( document, document_type, identity_public_key, identity_contract_nonce, user_fee_increase, - token_payment_info, signer, platform_version, options, @@ -234,10 +251,9 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { .await?, ), version => Err(ProtocolError::UnknownVersionMismatch { - method: - "DocumentsBatchTransition::new_document_replacement_transition_from_document" - .to_string(), - known_versions: vec![0, 1, 2], + method: "DocumentsBatchTransition::new_document_erase_transition_from_document" + .to_string(), + known_versions: vec![0, 1], received: version, }), } @@ -280,7 +296,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_transfer_transition_from_document( document, @@ -296,26 +311,11 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_transfer_transition_from_document( - document, - document_type, - recipient_owner_id, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - token_payment_info, - signer, - platform_version, - options, - ) - .await?, - ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_replacement_transition_from_document" .to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, }), } @@ -355,7 +355,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_deletion_transition_from_document( document, @@ -370,24 +369,10 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_deletion_transition_from_document( - document, - document_type, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - token_payment_info, - signer, - platform_version, - options, - ) - .await?, - ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_deletion_transition_from_document" .to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, }), } @@ -429,7 +414,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_update_price_transition_from_document( document, @@ -445,26 +429,11 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_update_price_transition_from_document( - document, - document_type, - price, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - token_payment_info, - signer, - platform_version, - options, - ) - .await?, - ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_update_price_transition_from_document" .to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, }), } @@ -508,7 +477,6 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 1 => Ok( BatchTransitionV1::new_document_purchase_transition_from_document( document, @@ -525,70 +493,10 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransition { ) .await?, ), - 2 => Ok( - BatchTransitionV2::new_document_purchase_transition_from_document( - document, - document_type, - new_owner_id, - price, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - token_payment_info, - signer, - platform_version, - options, - ) - .await?, - ), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_document_purchase_transition_from_document" .to_string(), - known_versions: vec![0, 1, 2], - received: version, - }), - } - } -} - -impl DocumentsBatchTransitionMethodsV2 for BatchTransition { - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - options: Option, - platform_version: &PlatformVersion, - ) -> Result { - let batch_feature_version = options.unwrap_or_default().batch_feature_version.unwrap_or( - platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .default_current_version, - ); - match batch_feature_version { - 2 => { - BatchTransitionV2::new_document_erase_transition_from_document( - document, - document_type, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - options, - platform_version, - ) - .await - } - version => Err(ProtocolError::UnknownVersionMismatch { - method: "DocumentsBatchTransition::new_document_erase_transition_from_document" - .to_string(), - known_versions: vec![2], + known_versions: vec![0, 1], received: version, }), } @@ -621,31 +529,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - BatchTransitionV2::new_token_mint_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - amount, - issued_to_identity_id, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -674,7 +557,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_mint_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -704,30 +587,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - BatchTransitionV2::new_token_burn_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - amount, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -755,7 +614,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_burn_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -787,33 +646,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the transfer transition for batch version 1 - BatchTransitionV2::new_token_transfer_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - amount, - recipient_id, - public_note, - shared_encrypted_note, - private_encrypted_note, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -844,7 +676,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_transfer_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -874,31 +706,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the freeze transition for batch version 1 - BatchTransitionV2::new_token_freeze_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - freeze_identity_id, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -927,7 +734,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_freeze_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -957,31 +764,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the freeze transition for batch version 1 - BatchTransitionV2::new_token_unfreeze_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - unfreeze_identity_id, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1010,7 +792,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_unfreeze_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1040,31 +822,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the destroy frozen funds transition for batch version 1 - BatchTransitionV2::new_token_destroy_frozen_funds_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - frozen_identity_id, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1094,7 +851,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_destroy_frozen_funds_transition" .to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1124,31 +881,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the emergency action transition for batch version 1 - BatchTransitionV2::new_token_emergency_action_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - emergency_action, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1178,7 +910,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_emergency_action_transition" .to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1208,31 +940,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the emergency action transition for batch version 1 - BatchTransitionV2::new_token_config_update_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - update_token_configuration_item, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1261,7 +968,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_config_update_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1290,30 +997,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the emergency action transition for batch version 1 - BatchTransitionV2::new_token_claim_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - distribution_type, - public_note, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1341,7 +1024,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_claim_transition".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1372,31 +1055,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the emergency action transition for batch version 1 - BatchTransitionV2::new_token_change_direct_purchase_price_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - token_pricing_schedule, - public_note, - using_group_info, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1427,7 +1085,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { method: "DocumentsBatchTransition::new_token_change_direct_purchase_price_transition" .to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } @@ -1456,30 +1114,6 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { .batch_state_transition .default_current_version, ) { - 2 if platform_version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version - >= 2 => - { - // Create the emergency action transition for batch version 1 - BatchTransitionV2::new_token_direct_purchase_transition( - token_id, - owner_id, - data_contract_id, - token_contract_position, - amount, - total_agreed_price, - identity_public_key, - identity_contract_nonce, - user_fee_increase, - signer, - platform_version, - options, - ) - .await - } 1 | 0 if platform_version .dpp @@ -1508,7 +1142,7 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::new_token_direct_purchase_transition" .to_string(), - known_versions: vec![1, 2], + known_versions: vec![1], received: version, }), } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs index 2e1caa9d08d..60ad2b2a32c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v0/mod.rs @@ -11,29 +11,23 @@ use crate::identity::SecurityLevel; use crate::prelude::IdentityNonce; #[cfg(feature = "state-transition-signing")] use crate::prelude::UserFeeIncrease; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionRefV1, -}; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; #[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; -#[cfg(feature = "state-transition-signing")] use crate::state_transition::StateTransition; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::token_payment_info::TokenPaymentInfo; use crate::ProtocolError; use platform_value::Identifier; #[cfg(feature = "state-transition-signing")] use platform_version::version::PlatformVersion; use std::convert::TryFrom; +use crate::state_transition::batch_transition::batched_transition::{BatchedTransition, BatchedTransitionRef}; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; +use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +#[cfg(feature = "state-transition-signing")] +use crate::tokens::token_payment_info::TokenPaymentInfo; -/// Client-side constructors and signing helpers of a batch transition. The -/// trait is not selected by the version tables; it is the API every batch -/// wire format shares, so it reads a batch through the accessor view that -/// knows every format: a format 2 batch must see its own transitions to -/// compute the key its signature needs. -pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 { +pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV0 { #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_document_creation_transition_from_document>( @@ -77,6 +71,25 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 options: Option, ) -> Result; + /// Builds a signed erase of one already deleted keep-history document. + /// + /// The transition carries no token payment: the deletion cost was charged + /// when the document was deleted. Whether this erase starts or continues an + /// erasure, and how many revisions it removes, are read from committed + /// state rather than signed here. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( @@ -137,14 +150,14 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 let mut highest_security_level = SecurityLevel::lowest_level(); if self - .transitions_iter_v1() - .any(|transition| matches!(transition, BatchedTransitionRefV1::Token(_))) + .transitions_iter() + .any(|transition| matches!(transition, BatchedTransitionRef::Token(_))) { // If we ever have a token transition it will be security level critical and so will the whole state transition highest_security_level = SecurityLevel::CRITICAL; } else if self - .transitions_iter_v1() - .any(|transition| matches!(transition, BatchedTransitionRefV1::Document(_))) + .transitions_iter() + .any(|transition| matches!(transition, BatchedTransitionRef::Document(_))) { // We know we don't have token transitions at this point let get_data_contract_security_level_requirement = @@ -154,8 +167,8 @@ pub trait DocumentsBatchTransitionMethodsV0: DocumentsBatchTransitionAccessorsV1 .to_string(), ), )?; - for transition in self.transitions_iter_v1() { - if let BatchedTransitionRefV1::Document(document_transition) = transition { + for transition in self.transitions_iter() { + if let BatchedTransitionRef::Document(document_transition) = transition { let document_type_name = document_transition.base().document_type_name(); let data_contract_id = document_transition.base().data_contract_id(); let document_security_level = get_data_contract_security_level_requirement( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 4297b298ee9..0e3e0340ffd 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -14,7 +14,7 @@ use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; #[cfg(feature = "state-transition-signing")] use crate::prelude::{IdentityNonce, UserFeeIncrease}; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; #[cfg(feature = "state-transition-signing")] @@ -34,7 +34,7 @@ use platform_version::version::PlatformVersion; /// A trait defining methods to create various token-related state transitions as part of a document batch. /// -/// This trait builds on `DocumentsBatchTransitionAccessorsV1` and provides a unified interface +/// This trait builds on `DocumentsBatchTransitionAccessorsV0` and provides a unified interface /// for constructing signed `StateTransition`s for token operations such as minting, burning, /// transferring, freezing, claiming, and direct purchases. /// @@ -43,7 +43,7 @@ use platform_version::version::PlatformVersion; /// version selection, group action metadata, and fee calculations. /// /// All methods in this trait require the `state-transition-signing` feature to be enabled. -pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV1 { +pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 { /// Creates a `StateTransition` to mint new tokens. /// /// # Parameters diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs deleted file mode 100644 index 4c4a8bea4ef..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v2/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::document_type::DocumentTypeRef; -#[cfg(feature = "state-transition-signing")] -use crate::document::Document; -#[cfg(feature = "state-transition-signing")] -use crate::identity::signer::Signer; -#[cfg(feature = "state-transition-signing")] -use crate::identity::IdentityPublicKey; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::{IdentityNonce, UserFeeIncrease}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::StateTransition; -#[cfg(feature = "state-transition-signing")] -use crate::version::PlatformVersion; -#[cfg(feature = "state-transition-signing")] -use crate::ProtocolError; - -/// Constructors introduced with the V2 batch transition wire format. -pub trait DocumentsBatchTransitionMethodsV2 { - /// Builds a signed erase of one already deleted keep-history document. - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - options: Option, - platform_version: &PlatformVersion, - ) -> Result; -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index 4fffbea81eb..d44ee39e535 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -49,7 +49,6 @@ mod state_transition_estimated_fee_validation; mod state_transition_like; mod v0; mod v1; -mod v2; #[cfg(feature = "validation")] mod validation; mod version; @@ -63,7 +62,6 @@ use crate::state_transition::batch_transition::fields::property_names; use crate::identity::state_transition::OptionallyAssetLockProved; pub use v0::*; pub use v1::*; -pub use v2::*; #[derive( Debug, @@ -93,8 +91,6 @@ pub enum BatchTransition { V0(BatchTransitionV0), #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))] V1(BatchTransitionV1), - #[cfg_attr(feature = "serde-conversion", serde(rename = "2"))] - V2(BatchTransitionV2), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs index bdad604186b..57b24b9b2c2 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs @@ -1,6 +1,6 @@ use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; use crate::state_transition::batch_transition::batched_transition::{ - DocumentPurchaseTransition, DocumentTransferTransition, + DocumentEraseTransition, DocumentPurchaseTransition, DocumentTransferTransition, }; use crate::state_transition::batch_transition::token_direct_purchase_transition::TokenDirectPurchaseTransition; use crate::state_transition::batch_transition::{ @@ -16,6 +16,7 @@ pub trait BatchTransitionResolversV0 { fn as_transition_delete(&self) -> Option<&DocumentDeleteTransition>; fn as_transition_transfer(&self) -> Option<&DocumentTransferTransition>; fn as_transition_purchase(&self) -> Option<&DocumentPurchaseTransition>; + fn as_transition_erase(&self) -> Option<&DocumentEraseTransition>; fn as_transition_token_burn(&self) -> Option<&TokenBurnTransition>; fn as_transition_token_mint(&self) -> Option<&TokenMintTransition>; fn as_transition_token_transfer(&self) -> Option<&TokenTransferTransition>; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs index b47fc40e67f..70b5ded5676 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs @@ -3,7 +3,7 @@ use crate::consensus::basic::BasicError; use crate::consensus::state::identity::IdentityInsufficientBalanceError; use crate::consensus::ConsensusError; use crate::fee::Credits; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::{ @@ -23,7 +23,7 @@ impl StateTransitionEstimatedFeeValidation for BatchTransition { .fee_version .state_transition_min_fees .document_batch_sub_transition - .saturating_mul(self.transitions_len_v1() as u64)) + .saturating_mul(self.transitions_len() as u64)) } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs index 17fdf860ca4..6e086e05299 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_like.rs @@ -13,7 +13,6 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.modified_data_ids(), BatchTransition::V1(transition) => transition.modified_data_ids(), - BatchTransition::V2(transition) => transition.modified_data_ids(), } } @@ -21,7 +20,6 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(_) => 0, BatchTransition::V1(_) => 1, - BatchTransition::V2(_) => 2, } } /// returns the type of State Transition @@ -29,7 +27,6 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.state_transition_type(), BatchTransition::V1(transition) => transition.state_transition_type(), - BatchTransition::V2(transition) => transition.state_transition_type(), } } @@ -37,7 +34,6 @@ impl StateTransitionLike for BatchTransition { match self { BatchTransition::V0(transition) => transition.unique_identifiers(), BatchTransition::V1(transition) => transition.unique_identifiers(), - BatchTransition::V2(transition) => transition.unique_identifiers(), } } } @@ -47,7 +43,6 @@ impl StateTransitionHasUserFeeIncrease for BatchTransition { match self { BatchTransition::V0(transition) => transition.user_fee_increase(), BatchTransition::V1(transition) => transition.user_fee_increase(), - BatchTransition::V2(transition) => transition.user_fee_increase(), } } @@ -55,7 +50,6 @@ impl StateTransitionHasUserFeeIncrease for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_user_fee_increase(user_fee_increase), BatchTransition::V1(transition) => transition.set_user_fee_increase(user_fee_increase), - BatchTransition::V2(transition) => transition.set_user_fee_increase(user_fee_increase), } } } @@ -66,7 +60,6 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.signature(), BatchTransition::V1(transition) => transition.signature(), - BatchTransition::V2(transition) => transition.signature(), } } /// set a new signature @@ -74,7 +67,6 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature(signature), BatchTransition::V1(transition) => transition.set_signature(signature), - BatchTransition::V2(transition) => transition.set_signature(signature), } } @@ -82,7 +74,6 @@ impl StateTransitionSingleSigned for BatchTransition { match self { BatchTransition::V0(transition) => transition.set_signature_bytes(signature), BatchTransition::V1(transition) => transition.set_signature_bytes(signature), - BatchTransition::V2(transition) => transition.set_signature_bytes(signature), } } } @@ -92,7 +83,6 @@ impl StateTransitionOwned for BatchTransition { match self { BatchTransition::V0(transition) => transition.owner_id(), BatchTransition::V1(transition) => transition.owner_id(), - BatchTransition::V2(transition) => transition.owner_id(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs index 3d70d83a6f5..c8011b7ec59 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs @@ -30,14 +30,13 @@ mod batch_transition_tests { TokenTransitionActionType, TokenTransitionActionTypeGetter, }; use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef, BatchedTransitionV1, - DocumentTransitionV1, + BatchedTransition, BatchedTransitionMutRef, BatchedTransitionRef, }; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{ - BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, + BatchTransitionV0, BatchTransitionV1, }; use crate::state_transition::StateTransitionLike; use crate::state_transition::StateTransition; @@ -1187,165 +1186,4 @@ mod batch_transition_tests { assert!(matches!(second, BatchedTransitionRef::Token(_))); assert!(iter.next().is_none()); } - - // ----------------------------------------------------------------------- - // Batch format 2: the only format whose document shell carries an erase - // ----------------------------------------------------------------------- - - fn make_erase_transition(nonce: u64) -> DocumentTransitionV1 { - use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; - use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; - - DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { - base: make_base_transition(nonce), - })) - } - - fn make_batch_v2(transitions: Vec) -> BatchTransitionV2 { - BatchTransitionV2 { - owner_id: Identifier::new([0x03; 32]), - transitions, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::default(), - } - } - - #[test] - fn should_see_every_format_through_the_format_aware_view_and_only_the_old_ones_through_the_shipped_view( - ) { - use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; - - let v0 = BatchTransition::V0(make_batch_v0(vec![make_delete_transition(1)])); - let v1 = BatchTransition::V1(make_batch_v1(vec![ - BatchedTransition::Document(make_delete_transition(1)), - BatchedTransition::Token(make_token_burn_transition(2, 5)), - ])); - let v2 = BatchTransition::V2(make_batch_v2(vec![ - BatchedTransitionV1::Document(make_erase_transition(1)), - BatchedTransitionV1::Token(make_token_burn_transition(2, 5)), - ])); - - for (batch, expected_len) in [(&v0, 1), (&v1, 2), (&v2, 2)] { - assert_eq!(batch.transitions_len_v1(), expected_len); - assert_eq!(batch.transitions_iter_v1().count(), expected_len); - assert!(batch.first_transition_v1().is_some()); - assert!(batch.contains_document_transition_v1()); - } - assert!(v2.contains_token_transition_v1()); - assert!(v2 - .first_transition_v1() - .expect("first") - .as_transition_erase() - .is_some()); - - // The shipped view cannot represent an erase, so it reports a format 2 - // batch as empty: a shipped generation that ever received one would - // refuse it as a batch without transitions instead of dropping items. - assert_eq!(v0.transitions_len(), 1); - assert_eq!(v1.transitions_len(), 2); - assert_eq!(v2.transitions_len(), 0); - assert!(v2.transitions_are_empty()); - assert!(v2.transitions_iter().next().is_none()); - assert!(v2.first_transition().is_none()); - assert!(!v2.contains_document_transition()); - assert!(!v2.contains_token_transition()); - } - - #[test] - fn should_round_trip_a_format_2_batch_with_an_erase_through_bytes() { - use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; - - let batch = BatchTransition::V2(make_batch_v2(vec![ - BatchedTransitionV1::Document(make_erase_transition(1)), - BatchedTransitionV1::Document(DocumentTransitionV1::Delete( - DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { - base: make_base_transition(2), - }), - )), - ])); - - let bytes = batch.serialize_to_bytes().expect("serialize"); - let recovered = - BatchTransition::deserialize_from_bytes_untrusted(&bytes).expect("deserialize"); - - assert_eq!(recovered, batch); - } - - /// Formats 1 and 2 lay out their items identically except for the kinds - /// their document shells know, so relabeling a format 2 batch as format 1 - /// shows exactly what software that only knows format 1 would do with the - /// same items: it decodes every shipped kind and refuses an erase. - #[test] - fn should_refuse_an_erase_in_the_bytes_of_a_format_1_batch() { - use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; - - let relabel_as_format_1 = |batch: BatchTransition| { - let mut bytes = batch.serialize_to_bytes().expect("serialize"); - assert_eq!(bytes[0], 2, "the first byte is the batch format"); - bytes[0] = 1; - BatchTransition::deserialize_from_bytes_untrusted(&bytes) - }; - - let delete_only = relabel_as_format_1(BatchTransition::V2(make_batch_v2(vec![ - BatchedTransitionV1::Document(DocumentTransitionV1::Delete( - DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { - base: make_base_transition(2), - }), - )), - ]))) - .expect("a delete decodes in format 1"); - assert!(matches!(delete_only, BatchTransition::V1(_))); - assert_eq!(delete_only.transitions_len(), 1); - - let with_erase = relabel_as_format_1(BatchTransition::V2(make_batch_v2(vec![ - BatchedTransitionV1::Document(make_erase_transition(1)), - ]))); - assert!( - with_erase.is_err(), - "format 1 has no erase kind, so the bytes must not decode" - ); - } - - #[test] - fn should_refuse_to_downcast_an_erase_to_the_shipped_shells() { - let erase = BatchedTransitionV1::Document(make_erase_transition(1)); - let delete = BatchedTransitionV1::Document(DocumentTransitionV1::Delete( - DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { - base: make_base_transition(2), - }), - )); - - assert!(BatchedTransition::try_from(erase).is_err()); - assert!(matches!( - BatchedTransition::try_from(delete), - Ok(BatchedTransition::Document(DocumentTransition::Delete(_))) - )); - } - - #[test] - fn should_upcast_and_set_shipped_transitions_on_a_format_2_batch() { - let mut batch = BatchTransition::V2(make_batch_v2(vec![BatchedTransitionV1::Document( - make_erase_transition(1), - )])); - - batch.set_transitions(vec![BatchedTransition::Document(make_delete_transition(7))]); - batch.set_identity_contract_nonce(9); - - let BatchTransition::V2(batch) = batch else { - panic!("the format does not change"); - }; - assert!(matches!( - batch.transitions.as_slice(), - [BatchedTransitionV1::Document(DocumentTransitionV1::Delete( - _ - ))] - )); - assert_eq!( - batch.transitions[0] - .borrow_as_ref() - .identity_contract_nonce(), - 9 - ); - } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs index 73f5b7bb738..b3dfb97b985 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs @@ -23,7 +23,8 @@ use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransi use crate::state_transition::batch_transition::BatchTransitionV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, + DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -182,6 +183,54 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV0 { } #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + // Erase never carries a token payment: it acts on a document whose + // deletion cost was already charged. + let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( + document, + document_type, + None, + identity_contract_nonce, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + platform_version, + )? + .into(); + let documents_batch_transition: BatchTransition = BatchTransitionV0 { + owner_id, + transitions: vec![erase_transition], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs index 0c7247ce76c..0a8fc71205b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs @@ -28,7 +28,8 @@ use std::slice::Iter; use crate::state_transition::batch_transition::BatchTransitionV1; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, + BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, + DocumentIndexOnlyDeleteTransition, }; #[cfg(feature = "state-transition-signing")] use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; @@ -257,6 +258,54 @@ impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV1 { } #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_document_erase_transition_from_document>( + document: Document, + document_type: DocumentTypeRef<'_>, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let owner_id = document.owner_id(); + let resolved_options = options.unwrap_or_default(); + // Erase never carries a token payment: it acts on a document whose + // deletion cost was already charged. + let erase_transition: DocumentTransition = DocumentEraseTransition::from_document( + document, + document_type, + None, + identity_contract_nonce, + resolved_options.method_feature_version, + resolved_options.base_feature_version, + platform_version, + )? + .into(); + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Document(erase_transition)], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + let mut state_transition: StateTransition = documents_batch_transition.into(); + let required_security_level = document_type.security_level_requirement(); + state_transition + .sign_external_with_options( + identity_public_key, + signer, + Some(|_, _| Ok(required_security_level)), + resolved_options.signing_options, + ) + .await?; + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] async fn new_document_transfer_transition_from_document>( document: Document, document_type: DocumentTypeRef<'_>, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs deleted file mode 100644 index e85d03b799f..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/identity_signed.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::identity::SecurityLevel::{CRITICAL, HIGH, MEDIUM}; -use crate::identity::{KeyID, Purpose, SecurityLevel}; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; -use crate::state_transition::batch_transition::BatchTransitionV2; -use crate::state_transition::StateTransitionIdentitySigned; - -impl StateTransitionIdentitySigned for BatchTransitionV2 { - fn signature_public_key_id(&self) -> KeyID { - self.signature_public_key_id - } - - fn set_signature_public_key_id(&mut self, key_id: KeyID) { - self.signature_public_key_id = key_id - } - - fn security_level_requirement(&self, purpose: Purpose) -> Vec { - if purpose == Purpose::TRANSFER { - vec![CRITICAL] - } else { - // These are the available key levels that must sign the state transition - // However the fact that it is signed by one of these does not guarantee that it - // meets the security level requirement, as that is dictated from within the data - // contract - vec![CRITICAL, HIGH, MEDIUM] - } - } - - fn purpose_requirement(&self) -> Vec { - if self.transitions_len_v1() == 1 { - if let Some(first) = self.first_transition_v1() { - if first.as_transition_token_claim().is_some() - || first.as_transition_token_transfer().is_some() - { - return vec![Purpose::AUTHENTICATION, Purpose::TRANSFER]; - } - } - } - - vec![Purpose::AUTHENTICATION] - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs deleted file mode 100644 index 5194e165b84..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod identity_signed; -mod state_transition_like; -mod types; -mod v0_methods; -mod v1_methods; -mod version; - -use crate::identity::KeyID; - -use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; -use crate::ProtocolError; -use bincode::{Decode, DecodeUntrusted, Encode}; -use platform_serialization_derive::PlatformSignable; - -use crate::prelude::UserFeeIncrease; -#[cfg(feature = "json-conversion")] -use crate::serialization::json_safe_fields; -use platform_value::{BinaryData, Identifier}; -#[cfg(feature = "serde-conversion")] -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "json-conversion", json_safe_fields)] -#[derive(Debug, Clone, PartialEq, Encode, Decode, PlatformSignable, DecodeUntrusted)] -#[cfg_attr( - feature = "serde-conversion", - derive(Serialize, Deserialize), - serde(rename_all = "camelCase") -)] -#[derive(Default)] -pub struct BatchTransitionV2 { - pub owner_id: Identifier, - pub transitions: Vec, - pub user_fee_increase: UserFeeIncrease, - #[platform_signable(exclude_from_sig_hash)] - pub signature_public_key_id: KeyID, - #[platform_signable(exclude_from_sig_hash)] - pub signature: BinaryData, -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs deleted file mode 100644 index dc2b25e5662..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/state_transition_like.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::prelude::UserFeeIncrease; -use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; -use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionV2}; -use crate::state_transition::StateTransitionHasUserFeeIncrease; -use crate::state_transition::StateTransitionType::Batch; -use crate::state_transition::{StateTransition, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, StateTransitionType}; -use crate::version::FeatureVersion; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use platform_value::{BinaryData, Identifier}; -use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; -use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; -use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; - -impl From for StateTransition { - fn from(value: BatchTransitionV2) -> Self { - let document_batch_transition: BatchTransition = value.into(); - document_batch_transition.into() - } -} - -impl StateTransitionLike for BatchTransitionV2 { - /// Returns ID of the created contract - fn modified_data_ids(&self) -> Vec { - self.transitions - .iter() - .filter_map(|t| match t { - BatchedTransitionV1::Document(document_transition) => { - Some(document_transition.base().id()) - } - BatchedTransitionV1::Token(_) => None, - }) - .collect() - } - - fn state_transition_protocol_version(&self) -> FeatureVersion { - 2 - } - /// returns the type of State Transition - fn state_transition_type(&self) -> StateTransitionType { - Batch - } - - /// We create a list of unique identifiers for the batch - fn unique_identifiers(&self) -> Vec { - self.transitions - .iter() - .map(|transition| match transition { - BatchedTransitionV1::Document(document_transition) => { - format!( - "{}-{}-{:x}", - BASE64_STANDARD.encode(self.owner_id), - BASE64_STANDARD.encode(document_transition.data_contract_id()), - document_transition.identity_contract_nonce() - ) - } - BatchedTransitionV1::Token(token_transition) => { - format!( - "{}-{}-{:x}", - BASE64_STANDARD.encode(self.owner_id), - BASE64_STANDARD.encode(token_transition.data_contract_id()), - token_transition.identity_contract_nonce() - ) - } - }) - .collect() - } -} - -impl StateTransitionHasUserFeeIncrease for BatchTransitionV2 { - fn user_fee_increase(&self) -> UserFeeIncrease { - self.user_fee_increase - } - - fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) { - self.user_fee_increase = user_fee_increase - } -} - -impl StateTransitionSingleSigned for BatchTransitionV2 { - /// returns the signature as a byte-array - fn signature(&self) -> &BinaryData { - &self.signature - } - /// set a new signature - fn set_signature(&mut self, signature: BinaryData) { - self.signature = signature - } - - fn set_signature_bytes(&mut self, signature: Vec) { - self.signature = BinaryData::new(signature) - } -} - -impl StateTransitionOwned for BatchTransitionV2 { - /// Get owner ID - fn owner_id(&self) -> Identifier { - self.owner_id - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs deleted file mode 100644 index d019af085d5..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/types.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::state_transition::batch_transition::fields::property_names::*; -use crate::state_transition::batch_transition::fields::*; -use crate::state_transition::batch_transition::BatchTransitionV2; -use crate::state_transition::StateTransitionFieldTypes; - -impl StateTransitionFieldTypes for BatchTransitionV2 { - fn binary_property_paths() -> Vec<&'static str> { - vec![SIGNATURE] - } - - fn identifiers_property_paths() -> Vec<&'static str> { - vec![OWNER_ID] - } - - fn signature_property_paths() -> Vec<&'static str> { - vec![SIGNATURE, SIGNATURE_PUBLIC_KEY_ID] - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs deleted file mode 100644 index 3db2232f463..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v0_methods.rs +++ /dev/null @@ -1,459 +0,0 @@ -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::document_type::DocumentTypeRef; -#[cfg(feature = "state-transition-signing")] -use crate::document::{Document, DocumentV0Getters}; -use crate::fee::Credits; -#[cfg(feature = "state-transition-signing")] -use crate::identity::signer::Signer; -use crate::prelude::IdentityNonce; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::IdentityPublicKey; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::UserFeeIncrease; -use crate::state_transition::batch_transition::batched_transition::BatchedTransition; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::batched_transition::{ - DocumentPurchaseTransition, DocumentReplaceTransition, DocumentTransferTransition, - DocumentUpdatePriceTransition, -}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; -use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; -use crate::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; - -use crate::state_transition::batch_transition::BatchTransitionV2; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{ - BatchTransition, DocumentDeleteTransition, DocumentEraseTransition, - DocumentIndexOnlyDeleteTransition, -}; -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::document_type::accessors::DocumentTypeV2Getters; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::batched_transition::DocumentTransitionV1; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::StateTransition; -use crate::ProtocolError; -#[cfg(feature = "state-transition-signing")] -use platform_value::Identifier; -#[cfg(feature = "state-transition-signing")] -use platform_version::version::PlatformVersion; -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; -use crate::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; -use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::token_payment_info::TokenPaymentInfo; - -impl DocumentsBatchTransitionMethodsV0 for BatchTransitionV2 { - #[cfg(feature = "state-transition-signing")] - async fn new_document_creation_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - entropy: [u8; 32], - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - let create_transition = DocumentCreateTransition::from_document( - document, - document_type, - entropy, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )?; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(create_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_document_replacement_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - let replace_transition = DocumentReplaceTransition::from_document( - document, - document_type, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )?; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(replace_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_document_deletion_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - // Deleting an indexOnly document is a different OPERATION from - // deleting a stored one (delete-by-values vs delete-by-id), so the - // factory picks the transition kind from the doctype's storage - // mode — every construction path, SDKs included, produces the kind - // the ABCI structure gates require, with no per-client knowledge - // of the storage layout. - let delete_transition: DocumentTransitionV1 = if document_type.index_only() { - DocumentIndexOnlyDeleteTransition::from_document( - document, - document_type, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )? - .into() - } else { - DocumentDeleteTransition::from_document( - document, - document_type, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )? - .into() - }; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(delete_transition)], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_transfer_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - recipient_owner_id: Identifier, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - let transfer_transition = DocumentTransferTransition::from_document( - document, - document_type, - token_payment_info, - identity_contract_nonce, - recipient_owner_id, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )?; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(transfer_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_document_update_price_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - price: Credits, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - let transfer_transition = DocumentUpdatePriceTransition::from_document( - document, - document_type, - price, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )?; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(transfer_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_document_purchase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - new_owner_id: Identifier, - price: Credits, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - token_payment_info: Option, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let resolved_options = options.unwrap_or_default(); - let purchase_transition = DocumentPurchaseTransition::from_document( - document, - document_type, - price, - token_payment_info, - identity_contract_nonce, - platform_version, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - )?; - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id: new_owner_id, - transitions: vec![BatchedTransitionV1::Document(purchase_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } - - fn set_transitions(&mut self, transitions: Vec) { - self.transitions = transitions.into_iter().map(Into::into).collect(); - } - - fn set_identity_contract_nonce(&mut self, identity_contract_nonce: IdentityNonce) { - self.transitions - .iter_mut() - .for_each(|transition| transition.set_identity_contract_nonce(identity_contract_nonce)); - } - - fn all_conflicting_index_collateral_voting_funds( - &self, - ) -> Result, ProtocolError> { - let (total, any_voting_funds): (Option, bool) = self - .transitions - .iter() - .filter_map(|transition| { - transition - .as_transition_create() - .and_then(|document_create_transition| { - // Safely sum up values to avoid overflow. - document_create_transition - .prefunded_voting_balance() - .as_ref() - .map(|(_, credits)| *credits) - }) - }) - .fold((None, false), |(acc, _), price| match acc { - Some(acc_val) => acc_val - .checked_add(price) - .map_or((None, true), |sum| (Some(sum), true)), - None => (Some(price), true), - }); - - match (total, any_voting_funds) { - (Some(total), _) => Ok(Some(total)), - (None, true) => Err(ProtocolError::Overflow( - "overflow in all voting funds amount", - )), // Overflow occurred - _ => Ok(None), - } - } - - fn all_document_purchases_amount(&self) -> Result, ProtocolError> { - let (total, any_purchases): (Option, bool) = self - .transitions - .iter() - .filter_map(|transition| { - transition - .as_transition_purchase() - .map(|purchase| purchase.price()) - }) - .fold((None, false), |(acc, _), price| match acc { - Some(acc_val) => acc_val - .checked_add(price) - .map_or((None, true), |sum| (Some(sum), true)), - None => (Some(price), true), - }); - - match (total, any_purchases) { - (Some(total), _) => Ok(Some(total)), - (None, true) => Err(ProtocolError::Overflow("overflow in all purchases amount")), // Overflow occurred - _ => Ok(None), // No purchases were found - } - } -} - -impl DocumentsBatchTransitionMethodsV2 for BatchTransitionV2 { - #[cfg(feature = "state-transition-signing")] - #[allow(clippy::too_many_arguments)] - async fn new_document_erase_transition_from_document>( - document: Document, - document_type: DocumentTypeRef<'_>, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - options: Option, - platform_version: &PlatformVersion, - ) -> Result { - let owner_id = document.owner_id(); - let resolved_options = options.unwrap_or_default(); - let erase_transition: DocumentTransitionV1 = DocumentEraseTransition::from_document( - document, - document_type, - None, - identity_contract_nonce, - resolved_options.method_feature_version, - resolved_options.base_feature_version, - platform_version, - )? - .into(); - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Document(erase_transition)], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - let required_security_level = document_type.security_level_requirement(); - state_transition - .sign_external_with_options( - identity_public_key, - signer, - Some(|_, _| Ok(required_security_level)), - resolved_options.signing_options, - ) - .await?; - Ok(state_transition) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs deleted file mode 100644 index 7e8cc5e3bf6..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/v1_methods.rs +++ /dev/null @@ -1,929 +0,0 @@ -#[cfg(feature = "state-transition-signing")] -use crate::fee::Credits; -#[cfg(feature = "state-transition-signing")] -use crate::identity::signer::Signer; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::IdentityNonce; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::IdentityPublicKey; -#[cfg(feature = "state-transition-signing")] -use crate::prelude::UserFeeIncrease; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::batched_transition::BatchedTransitionV1; -use crate::state_transition::batch_transition::BatchTransitionV2; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{TokenClaimTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenTransferTransition, TokenUnfreezeTransition}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::BatchTransition; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::StateTransition; -#[cfg(feature = "state-transition-signing")] -use crate::ProtocolError; -#[cfg(feature = "state-transition-signing")] -use platform_value::Identifier; -#[cfg(feature = "state-transition-signing")] -use platform_version::version::PlatformVersion; -#[cfg(feature = "state-transition-signing")] -use crate::balances::credits::TokenAmount; -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; -#[cfg(feature = "state-transition-signing")] -use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; -#[cfg(feature = "state-transition-signing")] -use crate::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; -use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_burn_transition::TokenBurnTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_claim_transition::TokenClaimTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_config_update_transition::TokenConfigUpdateTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_direct_purchase_transition::TokenDirectPurchaseTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_emergency_action_transition::TokenEmergencyActionTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_freeze_transition::TokenFreezeTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_mint_transition::TokenMintTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_transfer_transition::TokenTransferTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::token_unfreeze_transition::TokenUnfreezeTransitionV0; -#[cfg(feature = "state-transition-signing")] -use crate::state_transition::GetDataContractSecurityLevelRequirementFn; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::emergency_action::TokenEmergencyAction; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::{PrivateEncryptedNote, SharedEncryptedNote}; -#[cfg(feature = "state-transition-signing")] -use crate::tokens::token_pricing_schedule::TokenPricingSchedule; - -impl DocumentsBatchTransitionMethodsV1 for BatchTransitionV2 { - #[cfg(feature = "state-transition-signing")] - async fn new_token_mint_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - amount: TokenAmount, - issued_to_identity_id: Option, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut mint_transition = TokenMintTransition::V0(TokenMintTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - issued_to_identity_id, - amount, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = - mint_transition.calculate_action_id(owner_id, platform_version)?; - mint_transition.base_mut().set_using_group_info(Some( - GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - }, - )) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - mint_transition.base_mut().set_using_group_info(Some(info)) - } - } - } - - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(mint_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = documents_batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_burn_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - amount: TokenAmount, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut burn_transition = TokenBurnTransition::V0(TokenBurnTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - burn_amount: amount, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = - burn_transition.calculate_action_id(owner_id, platform_version)?; - burn_transition.base_mut().set_using_group_info(Some( - GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - }, - )) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - burn_transition.base_mut().set_using_group_info(Some(info)) - } - } - } - - // Wrap in a batch transition - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(burn_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - - // Create the state transition - let mut state_transition: StateTransition = documents_batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - - Ok(state_transition) - } - #[cfg(feature = "state-transition-signing")] - async fn new_token_transfer_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - amount: TokenAmount, - recipient_id: Identifier, - public_note: Option, - shared_encrypted_note: Option, - private_encrypted_note: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - _platform_version: &PlatformVersion, - options: Option, - ) -> Result { - // Create the transfer transition for batch version 1 - let transfer_transition = TokenTransferTransition::V0(TokenTransferTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - recipient_id, - amount, - public_note, - shared_encrypted_note, - private_encrypted_note, - }); - - // Wrap in a batch transition - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(transfer_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - - // Create the state transition - let mut state_transition: StateTransition = documents_batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_freeze_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - frozen_identity_id: Identifier, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut freeze_transition = TokenFreezeTransition::V0(TokenFreezeTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - identity_to_freeze_id: frozen_identity_id, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = - freeze_transition.calculate_action_id(owner_id, platform_version)?; - freeze_transition.base_mut().set_using_group_info(Some( - GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - }, - )) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - freeze_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(freeze_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - - let mut state_transition: StateTransition = documents_batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_unfreeze_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - frozen_identity_id: Identifier, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut unfreeze_transition = TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - frozen_identity_id, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = - unfreeze_transition.calculate_action_id(owner_id, platform_version)?; - unfreeze_transition.base_mut().set_using_group_info(Some( - GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - }, - )) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - unfreeze_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let documents_batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(unfreeze_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - - let mut state_transition: StateTransition = documents_batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_destroy_frozen_funds_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - frozen_identity_id: Identifier, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut destroy_frozen_funds_transition = - TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - frozen_identity_id, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = destroy_frozen_funds_transition - .calculate_action_id(owner_id, platform_version)?; - destroy_frozen_funds_transition - .base_mut() - .set_using_group_info(Some(GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - })) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - destroy_frozen_funds_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token( - destroy_frozen_funds_transition.into(), - )], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_emergency_action_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - emergency_action: TokenEmergencyAction, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut emergency_action_transition = - TokenEmergencyActionTransition::V0(TokenEmergencyActionTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - emergency_action, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = emergency_action_transition - .calculate_action_id(owner_id, platform_version)?; - emergency_action_transition - .base_mut() - .set_using_group_info(Some(GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - })) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - emergency_action_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token( - emergency_action_transition.into(), - )], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_config_update_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - update_token_configuration_item: TokenConfigurationChangeItem, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut config_update_transition = - TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - update_token_configuration_item, - public_note, - }); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = - config_update_transition.calculate_action_id(owner_id, platform_version)?; - config_update_transition - .base_mut() - .set_using_group_info(Some(GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - })) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - config_update_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(config_update_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_claim_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - distribution_type: TokenDistributionType, - public_note: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - _platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let claim_transition = TokenClaimTransition::V0(TokenClaimTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - distribution_type, - public_note, - }); - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token(claim_transition.into())], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_change_direct_purchase_price_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - token_pricing_schedule: Option, - public_note: Option, - using_group_info: Option, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let mut change_direct_purchase_price_transition = - TokenSetPriceForDirectPurchaseTransition::V0( - TokenSetPriceForDirectPurchaseTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - price: token_pricing_schedule, - public_note, - }, - ); - - if let Some(using_group_info_status) = using_group_info { - match using_group_info_status { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( - group_contract_position, - ) => { - let action_id = change_direct_purchase_price_transition - .calculate_action_id(owner_id, platform_version)?; - change_direct_purchase_price_transition - .base_mut() - .set_using_group_info(Some(GroupStateTransitionInfo { - group_contract_position, - action_id, - action_is_proposer: true, - })) - } - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { - change_direct_purchase_price_transition - .base_mut() - .set_using_group_info(Some(info)) - } - } - } - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token( - change_direct_purchase_price_transition.into(), - )], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } - - #[cfg(feature = "state-transition-signing")] - async fn new_token_direct_purchase_transition>( - token_id: Identifier, - owner_id: Identifier, - data_contract_id: Identifier, - token_contract_position: u16, - amount: TokenAmount, - total_agreed_price: Credits, - identity_public_key: &IdentityPublicKey, - identity_contract_nonce: IdentityNonce, - user_fee_increase: UserFeeIncrease, - signer: &S, - _platform_version: &PlatformVersion, - options: Option, - ) -> Result { - let direct_purchase_transition = - TokenDirectPurchaseTransition::V0(TokenDirectPurchaseTransitionV0 { - base: TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce, - token_contract_position, - data_contract_id, - token_id, - using_group_info: None, - }), - token_count: amount, - total_agreed_price, - }); - - let batch_transition: BatchTransition = BatchTransitionV2 { - owner_id, - transitions: vec![BatchedTransitionV1::Token( - direct_purchase_transition.into(), - )], - user_fee_increase, - signature_public_key_id: 0, - signature: Default::default(), - } - .into(); - let mut state_transition: StateTransition = batch_transition.into(); - if let Some(options) = options { - state_transition - .sign_external_with_options( - identity_public_key, - signer, - None::, - options.signing_options, - ) - .await?; - } else { - state_transition - .sign_external( - identity_public_key, - signer, - None::, - ) - .await?; - } - Ok(state_transition) - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs deleted file mode 100644 index b52ddd83a0a..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v2/version.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::state_transition::batch_transition::BatchTransitionV2; -use crate::state_transition::FeatureVersioned; -use crate::version::FeatureVersion; - -impl FeatureVersioned for BatchTransitionV2 { - fn feature_version(&self) -> FeatureVersion { - 2 - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs index 0fe196a2db3..b2f89c158d7 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/mod.rs @@ -4,7 +4,6 @@ use crate::ProtocolError; use platform_version::version::PlatformVersion; mod v0; -mod v1; impl BatchTransition { pub fn validate_base_structure( @@ -20,10 +19,9 @@ impl BatchTransition { .validate_base_structure { 0 => self.validate_base_structure_v0(platform_version), - 1 => self.validate_base_structure_v1(platform_version), version => Err(ProtocolError::UnknownVersionMismatch { method: "DocumentsBatchTransition::validate_base_structure".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, }), } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 83887155a60..7bf75a47a24 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -4,7 +4,9 @@ use crate::consensus::basic::document::{ }; use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; use crate::consensus::basic::BasicError; -use crate::state_transition::batch_transition::batched_transition::DocumentIndexOnlyDeleteTransition; +use crate::state_transition::batch_transition::batched_transition::{ + DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, +}; use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; @@ -141,6 +143,43 @@ impl BatchTransition { Some(_) => {} } } + + // The erase kind joined the wire at protocol version 15. Old + // software cannot decode it at all, so no historical block can + // contain one; this check exists so that new software agrees + // with old software while an earlier protocol version is still + // active: without it, an erase submitted at protocol version 14 + // would decode fine here while being undecodable on nodes that + // predate the kind. + if let DocumentTransition::Erase(erase) = transition { + let feature_version = match erase { + DocumentEraseTransition::V0(_) => 0, + }; + match &platform_version + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + { + None => { + // The kind does not exist at this protocol + // version; the empty supported range (min 1, + // max 0) states exactly that. + result.add_error(BasicError::UnsupportedVersionError( + UnsupportedVersionError::new(feature_version, 1, 0), + )); + } + Some(bounds) if !bounds.bounds.check_version(feature_version) => { + result.add_error(BasicError::UnsupportedVersionError( + UnsupportedVersionError::new( + feature_version, + bounds.bounds.min_version, + bounds.bounds.max_version, + ), + )); + } + Some(_) => {} + } + } } // Make sure we don't have duplicate transitions @@ -328,6 +367,104 @@ mod tests { }) } + // ----------------------------------------------------------------------- + // erase kind wire gate + // ----------------------------------------------------------------------- + + fn make_erase(nonce: u64, id_byte: u8) -> DocumentTransition { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + + DocumentTransition::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::new([id_byte; 32]), + identity_contract_nonce: nonce, + document_type_name: "test_doc".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + }), + })) + } + + /// Software that predates the erase kind cannot decode one at all, so no + /// historical block can contain one. The gate exists so that new software + /// agrees with old software while protocol version 14 is still active: + /// refused there, where the kind's table entry is `None`, and admitted at + /// the latest version, whose entry publishes the kind's bounds. + #[test] + fn validate_base_structure_v0_gates_erase_by_protocol_version() { + let batch = make_batch_v0(vec![make_erase(1, 1)]); + + let released = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert!( + released + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .is_none(), + "the test needs a released version that publishes no erase bounds" + ); + let result = batch + .validate_base_structure_v0(released) + .expect("no protocol err"); + assert!( + result.errors.iter().any(|error| matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + )), + "protocol version 14 must reject an erase as an unsupported version, got {:?}", + result.errors + ); + + let result = batch + .validate_base_structure_v0(PlatformVersion::latest()) + .expect("no protocol err"); + assert!( + result.is_valid(), + "the latest protocol version must admit an erase, got {:?}", + result.errors + ); + } + + /// A batch may name one document at most once, whatever the kinds: the + /// duplicate finder the structure check runs fingerprints an erase by its + /// document type and id like every other kind, so an erase of a document + /// that another transition in the batch names is reported with it. + #[test] + fn validate_base_structure_v0_treats_an_erase_of_a_named_document_as_a_duplicate() { + let erase = make_erase(1, 7); + let delete = make_delete(2, 7); + let other_document = make_create(3); + + let transitions = vec![&erase, &delete, &other_document]; + let duplicates = find_duplicates_by_id(&transitions, PlatformVersion::latest()) + .expect("no protocol err"); + + assert_eq!( + duplicates.len(), + 2, + "the erase and the delete name the same document, got {duplicates:?}" + ); + assert!(duplicates + .iter() + .any(|t| matches!(t, DocumentTransition::Erase(_)))); + } + + /// An erase in a batch is bounded like every other transition. + #[test] + fn validate_base_structure_v0_bounds_the_nonce_of_an_erase() { + let result = make_batch_v0(vec![make_erase(u64::MAX, 1)]) + .validate_base_structure_v0(PlatformVersion::latest()) + .expect("no protocol err"); + + assert!( + result.errors.iter().any(|error| matches!( + error, + ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) + )), + "expected NonceOutOfBoundsError, got {:?}", + result.errors + ); + } + // ----------------------------------------------------------------------- // indexOnlyDelete (delete-by-values kind) wire gate // ----------------------------------------------------------------------- diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs deleted file mode 100644 index c52ec1b29ec..00000000000 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v1/mod.rs +++ /dev/null @@ -1,728 +0,0 @@ -use crate::consensus::basic::document::{ - DocumentTransitionsAreAbsentError, DuplicateDocumentTransitionsWithIdsError, - MaxDocumentsTransitionsExceededError, NonceOutOfBoundsError, -}; -use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; -use crate::consensus::basic::BasicError; -use crate::state_transition::batch_transition::batched_transition::{ - DocumentEraseTransition, DocumentIndexOnlyDeleteTransition, -}; - -use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; -use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use crate::state_transition::batch_transition::BatchTransition; -use crate::validation::SimpleConsensusValidationResult; -use crate::ProtocolError; -use platform_value::Identifier; -use platform_version::version::dpp_versions::dpp_state_transition_serialization_versions::DocumentFeatureVersionBounds; -use platform_version::version::PlatformVersion; -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; -use crate::consensus::basic::group::GroupActionNotAllowedOnTransitionError; -use crate::consensus::basic::token::{InvalidActionIdError, InvalidTokenIdError}; -use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransitionRefV1, DocumentTransitionRefV1, -}; -use crate::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; -use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionTypeGetter; -use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; -use crate::state_transition::batch_transition::token_config_update_transition::validate_structure::TokenConfigUpdateTransitionStructureValidation; -use crate::state_transition::batch_transition::token_destroy_frozen_funds_transition::validate_structure::TokenDestroyFrozenFundsTransitionStructureValidation; -use crate::state_transition::batch_transition::token_emergency_action_transition::validate_structure::TokenEmergencyActionTransitionStructureValidation; -use crate::state_transition::batch_transition::token_freeze_transition::validate_structure::TokenFreezeTransitionStructureValidation; -use crate::state_transition::batch_transition::token_mint_transition::validate_structure::TokenMintTransitionStructureValidation; -use crate::state_transition::batch_transition::token_claim_transition::validate_structure::TokenClaimTransitionStructureValidation; -use crate::state_transition::batch_transition::token_direct_purchase_transition::validate_structure::TokenDirectPurchaseTransitionStructureValidation; -use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::validate_structure::TokenSetPriceForDirectPurchaseTransitionStructureValidation; -use crate::state_transition::batch_transition::token_transfer_transition::validate_structure::TokenTransferTransitionStructureValidation; -use crate::state_transition::batch_transition::token_unfreeze_transition::validate_structure::TokenUnfreezeTransitionStructureValidation; -use crate::state_transition::state_transitions::document::batch_transition::batched_transition::token_burn_transition::validate_structure::TokenBurnTransitionStructureValidation; -use crate::state_transition::StateTransitionOwned; - -impl BatchTransition { - #[inline(always)] - pub(super) fn validate_base_structure_v1( - &self, - platform_version: &PlatformVersion, - ) -> Result { - if self.transitions_are_empty_v1() { - return Ok(SimpleConsensusValidationResult::new_with_error( - DocumentTransitionsAreAbsentError::new().into(), - )); - } - - let transitions_len = self.transitions_len_v1(); - - if transitions_len > u16::MAX as usize - || transitions_len as u16 - > platform_version - .system_limits - .max_transitions_in_documents_batch - { - return Ok(SimpleConsensusValidationResult::new_with_error( - MaxDocumentsTransitionsExceededError::new( - platform_version - .system_limits - .max_transitions_in_documents_batch, - ) - .into(), - )); - } - - // Group transitions by contract ID - let mut document_transitions_by_contracts: BTreeMap< - Identifier, - Vec, - > = BTreeMap::new(); - - // Group transitions by contract ID - let mut token_transitions: Vec<&TokenTransition> = vec![]; - - self.transitions_iter_v1() - .for_each(|batch_transition| match batch_transition { - BatchedTransitionRefV1::Document(document_transition) => { - let contract_identifier = document_transition.data_contract_id(); - - match document_transitions_by_contracts.entry(contract_identifier) { - Entry::Vacant(vacant) => { - vacant.insert(vec![document_transition]); - } - Entry::Occupied(mut identifiers) => { - identifiers.get_mut().push(document_transition); - } - }; - } - BatchedTransitionRefV1::Token(token_transition) => { - token_transitions.push(token_transition) - } - }); - - let mut result = SimpleConsensusValidationResult::default(); - - for transitions in document_transitions_by_contracts.values() { - for transition in transitions { - // We need to make sure that the identity contract nonce is within the allowed bounds - // This means that it is stored on 40 bits - if transition.identity_contract_nonce() & MISSING_IDENTITY_REVISIONS_FILTER > 0 { - result.add_error(BasicError::NonceOutOfBoundsError( - NonceOutOfBoundsError::new(transition.identity_contract_nonce()), - )); - } - - // The kinds that joined the wire after the first batch formats - // are gated on the version bounds the active protocol version - // publishes for them. Software that knows a newer generation of - // a kind decodes it whatever the active protocol version, so - // this is where it agrees with software that does not. - match transition { - DocumentTransitionRefV1::IndexOnlyDelete(index_only_delete) => { - let feature_version = match index_only_delete { - DocumentIndexOnlyDeleteTransition::V0(_) => 0, - }; - check_kind_version_bounds( - feature_version, - platform_version - .dpp - .state_transition_serialization_versions - .document_index_only_delete_state_transition - .as_ref(), - &mut result, - ); - } - DocumentTransitionRefV1::Erase(erase) => { - let feature_version = match erase { - DocumentEraseTransition::V0(_) => 0, - }; - check_kind_version_bounds( - feature_version, - platform_version - .dpp - .state_transition_serialization_versions - .document_erase_state_transition - .as_ref(), - &mut result, - ); - } - _ => {} - } - } - - // Make sure we don't have duplicate transitions - let duplicate_transitions = find_duplicates_by_id(transitions); - - if !duplicate_transitions.is_empty() { - let references: Vec<(String, [u8; 32])> = duplicate_transitions - .into_iter() - .map(|transition| { - Ok(( - transition.base().document_type_name().clone(), - transition.base().id().to_buffer(), - )) - }) - .collect::, anyhow::Error>>()?; - - result.add_error(BasicError::DuplicateDocumentTransitionsWithIdsError( - DuplicateDocumentTransitionsWithIdsError::new(references), - )); - } - } - - for transition in token_transitions { - // We need to make sure that the identity contract nonce is within the allowed bounds - // This means that it is stored on 40 bits - if transition.identity_contract_nonce() & MISSING_IDENTITY_REVISIONS_FILTER > 0 { - result.add_error(BasicError::NonceOutOfBoundsError( - NonceOutOfBoundsError::new(transition.identity_contract_nonce()), - )); - } - - let transition_token_id = transition.base().token_id(); - let calculated_token_id = transition.base().calculate_token_id(); - - // We need to verify that the token id is correct - if transition_token_id != calculated_token_id { - result.add_error(BasicError::InvalidTokenIdError(InvalidTokenIdError::new( - calculated_token_id, - transition_token_id, - ))); - } - - let consensus_result = match transition { - TokenTransition::Burn(burn_transition) => { - burn_transition.validate_structure(platform_version)? - } - TokenTransition::Mint(mint_transition) => { - mint_transition.validate_structure(platform_version)? - } - TokenTransition::Transfer(transfer_transition) => { - transfer_transition.validate_structure(self.owner_id(), platform_version)? - } - TokenTransition::Freeze(freeze_transition) => { - freeze_transition.validate_structure(platform_version)? - } - TokenTransition::Unfreeze(unfreeze_transition) => { - unfreeze_transition.validate_structure(platform_version)? - } - TokenTransition::DestroyFrozenFunds(destroy_frozen_funds_transition) => { - destroy_frozen_funds_transition.validate_structure(platform_version)? - } - TokenTransition::EmergencyAction(emergency_action_transition) => { - emergency_action_transition.validate_structure(platform_version)? - } - TokenTransition::ConfigUpdate(config_update_transition) => { - config_update_transition.validate_structure(platform_version)? - } - TokenTransition::Claim(release_transition) => { - release_transition.validate_structure(platform_version)? - } - TokenTransition::DirectPurchase(direct_purchase_transition) => { - direct_purchase_transition.validate_structure(platform_version)? - } - TokenTransition::SetPriceForDirectPurchase( - set_price_for_direct_purchase_transition, - ) => { - set_price_for_direct_purchase_transition.validate_structure(platform_version)? - } - }; - - if !consensus_result.is_valid() { - return Ok(consensus_result); - } - - // We need to verify that the action id given matches the expected action id - // But only if we are the proposer - if let Some(group_state_transition_info) = transition.base().using_group_info() { - if group_state_transition_info.action_is_proposer { - if let Some(calculated_action_id) = transition - .calculate_action_id(self.owner_id(), platform_version) - .transpose()? - { - if group_state_transition_info.action_id != calculated_action_id { - result.add_error(BasicError::InvalidActionIdError( - InvalidActionIdError::new( - calculated_action_id, - group_state_transition_info.action_id, - ), - )); - } - } else { - result.add_error(BasicError::GroupActionNotAllowedOnTransitionError( - GroupActionNotAllowedOnTransitionError::new( - transition.action_type().to_string(), - ), - )); - } - } else if !transition.can_calculate_action_id() { - result.add_error(BasicError::GroupActionNotAllowedOnTransitionError( - GroupActionNotAllowedOnTransitionError::new( - transition.action_type().to_string(), - ), - )); - } - } - } - - Ok(result) - } -} - -#[derive(Hash, Eq, PartialEq, Ord, PartialOrd)] -struct TransitionFingerprint<'a> { - document_type: &'a str, - id: Identifier, -} - -impl<'a> From> for TransitionFingerprint<'a> { - fn from(transition: DocumentTransitionRefV1<'a>) -> Self { - let base = transition.base(); - - Self { - document_type: base.document_type_name(), - id: base.id(), - } - } -} - -/// Records an error when the active protocol version does not publish -/// `feature_version` of a document transition kind: an absent entry means the -/// kind does not exist at all at that version, which the empty range (min 1, -/// max 0) states. -fn check_kind_version_bounds( - feature_version: u16, - bounds: Option<&DocumentFeatureVersionBounds>, - result: &mut SimpleConsensusValidationResult, -) { - match bounds { - None => result.add_error(BasicError::UnsupportedVersionError( - UnsupportedVersionError::new(feature_version, 1, 0), - )), - Some(bounds) if !bounds.bounds.check_version(feature_version) => result.add_error( - BasicError::UnsupportedVersionError(UnsupportedVersionError::new( - feature_version, - bounds.bounds.min_version, - bounds.bounds.max_version, - )), - ), - Some(_) => {} - } -} - -/// Finds the duplicates among the document transitions of one contract. Two -/// transitions are duplicates when they name the same document type and -/// document id, whatever their kinds. Every duplicated transition is reported, -/// the first one once per later duplicate. -/// -/// The shared `find_duplicates_by_id` helper and its version slot take the -/// shell of batch formats 0 and 1, which cannot hold an erase, so this -/// generation carries its own finder over the shell of every format. A later -/// generation that needs a different rule forks this module. -fn find_duplicates_by_id<'a>( - document_transitions: &[DocumentTransitionRefV1<'a>], -) -> Vec> { - let mut fingerprints: BTreeMap = - BTreeMap::new(); - let mut duplicates: Vec = vec![]; - - for transition in document_transitions { - let fingerprint = (*transition).into(); - - match fingerprints.entry(fingerprint) { - Entry::Occupied(val) => { - duplicates.push(*val.get()); - duplicates.push(*transition); - } - Entry::Vacant(v) => { - v.insert(*transition); - } - } - } - - duplicates -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::consensus::ConsensusError; - use crate::state_transition::batch_transition::batched_transition::document_create_transition::v0::DocumentCreateTransitionV0; - use crate::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransition; - use crate::state_transition::batch_transition::batched_transition::document_delete_transition::v0::DocumentDeleteTransitionV0; - use crate::state_transition::batch_transition::batched_transition::document_delete_transition::DocumentDeleteTransition; - use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; - use crate::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionV1, DocumentEraseTransition, DocumentTransition, - DocumentTransitionV1, - }; - use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use crate::state_transition::batch_transition::{ - BatchTransition, BatchTransitionV0, BatchTransitionV1, BatchTransitionV2, - }; - use platform_value::BinaryData; - use std::collections::BTreeMap; - - fn make_base(nonce: u64, type_name: &str) -> DocumentBaseTransition { - DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Identifier::new([1u8; 32]), - identity_contract_nonce: nonce, - document_type_name: type_name.to_string(), - data_contract_id: Identifier::new([0xAA; 32]), - }) - } - - fn make_create(nonce: u64) -> DocumentTransition { - DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 { - base: make_base(nonce, "test_doc"), - entropy: [0u8; 32], - data: BTreeMap::new(), - prefunded_voting_balance: None, - })) - } - - fn make_delete(nonce: u64, id_byte: u8) -> DocumentTransition { - DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { - base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Identifier::new([id_byte; 32]), - identity_contract_nonce: nonce, - document_type_name: "test_doc".to_string(), - data_contract_id: Identifier::new([0xAA; 32]), - }), - })) - } - - fn make_batch_v0(transitions: Vec) -> BatchTransition { - BatchTransition::V0(BatchTransitionV0 { - owner_id: Identifier::new([0x01; 32]), - transitions, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::default(), - }) - } - - fn make_batch_v1_empty() -> BatchTransition { - BatchTransition::V1(BatchTransitionV1 { - owner_id: Identifier::new([0x02; 32]), - transitions: vec![], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::default(), - }) - } - - // ----------------------------------------------------------------------- - // batch format 2 — the only format whose shell carries the erase kind - // ----------------------------------------------------------------------- - - fn make_erase(id_byte: u8, nonce: u64) -> DocumentTransitionV1 { - DocumentTransitionV1::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { - base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Identifier::new([id_byte; 32]), - identity_contract_nonce: nonce, - document_type_name: "note".to_string(), - data_contract_id: Identifier::new([0xAA; 32]), - }), - })) - } - - fn make_batch_v2(transitions: Vec) -> BatchTransition { - BatchTransition::V2(BatchTransitionV2 { - owner_id: Identifier::new([0x03; 32]), - transitions: transitions - .into_iter() - .map(BatchedTransitionV1::Document) - .collect(), - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::default(), - }) - } - - #[test] - fn should_admit_an_erase_in_a_format_2_batch() { - let result = make_batch_v2(vec![make_erase(1, 1)]) - .validate_base_structure_v1(PlatformVersion::latest()) - .expect("no protocol err"); - - assert!(result.is_valid(), "expected valid, got {:?}", result.errors); - } - - #[test] - fn should_treat_an_erase_of_a_document_named_by_another_transition_as_a_duplicate() { - let first = make_erase(7, 1); - let second = DocumentTransitionV1::Create(DocumentCreateTransition::V0( - DocumentCreateTransitionV0 { - base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Identifier::new([7; 32]), - identity_contract_nonce: 2, - document_type_name: "note".to_string(), - data_contract_id: Identifier::new([0xAA; 32]), - }), - entropy: [0u8; 32], - data: BTreeMap::new(), - prefunded_voting_balance: None, - }, - )); - let other_type = make_create(3); - - let duplicates = find_duplicates_by_id(&[ - first.borrow_as_ref(), - second.borrow_as_ref(), - DocumentTransitionRefV1::from(&other_type), - ]); - - assert_eq!( - duplicates.len(), - 2, - "a batch may name one document at most once, whatever the kinds" - ); - assert!(find_duplicates_by_id(&[]).is_empty()); - } - - #[test] - fn should_reject_an_empty_format_2_batch() { - let result = make_batch_v2(vec![]) - .validate_base_structure_v1(PlatformVersion::latest()) - .expect("no protocol err"); - - assert!(matches!( - result.errors.first(), - Some(ConsensusError::BasicError( - BasicError::DocumentTransitionsAreAbsentError(_) - )) - )); - } - - /// Software that knows the erase kind decodes it whatever protocol version - /// is active, so the structure check has to refuse the kind wherever the - /// active version publishes no bounds for it; that is how such software - /// agrees with software that cannot decode it at all. - #[test] - fn should_gate_an_erase_on_the_bounds_the_protocol_version_publishes() { - let batch = make_batch_v2(vec![make_erase(1, 1)]); - let is_unsupported_version = |error: &ConsensusError| { - matches!( - error, - ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) - ) - }; - - let without_erase = PlatformVersion::get(14).expect("protocol version 14 exists"); - assert!( - without_erase - .dpp - .state_transition_serialization_versions - .document_erase_state_transition - .is_none(), - "the test needs a version that publishes no erase bounds" - ); - let result = batch - .validate_base_structure_v1(without_erase) - .expect("no protocol err"); - assert!( - result.errors.iter().any(is_unsupported_version), - "a version without erase bounds must refuse the kind, got {:?}", - result.errors - ); - - let result = batch - .validate_base_structure_v1(PlatformVersion::latest()) - .expect("no protocol err"); - assert!( - !result.errors.iter().any(is_unsupported_version), - "the latest version publishes erase bounds, got {:?}", - result.errors - ); - } - - /// A format 2 batch never reaches the released structure generation in - /// production, because decoding rejects it by wire version first. Should - /// one arrive anyway, the shell that generation reads through cannot see - /// an erase and must report the batch as empty, so the batch is refused - /// rather than passed on with its transitions dropped. - #[test] - fn should_fail_closed_on_a_format_2_batch_through_the_released_structure_generation() { - let batch = make_batch_v2(vec![make_erase(1, 1)]); - - let released = PlatformVersion::get(14).expect("protocol version 14 exists"); - assert_eq!( - released - .dpp - .state_transitions - .documents - .documents_batch_transition - .validation - .validate_base_structure, - 0, - "protocol version 14 selects the released structure generation" - ); - let result = batch - .validate_base_structure(released) - .expect("no protocol err"); - assert!(matches!( - result.errors.first(), - Some(ConsensusError::BasicError( - BasicError::DocumentTransitionsAreAbsentError(_) - )) - )); - - let result = batch - .validate_base_structure(PlatformVersion::latest()) - .expect("no protocol err"); - assert!(result.is_valid(), "expected valid, got {:?}", result.errors); - } - - #[test] - fn should_bound_the_nonce_of_an_erase_in_a_format_2_batch() { - let result = make_batch_v2(vec![make_erase(1, u64::MAX)]) - .validate_base_structure_v1(PlatformVersion::latest()) - .expect("no protocol err"); - - assert!( - result.errors.iter().any(|error| matches!( - error, - ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) - )), - "expected NonceOutOfBoundsError, got {:?}", - result.errors - ); - } - - // ----------------------------------------------------------------------- - // empty batch — DocumentTransitionsAreAbsentError - // ----------------------------------------------------------------------- - - #[test] - fn validate_base_structure_v1_errors_when_v0_empty() { - let pv = PlatformVersion::latest(); - let batch = make_batch_v0(vec![]); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(!result.is_valid()); - let errors = result.errors; - assert_eq!(errors.len(), 1); - match &errors[0] { - ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} - other => panic!( - "expected DocumentTransitionsAreAbsentError, got {:?}", - other - ), - } - } - - #[test] - fn validate_base_structure_v1_errors_when_v1_empty() { - let pv = PlatformVersion::latest(); - let batch = make_batch_v1_empty(); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(!result.is_valid()); - match &result.errors[0] { - ConsensusError::BasicError(BasicError::DocumentTransitionsAreAbsentError(_)) => {} - other => panic!( - "expected DocumentTransitionsAreAbsentError, got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // valid single transition with no group / nonce / duplicate issues - // ----------------------------------------------------------------------- - - #[test] - fn validate_base_structure_v1_passes_with_single_valid_transition() { - let pv = PlatformVersion::latest(); - let batch = make_batch_v0(vec![make_create(1)]); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(result.is_valid(), "expected valid, got {:?}", result.errors); - } - - // ----------------------------------------------------------------------- - // nonce out of bounds — high bits set above 40-bit cap - // ----------------------------------------------------------------------- - - #[test] - fn validate_base_structure_v1_errors_on_nonce_with_high_bits_set() { - let pv = PlatformVersion::latest(); - // MISSING_IDENTITY_REVISIONS_FILTER masks the top bits used to mark - // a missing revision. Setting a value above 40 bits triggers the - // NonceOutOfBoundsError path. - let bad_nonce: u64 = u64::MAX; - let batch = make_batch_v0(vec![make_delete(bad_nonce, 7)]); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(!result.is_valid()); - let has_nonce_err = result.errors.iter().any(|e| { - matches!( - e, - ConsensusError::BasicError(BasicError::NonceOutOfBoundsError(_)) - ) - }); - assert!( - has_nonce_err, - "expected NonceOutOfBoundsError, got {:?}", - result.errors - ); - } - - // ----------------------------------------------------------------------- - // max-transitions-exceeded — early-return before any other checks - // ----------------------------------------------------------------------- - - #[test] - fn validate_base_structure_v1_errors_when_transitions_exceed_max() { - // The latest platform version caps the batch at a small number of - // transitions. Going over should produce a single - // MaxDocumentsTransitionsExceededError and NO other errors (the - // function early-returns). - let pv = PlatformVersion::latest(); - let max = pv.system_limits.max_transitions_in_documents_batch as usize; - let mut transitions = Vec::with_capacity(max + 1); - // Use distinct ids to avoid duplicate noise — but we never reach the - // duplicate-detection path anyway because we early-return. - for i in 0..(max + 1) { - transitions.push(make_delete(i as u64 + 1, i as u8)); - } - let batch = make_batch_v0(transitions); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(!result.is_valid()); - assert_eq!(result.errors.len(), 1, "should early-return with one error"); - match &result.errors[0] { - ConsensusError::BasicError(BasicError::MaxDocumentsTransitionsExceededError(_)) => {} - other => panic!( - "expected MaxDocumentsTransitionsExceededError, got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // V1 batch with only document transitions — same logic, takes the - // BatchedTransitionRef::Document arm of the iterator instead. - // ----------------------------------------------------------------------- - - #[test] - fn validate_base_structure_v1_passes_for_v1_with_documents_only() { - let pv = PlatformVersion::latest(); - let batch = BatchTransition::V1(BatchTransitionV1 { - owner_id: Identifier::new([0x02; 32]), - transitions: vec![BatchedTransition::Document(make_create(1))], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: BinaryData::default(), - }); - let result = batch - .validate_base_structure_v1(pv) - .expect("no protocol err"); - assert!(result.is_valid(), "expected valid, got {:?}", result.errors); - } -} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs index ed85796ecb7..be7d07021e1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/version.rs @@ -7,7 +7,6 @@ impl FeatureVersioned for BatchTransition { match self { BatchTransition::V0(v0) => v0.feature_version(), BatchTransition::V1(v1) => v1.feature_version(), - BatchTransition::V2(v2) => v2.feature_version(), } } } diff --git a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs index 721af904313..86e0015c68f 100644 --- a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs @@ -290,27 +290,6 @@ impl ExecutionEvent<'_> { ))) } } - StateTransitionAction::BatchActionV1(batch_action) => { - let user_fee_increase = action.user_fee_increase(); - let removed_balance = batch_action.all_used_balances()?; - let operations = - action.into_high_level_drive_operations(epoch, platform_version)?; - if let Some(identity) = identity { - Ok(ExecutionEvent::Paid { - identity, - removed_balance, - added_to_balance_outputs: None, - operations, - execution_operations: execution_context.operations_consume(), - additional_fixed_fee_cost: None, - user_fee_increase, - }) - } else { - Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "partial identity should be present for other state transitions", - ))) - } - } StateTransitionAction::MasternodeVoteAction(_) => { let operations = action.into_high_level_drive_operations(epoch, platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs index 008be12cc67..9a1925de7fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/mod.rs @@ -1,2 +1 @@ pub(crate) mod v0; -pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs index 0eafc05f797..0fc65514003 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v0/mod.rs @@ -20,11 +20,13 @@ use dpp::version::PlatformVersion; use drive::state_transition_action::batch::BatchTransitionAction; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionActionAccessorsV0; @@ -217,6 +219,19 @@ impl DocumentsBatchStateTransitionStructureValidationV0 for BatchTransition { )); } } + DocumentTransitionAction::EraseAction(erase_action) => { + let result = erase_action.validate_structure(platform_version)?; + if !result.is_valid() { + let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( + BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(erase_action.base(), self.owner_id(), self.user_fee_increase()), + ); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + result.errors, + )); + } + } DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { let result = index_only_delete_action.validate_structure(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs deleted file mode 100644 index abe20464afd..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs +++ /dev/null @@ -1,284 +0,0 @@ -use crate::error::Error; -use dpp::block::block_info::BlockInfo; -use dpp::consensus::basic::document::InvalidDocumentTransitionIdError; -use dpp::consensus::signature::{InvalidSignaturePublicKeySecurityLevelError, SignatureError}; -use dpp::dashcore::Network; -use dpp::document::Document; -use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dpp::identity::PartialIdentity; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use dpp::state_transition::batch_transition::BatchTransition; -use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionIdentitySigned, StateTransitionOwned}; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRefV1, DocumentTransitionRefV1}; -use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; -use dpp::validation::ConsensusValidationResult; - -use dpp::version::PlatformVersion; - -use drive::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::state_transitions::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; -use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::DocumentTransferTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::DocumentUpdatePriceTransitionActionAccessorsV0; -use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use drive::state_transition_action::StateTransitionAction; -use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; -use crate::error::execution::ExecutionError; -use crate::execution::types::execution_operation::ValidationOperation; -use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; -use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_transfer_transition_action::DocumentTransferTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_update_price_transition_action::DocumentUpdatePriceTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; - -/// Generation 1 validates the structure of batch action format 1, which is -/// what a batch of any wire format becomes for protocol version 15 and later, -/// including the erase of a keep-history document. -pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStructureValidationV1 -{ - fn validate_advanced_structure_from_state_v1( - &self, - block_info: &BlockInfo, - network: Network, - action: &BatchTransitionActionV1, - identity: &PartialIdentity, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error>; -} - -impl DocumentsBatchStateTransitionStructureValidationV1 for BatchTransition { - fn validate_advanced_structure_from_state_v1( - &self, - block_info: &BlockInfo, - network: Network, - action: &BatchTransitionActionV1, - identity: &PartialIdentity, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let security_levels = action.combined_security_level_requirement()?; - - let signing_key = identity.loaded_public_keys.get(&self.signature_public_key_id()).ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution("the key must exist for advanced structure validation as we already fetched it during signature validation")))?; - - if !security_levels.contains(&signing_key.security_level()) { - // We only need to bump the first identity data contract nonce as that will make a replay - // attack not possible - - let first_transition = self.first_transition_v1().ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution("There must be at least one state transition as this is already verified in basic validation")))?; - - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_batched_transition_ref_v1( - first_transition, - self.owner_id(), - self.user_fee_increase(), - ), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - vec![SignatureError::InvalidSignaturePublicKeySecurityLevelError( - InvalidSignaturePublicKeySecurityLevelError::new( - signing_key.security_level(), - security_levels, - ), - ) - .into()], - )); - } - - // We should validate that all newly created documents have valid ids - for transition in self.transitions_iter_v1() { - if let BatchedTransitionRefV1::Document(DocumentTransitionRefV1::Create( - create_transition, - )) = transition - { - // Validate the ID - let generated_document_id = Document::generate_document_id_v0( - create_transition.base().data_contract_id_ref(), - &self.owner_id(), - create_transition.base().document_type_name(), - &create_transition.entropy(), - ); - - // This hash will take 2 blocks (128 bytes) - execution_context.add_operation(ValidationOperation::DoubleSha256(2)); - - let id = create_transition.base().id(); - if generated_document_id != id { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( - create_transition.base(), - self.owner_id(), - self.user_fee_increase(), - ), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - vec![ - InvalidDocumentTransitionIdError::new(generated_document_id, id).into(), - ], - )); - } - } - } - - // Next we need to validate the structure of all actions (this means with the data contract) - for transition in action.transitions() { - match transition { - BatchedTransitionActionV1::DocumentErase(erase_action) => { - let result = erase_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(erase_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( - document_action, - )) => match document_action { - DocumentTransitionAction::CreateAction(create_action) => { - let result = create_action.validate_structure( - identity.id, - block_info, - network, - platform_version, - )?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(document_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::ReplaceAction(replace_action) => { - let result = replace_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(replace_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::DeleteAction(delete_action) => { - let result = delete_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(delete_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::TransferAction(transfer_action) => { - let result = transfer_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(transfer_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::UpdatePriceAction(update_price_action) => { - let result = update_price_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(update_price_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::PurchaseAction(purchase_action) => { - let result = purchase_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(purchase_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { - let result = - index_only_delete_action.validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action(index_only_delete_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - }, - BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( - token_transition_action, - )) => { - // token actions only need to do advanced structure validation on the base action - let result = token_transition_action - .base() - .validate_structure(platform_version)?; - if !result.is_valid() { - let bump_action = StateTransitionAction::BumpIdentityDataContractNonceAction( - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition_action(token_transition_action.base(), self.owner_id(), self.user_fee_increase()), - ); - - return Ok(ConsensusValidationResult::new_with_data_and_errors( - bump_action, - result.errors, - )); - } - } - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(_), - ) => { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "we should not have a bump identity contract nonce at this stage", - ))); - } - } - } - Ok(ConsensusValidationResult::new()) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs index 008be12cc67..9a1925de7fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/mod.rs @@ -1,2 +1 @@ pub(crate) mod v0; -pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs deleted file mode 100644 index 679be90040e..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/identity_contract_nonce/v1/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::error::Error; -use dpp::block::block_info::BlockInfo; -use dpp::identity::identity_nonce::{validate_identity_nonce_update, validate_new_identity_nonce}; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; - -use dpp::state_transition::batch_transition::BatchTransition; -use dpp::state_transition::StateTransitionOwned; -use dpp::validation::SimpleConsensusValidationResult; - -use crate::execution::types::execution_operation::ValidationOperation; -use crate::execution::types::state_transition_execution_context::{ - StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, -}; -use crate::platform_types::platform::PlatformStateRef; -use dpp::version::PlatformVersion; -use drive::grovedb::TransactionArg; - -/// Generation 1 reads the identity contract nonces of a batch of any wire -/// format, including format 2, whose document shell carries the erase kind. -pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionIdentityContractNonceV1 -{ - fn validate_identity_contract_nonces_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - tx: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result; -} - -impl DocumentsBatchStateTransitionIdentityContractNonceV1 for BatchTransition { - fn validate_identity_contract_nonces_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - tx: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result { - for transition in self.transitions_iter_v1() { - let revision_nonce = transition.identity_contract_nonce(); - let identity_id = self.owner_id(); - let (existing_nonce, fee) = platform.drive.fetch_identity_contract_nonce_with_fees( - identity_id.to_buffer(), - transition.data_contract_id().to_buffer(), - block_info, - true, - tx, - platform_version, - )?; - - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); - - let result = if let Some(existing_nonce) = existing_nonce { - validate_identity_nonce_update(existing_nonce, revision_nonce, identity_id) - } else { - validate_new_identity_nonce(revision_nonce, identity_id) - }; - if !result.is_valid() { - return Ok(result); - } - } - - Ok(SimpleConsensusValidationResult::new()) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs index 68637971e6e..9b55207150b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/mod.rs @@ -7,7 +7,6 @@ use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; mod v0; -mod v1; impl StateTransitionIsAllowedValidationV0 for BatchTransition { fn has_is_allowed_validation(&self) -> Result { @@ -28,10 +27,9 @@ impl StateTransitionIsAllowedValidationV0 for BatchTransition { .is_allowed { 0 => Ok(v0::validate_is_allowed_v0(self, platform)), - 1 => Ok(v1::validate_is_allowed_v1(self, platform)), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "StateTransition::validate_is_allowed".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs deleted file mode 100644 index 12a1ef487b2..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/is_allowed/v1/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::platform_types::platform::PlatformRef; -use crate::platform_types::platform_state::PlatformStateV0Methods; -use dpp::consensus::basic::document::ContestedDocumentsTemporarilyNotAllowedError; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use dpp::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; -use dpp::state_transition::batch_transition::BatchTransition; -use dpp::validation::ConsensusValidationResult; - -use crate::execution::validation::state_transition::batch::is_allowed::v0::TARGET_EPOCH_INDEX; - -/// Generation 1 reads a batch of any wire format, including format 2, whose -/// document shell carries the erase kind. The rule is unchanged: contested -/// document creation is not allowed before the target epoch. -#[inline(always)] -pub fn validate_is_allowed_v1( - state_transition: &BatchTransition, - platform: &PlatformRef, -) -> ConsensusValidationResult<()> { - #[cfg(feature = "testing-config")] - if platform - .config - .testing_configs - .disable_contested_documents_is_allowed_validation - { - return ConsensusValidationResult::new(); - } - - let block_info = platform.state.last_block_info(); - - if block_info.epoch.index >= TARGET_EPOCH_INDEX { - return ConsensusValidationResult::new(); - } - - let is_contested = state_transition.transitions_iter_v1().any(|transition| { - transition - .as_transition_create() - .and_then(|create| create.prefunded_voting_balance().as_ref()) - .is_some() - }); - - if is_contested { - return ConsensusValidationResult::new_with_errors(vec![ - ContestedDocumentsTemporarilyNotAllowedError::new( - block_info.epoch.index, - TARGET_EPOCH_INDEX, - ) - .into(), - ]); - } - - ConsensusValidationResult::new() -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index ac4f6bd3341..c1fef559bd1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -31,13 +31,9 @@ use crate::platform_types::platform::{PlatformRef, PlatformStateRef}; use crate::rpc::core::CoreRPCLike; use crate::execution::validation::state_transition::batch::advanced_structure::v0::DocumentsBatchStateTransitionStructureValidationV0; -use crate::execution::validation::state_transition::batch::advanced_structure::v1::DocumentsBatchStateTransitionStructureValidationV1; use crate::execution::validation::state_transition::batch::identity_contract_nonce::v0::DocumentsBatchStateTransitionIdentityContractNonceV0; -use crate::execution::validation::state_transition::batch::identity_contract_nonce::v1::DocumentsBatchStateTransitionIdentityContractNonceV1; use crate::execution::validation::state_transition::batch::state::v0::DocumentsBatchStateTransitionStateValidationV0; use crate::execution::validation::state_transition::batch::state::v1::DocumentsBatchStateTransitionStateValidationV1; -use crate::execution::validation::state_transition::batch::state::v2::DocumentsBatchStateTransitionActionTransformV2; -use crate::execution::validation::state_transition::batch::state::validation_v1::DocumentsBatchStateTransitionValidationV1; use crate::execution::validation::state_transition::processor::advanced_structure_with_state::StateTransitionStructureKnownInStateValidationV0; use crate::execution::validation::state_transition::processor::basic_structure::StateTransitionBasicStructureValidationV0; use crate::execution::validation::state_transition::processor::identity_nonces::StateTransitionIdentityNonceValidationV0; @@ -94,16 +90,9 @@ impl StateTransitionActionTransformer for BatchTransition { execution_context, tx, ), - 2 => self.transform_into_action_v2( - &platform.into(), - block_info, - validation_mode, - execution_context, - tx, - ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: transform_into_action".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, })), } @@ -160,16 +149,9 @@ impl StateTransitionIdentityNonceValidationV0 for BatchTransition { execution_context, platform_version, ), - 1 => self.validate_identity_contract_nonces_v1( - platform, - block_info, - tx, - execution_context, - platform_version, - ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: revision".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } @@ -213,30 +195,9 @@ impl StateTransitionStructureKnownInStateValidationV0 for BatchTransition { platform_version, ) } - 1 => { - let identity = - identity.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( - "The identity must be known on advanced structure validation", - )))?; - let StateTransitionAction::BatchActionV1(documents_batch_transition_action) = - action - else { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "action must be a documents batch transition action in format 1", - ))); - }; - self.validate_advanced_structure_from_state_v1( - block_info, - network, - documents_batch_transition_action, - identity, - execution_context, - platform_version, - ) - } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: advanced structure from state".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } @@ -290,30 +251,9 @@ impl StateTransitionStateValidation for BatchTransition { platform_version, ) } - 1 => { - let action = - action.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( - "documents batch structure validation should have an action", - )))?; - let StateTransitionAction::BatchActionV1(documents_batch_transition_action) = - action - else { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "action must be a documents batch transition action in format 1", - ))); - }; - self.validate_state_v1( - documents_batch_transition_action, - &platform.into(), - block_info, - execution_context, - tx, - platform_version, - ) - } version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: validate_state".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs index 71c5e28a336..c464bb94fa5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs @@ -1,5 +1,3 @@ pub(crate) mod fetch_keep_history_document_lifecycle; pub(crate) mod v0; pub(crate) mod v1; -pub(crate) mod v2; -pub(crate) mod validation_v1; 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 d8fd10e8e95..2486f1574c4 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,4 +1,3 @@ -use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::{ @@ -13,7 +12,6 @@ use dpp::platform_value::{Identifier, Value}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionRefV1; use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; @@ -211,123 +209,6 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( )) } -/// Like [`fetch_documents_for_transitions_knowing_contract_and_document_type`], -/// for transitions viewed through the shell that sees every batch wire format. -/// -/// The helper has its own version slot, which is dormant at the protocol -/// versions whose batches only exist in wire formats 0 and 1; those reach the -/// sibling function. -#[allow(clippy::too_many_arguments)] -pub(crate) fn fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type( - drive: &Drive, - contract: &DataContract, - document_type: DocumentTypeRef, - transitions: &[DocumentTransitionRefV1<'_>], - epoch: &Epoch, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, -) -> Result>, Error> { - match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type - { - Some(0) => { - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type_v0( - drive, - contract, - document_type, - transitions, - epoch, - execution_context, - transaction, - platform_version, - ) - } - Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { - method: - "fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type" - .to_string(), - known_versions: vec![0], - received: version, - })), - None => Err(Error::Execution(ExecutionError::VersionNotActive { - method: - "fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type" - .to_string(), - known_versions: vec![0], - })), - } -} - -/// Bills like generation 1 of the sibling helper, for transitions of any batch -/// wire format. -#[allow(clippy::too_many_arguments)] -fn fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type_v0( - drive: &Drive, - contract: &DataContract, - document_type: DocumentTypeRef, - transitions: &[DocumentTransitionRefV1<'_>], - epoch: &Epoch, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, -) -> Result>, Error> { - if transitions.is_empty() { - return Ok(ConsensusValidationResult::new_with_data(vec![])); - } - - let ids: Vec = transitions - .iter() - .map(|dt| Value::Identifier(dt.get_id().to_buffer())) - .collect(); - - let drive_query = DriveDocumentQuery { - contract, - document_type, - internal_clauses: InternalClauses { - primary_key_in_clause: Some(WhereClause { - field: "$id".to_string(), - operator: WhereOperator::In, - value: Value::Array(ids), - }), - primary_key_equal_clause: None, - in_clauses: Vec::new(), - range_clause: None, - equal_clauses: Default::default(), - }, - offset: None, - limit: Some(transitions.len() as u16), - order_by: Default::default(), - start_at: None, - start_at_included: false, - block_time_ms: None, - resolved_time_ranges: vec![], - sub_queries: vec![], - }; - - let documents_outcome = drive.query_documents( - drive_query, - Some(epoch), - false, - transaction, - Some(platform_version.protocol_version), - )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { - storage_fee: 0, - processing_fee: documents_outcome.cost(), - fee_refunds: Default::default(), - removed_bytes_from_system: 0, - })); - - Ok(ConsensusValidationResult::new_with_data( - documents_outcome.documents_owned(), - )) -} - // ============================================================================ // fetch_document_with_id // ============================================================================ diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 4fd93181846..c2de31db690 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -17,6 +17,7 @@ use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; @@ -144,6 +145,15 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, + DocumentTransitionAction::EraseAction(erase_action) => erase_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?, DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { index_only_delete_action.validate_state( platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs deleted file mode 100644 index 78e55531a3d..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v2/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::error::Error; -use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; -use crate::execution::validation::state_transition::state_transitions::batch::transformer::v1::BatchTransitionTransformerV1; -use crate::execution::validation::state_transition::ValidationMode; -use crate::platform_types::platform::PlatformStateRef; -use dpp::block::block_info::BlockInfo; -use dpp::prelude::ConsensusValidationResult; -use dpp::state_transition::batch_transition::BatchTransition; -use drive::grovedb::TransactionArg; -use drive::state_transition_action::StateTransitionAction; - -/// Generation 2 of the batch transformation, selected for protocol version 15 -/// and later: like generation 1 it threads the caller's execution context into -/// the transformer, and it runs transformer generation 1, which sees every batch -/// wire format and produces batch action format 1. -pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionActionTransformV2 -{ - fn transform_into_action_v2( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - validation_mode: ValidationMode, - execution_context: &mut StateTransitionExecutionContext, - tx: TransactionArg, - ) -> Result, Error>; -} - -impl DocumentsBatchStateTransitionActionTransformV2 for BatchTransition { - fn transform_into_action_v2( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - validation_mode: ValidationMode, - execution_context: &mut StateTransitionExecutionContext, - tx: TransactionArg, - ) -> Result, Error> { - let validation_result = self.try_into_action_v1( - platform, - block_info, - validation_mode.should_validate_batch_valid_against_state(), - tx, - execution_context, - )?; - - Ok(validation_result.map(StateTransitionAction::BatchActionV1)) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs deleted file mode 100644 index 603d491e2c5..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/validation_v1/mod.rs +++ /dev/null @@ -1,348 +0,0 @@ -use dpp::block::block_info::BlockInfo; -use dpp::consensus::ConsensusError; -use dpp::consensus::state::state_error::StateError; -use dpp::prelude::ConsensusValidationResult; -use dpp::state_transition::batch_transition::BatchTransition; -use dpp::state_transition::StateTransitionOwned; -use drive::state_transition_action::StateTransitionAction; -use dpp::version::PlatformVersion; -use drive::grovedb::TransactionArg; -use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; -use drive::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; -use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; -use crate::error::Error; -use crate::error::execution::ExecutionError; -use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; -use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_erase_transition_action::DocumentEraseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_replace_transition_action::DocumentReplaceTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_transfer_transition_action::DocumentTransferTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::document::document_update_price_transition_action::DocumentUpdatePriceTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_burn_transition_action::TokenBurnTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_claim_transition_action::TokenClaimTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_config_update_transition_action::TokenConfigUpdateTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_destroy_frozen_funds_transition_action::TokenDestroyFrozenFundsTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_emergency_action_transition_action::TokenEmergencyActionTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_freeze_transition_action::TokenFreezeTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_mint_transition_action::TokenMintTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_transfer_transition_action::TokenTransferTransitionActionValidation; -use crate::execution::validation::state_transition::batch::action_validation::token::token_unfreeze_transition_action::TokenUnfreezeTransitionActionValidation; -use crate::execution::validation::state_transition::batch::data_triggers::{data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutor}; -use crate::platform_types::platform::PlatformStateRef; - -/// Generation 1 validates batch action format 1 against state, which is what a -/// batch of any wire format becomes for protocol version 15 and later, -/// including the erase of a keep-history document. -pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionValidationV1 -{ - fn validate_state_v1( - &self, - action: BatchTransitionActionV1, - platform: &PlatformStateRef, - block_info: &BlockInfo, - execution_context: &mut StateTransitionExecutionContext, - tx: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, Error>; -} - -impl DocumentsBatchStateTransitionValidationV1 for BatchTransition { - fn validate_state_v1( - &self, - mut state_transition_action: BatchTransitionActionV1, - platform: &PlatformStateRef, - block_info: &BlockInfo, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let mut validation_result = ConsensusValidationResult::::new(); - - let owner_id = state_transition_action.owner_id(); - - let mut validated_transitions = vec![]; - - let data_trigger_bindings = if platform.config.execution.use_document_triggers { - data_trigger_bindings_list(platform_version)? - } else { - vec![] - }; - - // Next we need to validate the structure of all actions (this means with the data contract) - for transition in state_transition_action.transitions_take() { - let transition_validation_result = match &transition { - BatchedTransitionActionV1::DocumentErase(erase_action) => erase_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( - document_action, - )) => match document_action { - DocumentTransitionAction::CreateAction(create_action) => create_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - DocumentTransitionAction::ReplaceAction(replace_action) => replace_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - DocumentTransitionAction::TransferAction(transfer_action) => transfer_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - DocumentTransitionAction::DeleteAction(delete_action) => delete_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - DocumentTransitionAction::UpdatePriceAction(update_price_action) => { - update_price_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )? - } - DocumentTransitionAction::PurchaseAction(purchase_action) => purchase_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - DocumentTransitionAction::IndexOnlyDeleteAction(index_only_delete_action) => { - index_only_delete_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )? - } - }, - BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( - token_action, - )) => match token_action { - TokenTransitionAction::BurnAction(burn_action) => burn_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::MintAction(mint_action) => mint_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::TransferAction(transfer_action) => transfer_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::FreezeAction(freeze_action) => freeze_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::UnfreezeAction(unfreeze_action) => unfreeze_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::EmergencyActionAction(emergency_action_action) => { - emergency_action_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )? - } - TokenTransitionAction::DestroyFrozenFundsAction( - destroy_frozen_funds_action, - ) => destroy_frozen_funds_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::ConfigUpdateAction(config_update_action) => { - config_update_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )? - } - TokenTransitionAction::ClaimAction(claim_action) => claim_action - .validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - TokenTransitionAction::DirectPurchaseAction(direct_purchase_action) => { - direct_purchase_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )? - } - TokenTransitionAction::SetPriceForDirectPurchaseAction( - set_price_for_direct_purchase_action, - ) => set_price_for_direct_purchase_action.validate_state( - platform, - owner_id, - block_info, - execution_context, - transaction, - platform_version, - )?, - }, - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(_), - ) => { - return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( - "we should never start with a bump identity data contract nonce", - ))); - } - }; - - if !transition_validation_result.is_valid() { - // If a state transition isn't valid we still need to bump the identity data contract nonce - validation_result.add_errors(transition_validation_result.errors); - validated_transitions.push(BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce( - BumpIdentityDataContractNonceAction::try_from_borrowed_batched_transition_action_v1( - &transition, - owner_id, - state_transition_action.user_fee_increase(), - )?, - ), - )); - } else if platform.config.execution.use_document_triggers { - // An erase has no data triggers: it carries no document values - // and acts on a document that is already deleted. - if let BatchedTransitionActionV1::Batched( - BatchedTransitionAction::DocumentAction(document_transition), - ) = &transition - { - // The trigger context borrows the outer execution - // context so the triggers' billing and the real `dry_run` - // flag reach the processor. - let owner_id_value = self.owner_id(); - let mut data_trigger_execution_context = DataTriggerExecutionContext { - platform, - block_info, - transaction, - owner_id: &owner_id_value, - state_transition_execution_context: execution_context, - }; - let data_trigger_execution_result = document_transition - .validate_with_data_triggers( - &data_trigger_bindings, - &mut data_trigger_execution_context, - platform_version, - )?; - - if !data_trigger_execution_result.is_valid() { - // If a state transition isn't valid because of data triggers we still need - // to bump the identity data contract nonce - let consensus_errors: Vec = data_trigger_execution_result - .errors - .into_iter() - .map(|e| ConsensusError::StateError(StateError::DataTriggerError(e))) - .collect(); - validation_result.add_errors(consensus_errors); - validated_transitions.push(BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce( - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action( - document_transition.base(), - owner_id, - state_transition_action.user_fee_increase(), - ), - ), - )); - } else { - validated_transitions.push(transition); - } - } else { - validated_transitions.push(transition); - } - } else { - validated_transitions.push(transition); - } - } - - state_transition_action.set_transitions(validated_transitions); - - validation_result.set_data(StateTransitionAction::BatchActionV1( - state_transition_action, - )); - - Ok(validation_result) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs index bcf204167bd..d7d4249a681 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/creation.rs @@ -5052,7 +5052,7 @@ mod creation_tests { assert_matches!( documents_batch_create_transition, - StateTransition::Batch(BatchTransition::V2(_)) + StateTransition::Batch(BatchTransition::V1(_)) ); let documents_batch_create_serialized_transition = documents_batch_create_transition diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 89ad23eedda..297548bef21 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -16,7 +16,7 @@ use dpp::document::Document; use dpp::identifier::Identifier; use dpp::identity::{Identity, IdentityPublicKey, SecurityLevel}; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::StateTransition; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::TokenPaymentInfo; @@ -252,8 +252,8 @@ impl Fixture { self.nonce, 0, &self.owner_signer, - None, platform_version, + None, ) .await .expect("expected an erase transition"); @@ -267,8 +267,8 @@ impl Fixture { self.stranger_nonce, 0, &self.stranger_signer, - None, platform_version, + None, ) .await .expect("expected an erase transition"); @@ -347,8 +347,8 @@ impl Fixture { nonce, 0, signer, - None, platform_version, + None, ) .await .expect("expected an erase transition") @@ -427,27 +427,27 @@ async fn rewrite_delete_as_erase( ) -> StateTransition { use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransitionV1, DocumentEraseTransition, DocumentTransitionV1, + BatchedTransition, DocumentEraseTransition, DocumentTransition, }; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::BatchTransition; - let StateTransition::Batch(BatchTransition::V2(mut batch)) = transition else { - panic!("expected a v2 batch transition"); + let StateTransition::Batch(BatchTransition::V1(mut batch)) = transition else { + panic!("expected a format 1 batch transition"); }; - let BatchedTransitionV1::Document(DocumentTransitionV1::Delete(delete)) = + let BatchedTransition::Document(DocumentTransition::Delete(delete)) = batch.transitions.remove(0) else { panic!("expected a single document delete"); }; batch .transitions - .push(BatchedTransitionV1::Document(DocumentTransitionV1::Erase( + .push(BatchedTransition::Document(DocumentTransition::Erase( DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: delete.base().clone(), }), ))); - let mut rebuilt: StateTransition = BatchTransition::V2(batch).into(); + let mut rebuilt: StateTransition = BatchTransition::V1(batch).into(); rebuilt .sign_external( key, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 74aa1edd5a8..016d968b111 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -2609,8 +2609,7 @@ mod test_suite { use drive::drive::Drive; use simple_signer::signer::SimpleSigner; use std::sync::Arc; - use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRefV1; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; @@ -2983,22 +2982,20 @@ mod test_suite { }; // --- Extract TokenTransition from claim_transition --- - let StateTransition::Batch(batch) = claim_transition else { - return Err("expected a batch transition".to_string()); + let StateTransition::Batch(BatchTransition::V1(batch_v1)) = claim_transition else { + return Err("expected BatchTransition::V1".to_string()); }; // Assume only one transition was in the batch - if batch.transitions_len_v1() != 1 { + let [batched_transition] = batch_v1.transitions.as_slice() else { return Err(format!( "expected exactly one batched transition, got {}", - batch.transitions_len_v1() + batch_v1.transitions.len() )); - } + }; - let Some(BatchedTransitionRefV1::Token(token_transition)) = - batch.first_transition_v1() - else { - return Err("expected a token transition".to_string()); + let BatchedTransition::Token(token_transition) = batched_transition else { + return Err("expected BatchedTransition::Token".to_string()); }; // --- Now build expected document --- diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs index 487fdac4808..694ae41fabd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs @@ -8,10 +8,10 @@ mod token_transfer_tests { use dpp::state_transition::batch_transition::TokenMintTransition; use dpp::data_contract::group::v0::GroupV0; use dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; - use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; + use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::{GetDataContractSecurityLevelRequirementFn, StateTransition}; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionMutRefV1; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionMutRef; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::identity::KeyType; @@ -1230,9 +1230,9 @@ mod token_transfer_tests { // here we add fake info if let StateTransition::Batch(batch) = &mut token_transfer_transition { let first_transition = batch - .first_transition_mut_v1() + .first_transition_mut() .expect("expected_first_transition"); - if let BatchedTransitionMutRefV1::Token(TokenTransition::Transfer(transfer)) = + if let BatchedTransitionMutRef::Token(TokenTransition::Transfer(transfer)) = first_transition { transfer diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs index 008be12cc67..9a1925de7fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/mod.rs @@ -1,2 +1 @@ pub(crate) mod v0; -pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 01480cab7fe..1634c94baa6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -56,6 +56,7 @@ use dpp::state_transition::batch_transition::batched_transition::document_purcha use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; +use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionAction; use drive::state_transition_action::batch::BatchTransitionAction; @@ -856,6 +857,16 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } + DocumentTransition::Erase(document_erase_transition) => { + let (batched_action, fee_result) = DocumentEraseTransitionAction::try_from_document_borrowed_erase_transition_with_contract_lookup(document_erase_transition, owner_id, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + })?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } DocumentTransition::IndexOnlyDelete(document_index_only_delete_transition) => { let (batched_action, fee_result) = DocumentIndexOnlyDeleteTransitionAction::try_from_document_borrowed_index_only_delete_transition_with_contract_lookup(document_index_only_delete_transition, owner_id, user_fee_increase, |_identifier| { Ok(data_contract_fetch_info.clone()) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs deleted file mode 100644 index 38f2ec9e912..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v1/mod.rs +++ /dev/null @@ -1,1148 +0,0 @@ -// Generation 1 of the batch transformer, selected for protocol version 15 and -// later. It views the batch through the shell that sees every batch wire -// format, so a format 2 batch carrying an erase of a keep-history document is -// transformed like any other, and it produces batch action format 1, whose -// items may include that erase. -// -// The aggregator helpers `ConsensusValidationResult::flatten` / -// `merge_many` are versioned via `dpp.validation.validation_result` on -// `PlatformVersion`. v0 (PROTOCOL_VERSION_11 and below) preserves the -// legacy `Some(empty_vec)`-on-no-data behavior for chain reproducibility; -// v1 (PROTOCOL_VERSION_12+) returns `data: None` in that case so an -// all-failed batch flows down the unpaid path. See issue #2867. - -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; -use std::sync::Arc; - -use crate::error::Error; -use crate::platform_types::platform::PlatformStateRef; -use dpp::consensus::basic::document::{DataContractNotPresentError, InvalidDocumentTypeError}; -use dpp::consensus::basic::value_error::ValueError; -use dpp::consensus::basic::BasicError; - -use dpp::consensus::state::document::document_not_found_error::DocumentNotFoundError; -use dpp::consensus::state::document::document_owner_id_mismatch_error::DocumentOwnerIdMismatchError; - -use dpp::consensus::state::document::invalid_document_revision_error::InvalidDocumentRevisionError; -use dpp::consensus::state::state_error::StateError; -use dpp::data_contract::accessors::v0::DataContractV0Getters; - -use dpp::block::block_info::BlockInfo; -use dpp::consensus::state::document::document_incorrect_purchase_price_error::DocumentIncorrectPurchasePriceError; -use dpp::consensus::state::document::document_not_for_sale_error::DocumentNotForSaleError; -use dpp::document::property_names::PRICE; -use dpp::document::{Document, DocumentV0Getters}; -use dpp::fee::Credits; -use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; -use dpp::prelude::{Revision, UserFeeIncrease}; -use dpp::validation::SimpleConsensusValidationResult; -use dpp::{consensus::ConsensusError, prelude::Identifier, validation::ConsensusValidationResult}; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransitionRefV1, DocumentTransitionRefV1, -}; -use dpp::state_transition::batch_transition::BatchTransition; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; -use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; -use dpp::state_transition::{StateTransitionHasUserFeeIncrease, StateTransitionOwned}; -use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::DocumentReplaceTransitionAction; -use drive::state_transition_action::batch::v1::{ - BatchTransitionActionV1, BatchedTransitionActionV1, -}; - -use crate::execution::validation::state_transition::batch::state::v0::fetch_documents::fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type; -use dpp::version::PlatformVersion; -use drive::grovedb::TransactionArg; - -use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; -use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; -use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; -use drive::drive::contract::DataContractFetchInfo; -use drive::drive::Drive; -use drive::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::DocumentTransferTransitionAction; -use drive::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::DocumentUpdatePriceTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_burn_transition_action::TokenBurnTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_config_update_transition_action::TokenConfigUpdateTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_destroy_frozen_funds_transition_action::TokenDestroyFrozenFundsTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_emergency_action_transition_action::TokenEmergencyActionTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_freeze_transition_action::TokenFreezeTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_transition_action::TokenMintTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::TokenClaimTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionAction; -use drive::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; -use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; -use crate::execution::types::execution_operation::ValidationOperation; -use crate::execution::types::state_transition_execution_context::{StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0}; -use crate::platform_types::platform_state::PlatformStateV0Methods; - -pub(in crate::execution::validation::state_transition::state_transitions::batch) trait BatchTransitionTransformerV1 -{ - fn try_into_action_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - full_validation: bool, - transaction: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - ) -> Result, Error>; -} - -trait BatchTransitionInternalTransformerV1 { - #[allow(clippy::too_many_arguments)] - fn transform_document_transitions_within_contract_v1( - platform: &PlatformStateRef, - block_info: &BlockInfo, - full_validation: bool, - data_contract_id: &Identifier, - owner_id: Identifier, - document_transitions: &BTreeMap<&String, Vec>>, - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result>, Error>; - #[allow(clippy::too_many_arguments)] - fn transform_document_transitions_within_document_type_v1( - platform: &PlatformStateRef, - block_info: &BlockInfo, - full_validation: bool, - data_contract_fetch_info: Arc, - document_type_name: &str, - owner_id: Identifier, - document_transitions: &[DocumentTransitionRefV1<'_>], - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result>, Error>; - #[allow(clippy::too_many_arguments)] - fn transform_token_transitions_within_contract_v1( - platform: &PlatformStateRef, - data_contract_id: &Identifier, - block_info: &BlockInfo, - validate_against_state: bool, - owner_id: Identifier, - token_transitions: &[&TokenTransition], - user_fee_increase: UserFeeIncrease, - transaction: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result>, Error>; - #[allow(clippy::too_many_arguments)] - /// Transfer token transition - fn transform_token_transition_v1( - drive: &Drive, - transaction: TransactionArg, - block_info: &BlockInfo, - validate_against_state: bool, - data_contract_fetch_info: Arc, - transition: &TokenTransition, - owner_id: Identifier, - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error>; - /// The data contract can be of multiple difference versions - #[allow(clippy::too_many_arguments)] - fn transform_document_transition_v1( - drive: &Drive, - transaction: TransactionArg, - full_validation: bool, - block_info: &BlockInfo, - data_contract_fetch_info: Arc, - transition: DocumentTransitionRefV1<'_>, - replaced_documents: &[Document], - user_fee_increase: UserFeeIncrease, - owner_id: Identifier, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error>; - fn find_replaced_document_v1<'a>( - document_transition: DocumentTransitionRefV1<'_>, - fetched_documents: &'a [Document], - ) -> ConsensusValidationResult<&'a Document>; - fn check_ownership_of_old_replaced_document_v1( - document_id: Identifier, - fetched_document: &Document, - owner_id: &Identifier, - ) -> SimpleConsensusValidationResult; - fn check_revision_is_bumped_by_one_during_replace_v1( - transition_revision: Revision, - document_id: Identifier, - original_document: &Document, - ) -> SimpleConsensusValidationResult; - fn failed_per_transition_action( - base_transition: &DocumentBaseTransition, - owner_id: Identifier, - errors: Vec, - ) -> Result, Error>; -} - -impl BatchTransitionTransformerV1 for BatchTransition { - fn try_into_action_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - validate_against_state: bool, - transaction: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - ) -> Result, Error> { - let owner_id = self.owner_id(); - let user_fee_increase = self.user_fee_increase(); - let platform_version = platform.state.current_platform_version()?; - let mut document_transitions_by_contracts_and_types: BTreeMap< - &Identifier, - BTreeMap<&String, Vec>, - > = BTreeMap::new(); - - let mut token_transitions_by_contracts: BTreeMap<&Identifier, Vec<&TokenTransition>> = - BTreeMap::new(); - - // We want to validate by contract, and then for each document type within a contract - for transition in self.transitions_iter_v1() { - match transition { - BatchedTransitionRefV1::Document(document_transition) => { - let document_type = document_transition.base().document_type_name(); - let data_contract_id = document_transition.base().data_contract_id_ref(); - - match document_transitions_by_contracts_and_types.entry(data_contract_id) { - Entry::Vacant(v) => { - v.insert(BTreeMap::from([(document_type, vec![document_transition])])); - } - Entry::Occupied(mut transitions_by_types_in_contract) => { - match transitions_by_types_in_contract - .get_mut() - .entry(document_type) - { - Entry::Vacant(v) => { - v.insert(vec![document_transition]); - } - Entry::Occupied(mut o) => o.get_mut().push(document_transition), - } - } - } - } - BatchedTransitionRefV1::Token(token_transition) => { - let data_contract_id = token_transition.base().data_contract_id_ref(); - - match token_transitions_by_contracts.entry(data_contract_id) { - Entry::Vacant(v) => { - v.insert(vec![token_transition]); - } - Entry::Occupied(mut transitions_by_tokens_in_contract) => { - transitions_by_tokens_in_contract - .get_mut() - .push(token_transition) - } - } - } - } - } - - let validation_result_documents = document_transitions_by_contracts_and_types - .iter() - .map( - |(data_contract_id, document_transitions_by_document_type)| { - Self::transform_document_transitions_within_contract_v1( - platform, - block_info, - validate_against_state, - data_contract_id, - owner_id, - document_transitions_by_document_type, - user_fee_increase, - execution_context, - transaction, - platform_version, - ) - }, - ) - .collect::>>, Error>>( - )?; - - let mut validation_result_tokens = token_transitions_by_contracts - .iter() - .map(|(data_contract_id, token_transitions)| { - Self::transform_token_transitions_within_contract_v1( - platform, - data_contract_id, - block_info, - validate_against_state, - owner_id, - token_transitions, - user_fee_increase, - transaction, - execution_context, - platform_version, - ) - }) - .collect::>>, Error>>( - )?; - - let mut validation_results = validation_result_documents; - - validation_results.append(&mut validation_result_tokens); - - let validation_result = - ConsensusValidationResult::flatten(validation_results, platform_version)?; - - if validation_result.has_data() { - let (transitions, errors) = validation_result.into_data_and_errors()?; - let batch_transition_action = BatchTransitionActionV1 { - owner_id, - transitions, - user_fee_increase, - }; - Ok(ConsensusValidationResult::new_with_data_and_errors( - batch_transition_action, - errors, - )) - } else { - Ok(ConsensusValidationResult::new_with_errors( - validation_result.errors, - )) - } - } -} - -impl BatchTransitionInternalTransformerV1 for BatchTransition { - /// Roll up the per-token-transition results via the versioned - /// [`ConsensusValidationResult::merge_many`] facade. - /// - /// The aggregator's v0→v1 change at PROTOCOL_VERSION_12 (see issue - /// #2867) also affects this token path. **Tokens-always-pay - /// invariant**: every per-token sub-transformer - /// (`try_from_borrowed_token_*_transition_with_contract_lookup`) - /// emits a `BumpIdentityDataContractNonce` action on its - /// base-validation failure path, so each per-token result carries - /// `data: Some(...)` even when validation fails. The v1 aggregator - /// therefore never collapses an all-failed token batch to - /// `data: None`, and token failures continue to land as - /// `PaidConsensusError` (tx stays in the block, user pays for the - /// validation work) under both v11 and v12. - /// - /// A future change to a token sub-transformer that drops the bump - /// emission on a failure path would silently route that failure to - /// `UnpaidConsensusError` (tx removed from the block by - /// `prepare_proposal`). See - /// `tests/token/burn/mod.rs::test_token_burn_trying_to_burn_more_than_we_have` - /// and its `_protocol_version_11` sibling for the regression pin. - fn transform_token_transitions_within_contract_v1( - platform: &PlatformStateRef, - data_contract_id: &Identifier, - block_info: &BlockInfo, - validate_against_state: bool, - owner_id: Identifier, - token_transitions: &[&TokenTransition], - user_fee_increase: UserFeeIncrease, - transaction: TransactionArg, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - let drive = platform.drive; - // Data Contract must exist - let Some(data_contract_fetch_info) = drive - .get_contract_with_fetch_info_and_fee( - data_contract_id.to_buffer(), - None, - false, - transaction, - platform_version, - )? - .1 - else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::DataContractNotPresentError(DataContractNotPresentError::new( - *data_contract_id, - )) - .into(), - )); - }; - - let validation_result = token_transitions - .iter() - .map(|token_transition| { - Self::transform_token_transition_v1( - platform.drive, - transaction, - block_info, - validate_against_state, - data_contract_fetch_info.clone(), - token_transition, - owner_id, - user_fee_increase, - execution_context, - platform_version, - ) - }) - .map(|result| result.map(|result| result.map(BatchedTransitionActionV1::Batched))) - .collect::>, Error>>( - )?; - let validation_result = - ConsensusValidationResult::merge_many(validation_result, platform_version)?; - Ok(validation_result) - } - fn transform_document_transitions_within_contract_v1( - platform: &PlatformStateRef, - block_info: &BlockInfo, - validate_against_state: bool, - data_contract_id: &Identifier, - owner_id: Identifier, - document_transitions: &BTreeMap<&String, Vec>>, - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - let drive = platform.drive; - // Data Contract must exist - let Some(data_contract_fetch_info) = drive - .get_contract_with_fetch_info_and_fee( - data_contract_id.0 .0, - None, - false, - transaction, - platform_version, - )? - .1 - else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::DataContractNotPresentError(DataContractNotPresentError::new( - *data_contract_id, - )) - .into(), - )); - }; - - let validation_result = document_transitions - .iter() - .map(|(document_type_name, document_transitions)| { - Self::transform_document_transitions_within_document_type_v1( - platform, - block_info, - validate_against_state, - data_contract_fetch_info.clone(), - document_type_name, - owner_id, - document_transitions, - user_fee_increase, - execution_context, - transaction, - platform_version, - ) - }) - .collect::>>, Error>>( - )?; - Ok(ConsensusValidationResult::flatten( - validation_result, - platform_version, - )?) - } - - fn transform_document_transitions_within_document_type_v1( - platform: &PlatformStateRef, - block_info: &BlockInfo, - validate_against_state: bool, - data_contract_fetch_info: Arc, - document_type_name: &str, - owner_id: Identifier, - document_transitions: &[DocumentTransitionRefV1<'_>], - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - // We use temporary execution context without dry run, - // because despite the dryRun, we need to get the - // data contract to proceed with following logic - // let tmp_execution_context = StateTransitionExecutionContext::default_for_platform_version(platform_version)?; - // - // execution_context.add_operations(tmp_execution_context.operations_slice()); - - let dry_run = false; //maybe reenable - - let data_contract = &data_contract_fetch_info.contract; - - let Some(document_type) = data_contract.document_type_optional_for_name(document_type_name) - else { - return Ok(ConsensusValidationResult::new_with_error( - InvalidDocumentTypeError::new(document_type_name.to_owned(), data_contract.id()) - .into(), - )); - }; - - let replace_and_transfer_transitions = document_transitions - .iter() - .filter(|transition| { - matches!( - transition, - DocumentTransitionRefV1::Replace(_) - | DocumentTransitionRefV1::Transfer(_) - | DocumentTransitionRefV1::Purchase(_) - | DocumentTransitionRefV1::UpdatePrice(_) - ) - }) - .copied() - .collect::>(); - - // We fetch documents only for replace and transfer transitions - // since we need them to create transition actions. Below we also - // perform state validation for replace and transfer transitions - // only; other transitions are validated in their validate_state - // functions. - // TODO: Think more about this architecture - // - // PROTOCOL_VERSION_11 consensus-safety: the fetch fn now takes - // `&mut execution_context` and bills the query cost internally - // — but only when `transform_into_action: 1`. On v0 it forces - // `epoch=None` (zero cost) and skips `add_operation`, matching - // pre-PR exactly. - let fetched_documents_validation_result = - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type( - platform.drive, - data_contract, - document_type, - replace_and_transfer_transitions.as_slice(), - &block_info.epoch, - execution_context, - transaction, - platform_version, - )?; - - if !fetched_documents_validation_result.is_valid() { - return Ok(ConsensusValidationResult::new_with_errors( - fetched_documents_validation_result.errors, - )); - } - - let replaced_documents = fetched_documents_validation_result.into_data()?; - - Ok(if !dry_run { - let document_transition_actions_validation_result = document_transitions - .iter() - .map(|transition| { - // we validate every transition in this document type - Self::transform_document_transition_v1( - platform.drive, - transaction, - validate_against_state, - block_info, - data_contract_fetch_info.clone(), - *transition, - &replaced_documents, - user_fee_increase, - owner_id, - execution_context, - platform_version, - ) - }) - .collect::>, Error>>( - )?; - - let result = ConsensusValidationResult::merge_many( - document_transition_actions_validation_result, - platform_version, - )?; - - if !result.is_valid() { - return Ok(result); - } - result - } else { - ConsensusValidationResult::default() - }) - } - - /// The data contract can be of multiple difference versions - fn transform_token_transition_v1( - drive: &Drive, - transaction: TransactionArg, - block_info: &BlockInfo, - validate_against_state: bool, - data_contract_fetch_info: Arc, - transition: &TokenTransition, - owner_id: Identifier, - user_fee_increase: UserFeeIncrease, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let approximate_for_costs = !validate_against_state; - match transition { - TokenTransition::Burn(token_burn_transition) => { - let (batched_action, fee_result) = TokenBurnTransitionAction::try_from_borrowed_token_burn_transition_with_contract_lookup(drive, owner_id, token_burn_transition, approximate_for_costs, transaction, block_info,user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::Mint(token_mint_transition) => { - let (batched_action, fee_result) = TokenMintTransitionAction::try_from_borrowed_token_mint_transition_with_contract_lookup(drive, owner_id, token_mint_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::Transfer(token_transfer_transition) => { - let (token_transfer_action, fee_result) = TokenTransferTransitionAction::try_from_borrowed_token_transfer_transition_with_contract_lookup(drive, owner_id, token_transfer_transition, approximate_for_costs, transaction, block_info, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - let batched_action = BatchedTransitionAction::TokenAction( - TokenTransitionAction::TransferAction(token_transfer_action), - ); - Ok(batched_action.into()) - } - TokenTransition::Freeze(token_freeze_transition) => { - let (batched_action, fee_result) = TokenFreezeTransitionAction::try_from_borrowed_token_freeze_transition_with_contract_lookup(drive, owner_id, token_freeze_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::Unfreeze(token_unfreeze_transition) => { - let (batched_action, fee_result) = TokenUnfreezeTransitionAction::try_from_borrowed_token_unfreeze_transition_with_contract_lookup(drive, owner_id, token_unfreeze_transition, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::DestroyFrozenFunds(destroy_frozen_funds) => { - let (batched_action, fee_result) = TokenDestroyFrozenFundsTransitionAction::try_from_borrowed_token_destroy_frozen_funds_transition_with_contract_lookup(drive, owner_id, destroy_frozen_funds, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::EmergencyAction(emergency_action) => { - let (batched_action, fee_result) = TokenEmergencyActionTransitionAction::try_from_borrowed_token_emergency_action_transition_with_contract_lookup(drive, owner_id, emergency_action, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::ConfigUpdate(token_config_update) => { - let (batched_action, fee_result) = TokenConfigUpdateTransitionAction::try_from_borrowed_token_config_update_transition_with_contract_lookup(drive, owner_id, token_config_update, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::Claim(claim) => { - let (batched_action, fee_result) = TokenClaimTransitionAction::try_from_borrowed_token_claim_transition_with_contract_lookup(drive, owner_id, claim, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::DirectPurchase(direct_purchase) => { - let (batched_action, fee_result) = TokenDirectPurchaseTransitionAction::try_from_borrowed_token_direct_purchase_transition_with_contract_lookup(drive, owner_id, direct_purchase, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - TokenTransition::SetPriceForDirectPurchase(set_price_for_direct_purchase) => { - let (batched_action, fee_result) = TokenSetPriceForDirectPurchaseTransitionAction::try_from_borrowed_token_set_price_for_direct_purchase_transition_with_contract_lookup(drive, owner_id, set_price_for_direct_purchase, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - } - } - - /// Per-transition handler for document arms. - /// - /// Each per-transition failure path (ownership mismatch, revision - /// mismatch, missing target document, etc.) emits a nonce-bump action so - /// the submitter pays for validation work that has already run. - /// - /// The `user_fee_increase` argument passed into each - /// `BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition` - /// call is `0` deliberately: the value gets overridden by the outer - /// Documents Batch's `user_fee_increase` when the per-transition - /// action rolls up into the `BatchTransitionAction`, so any - /// per-site value would be discarded. - fn transform_document_transition_v1<'a>( - drive: &Drive, - transaction: TransactionArg, - validate_against_state: bool, - block_info: &BlockInfo, - data_contract_fetch_info: Arc, - transition: DocumentTransitionRefV1<'_>, - replaced_documents: &[Document], - user_fee_increase: UserFeeIncrease, - owner_id: Identifier, - execution_context: &mut StateTransitionExecutionContext, - platform_version: &PlatformVersion, - ) -> Result, Error> { - if let Some(max_depth) = platform_version.system_limits.max_document_value_depth { - if let Some(actual_depth) = transition.first_data_depth_exceeding(max_depth as usize) { - return Self::failed_per_transition_action( - transition.base(), - owner_id, - vec![ConsensusError::BasicError(BasicError::ValueError( - ValueError::new_from_string(format!( - "document value depth {actual_depth} exceeds system maximum {max_depth}" - )), - ))], - ); - } - } - - match transition { - DocumentTransitionRefV1::Create(document_create_transition) => { - let (document_create_action, fee_result) = DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup( - drive, owner_id, transaction, - document_create_transition, block_info, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - }, platform_version)?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - Ok(document_create_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::Replace(document_replace_transition) => { - let validation_result = - Self::find_replaced_document_v1(transition, replaced_documents); - - if !validation_result.is_valid_with_data() { - // Keep this bump emission inline (not via Self::failed_per_transition_action): - // it's the one legacy v0 bump site, and routing it through the helper would - // drop the bump on PROTOCOL_VERSION_11 and diverge v11 chain replay (#2867). - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( - document_replace_transition.base(), - owner_id, - 0, - ); - return Ok(ConsensusValidationResult::new_with_data_and_errors( - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), - ), - validation_result.errors, - )); - } - - let original_document = validation_result.into_data()?; - - let validation_result = Self::check_ownership_of_old_replaced_document_v1( - document_replace_transition.base().id(), - original_document, - &owner_id, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_replace_transition.base(), - owner_id, - validation_result.errors, - ); - } - - if validate_against_state { - //there are situations where we don't want to validate this against the state - // for example when we already applied the state transition action - // and we are just validating it happened - let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( - document_replace_transition.revision(), - document_replace_transition.base().id(), - original_document, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_replace_transition.base(), - owner_id, - validation_result.errors, - ); - } - } - - let (document_replace_action, fee_result) = - DocumentReplaceTransitionAction::try_from_borrowed_document_replace_transition( - document_replace_transition, - owner_id, - original_document, - block_info, - user_fee_increase, - |_identifier| Ok(data_contract_fetch_info.clone()), - )?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(document_replace_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::Delete(document_delete_transition) => { - let (batched_action, fee_result) = DocumentDeleteTransitionAction::try_from_document_borrowed_delete_transition_with_contract_lookup(document_delete_transition, owner_id, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - })?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::IndexOnlyDelete(document_index_only_delete_transition) => { - let (batched_action, fee_result) = DocumentIndexOnlyDeleteTransitionAction::try_from_document_borrowed_index_only_delete_transition_with_contract_lookup(document_index_only_delete_transition, owner_id, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - })?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::Erase(document_erase_transition) => { - let (batched_action, fee_result) = DocumentEraseTransitionAction::try_from_document_borrowed_erase_transition_with_contract_lookup(document_erase_transition, owner_id, user_fee_increase, |_identifier| { - Ok(data_contract_fetch_info.clone()) - })?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(batched_action) - } - DocumentTransitionRefV1::Transfer(document_transfer_transition) => { - let validation_result = - Self::find_replaced_document_v1(transition, replaced_documents); - - if !validation_result.is_valid_with_data() { - return Self::failed_per_transition_action( - document_transfer_transition.base(), - owner_id, - validation_result.errors, - ); - } - - let original_document = validation_result.into_data()?; - - let validation_result = Self::check_ownership_of_old_replaced_document_v1( - document_transfer_transition.base().id(), - original_document, - &owner_id, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_transfer_transition.base(), - owner_id, - validation_result.errors, - ); - } - - if validate_against_state { - //there are situations where we don't want to validate this against the state - // for example when we already applied the state transition action - // and we are just validating it happened - let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( - document_transfer_transition.revision(), - document_transfer_transition.base().id(), - original_document, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_transfer_transition.base(), - owner_id, - validation_result.errors, - ); - } - } - - let (document_transfer_action, fee_result) = - DocumentTransferTransitionAction::try_from_borrowed_document_transfer_transition( - document_transfer_transition, - owner_id, - original_document.clone(), //todo: remove clone - block_info, - user_fee_increase, - |_identifier| Ok(data_contract_fetch_info.clone()), - )?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(document_transfer_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::UpdatePrice(document_update_price_transition) => { - let validation_result = - Self::find_replaced_document_v1(transition, replaced_documents); - - if !validation_result.is_valid_with_data() { - return Self::failed_per_transition_action( - document_update_price_transition.base(), - owner_id, - validation_result.errors, - ); - } - - let original_document = validation_result.into_data()?; - - let validation_result = Self::check_ownership_of_old_replaced_document_v1( - document_update_price_transition.base().id(), - original_document, - &owner_id, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_update_price_transition.base(), - owner_id, - validation_result.errors, - ); - } - - if validate_against_state { - //there are situations where we don't want to validate this against the state - // for example when we already applied the state transition action - // and we are just validating it happened - let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( - document_update_price_transition.revision(), - document_update_price_transition.base().id(), - original_document, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_update_price_transition.base(), - owner_id, - validation_result.errors, - ); - } - } - - let (document_update_price_action, fee_result) = - DocumentUpdatePriceTransitionAction::try_from_borrowed_document_update_price_transition( - document_update_price_transition, - owner_id, - original_document.clone(), //todo: find a way to not have to use cloning - block_info, - user_fee_increase, - |_identifier| Ok(data_contract_fetch_info.clone()), - )?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(document_update_price_action.map(BatchedTransitionActionV1::Batched)) - } - DocumentTransitionRefV1::Purchase(document_purchase_transition) => { - let validation_result = - Self::find_replaced_document_v1(transition, replaced_documents); - - if !validation_result.is_valid_with_data() { - return Self::failed_per_transition_action( - document_purchase_transition.base(), - owner_id, - validation_result.errors, - ); - } - - let original_document = validation_result.into_data()?; - - let Some(listed_price) = original_document - .properties() - .get_optional_integer::(PRICE)? - else { - return Self::failed_per_transition_action( - document_purchase_transition.base(), - owner_id, - vec![ - StateError::DocumentNotForSaleError(DocumentNotForSaleError::new( - original_document.id(), - )) - .into(), - ], - ); - }; - - if listed_price != document_purchase_transition.price() { - return Self::failed_per_transition_action( - document_purchase_transition.base(), - owner_id, - vec![StateError::DocumentIncorrectPurchasePriceError( - DocumentIncorrectPurchasePriceError::new( - original_document.id(), - document_purchase_transition.price(), - listed_price, - ), - ) - .into()], - ); - } - - if validate_against_state { - //there are situations where we don't want to validate this against the state - // for example when we already applied the state transition action - // and we are just validating it happened - let validation_result = Self::check_revision_is_bumped_by_one_during_replace_v1( - document_purchase_transition.revision(), - document_purchase_transition.base().id(), - original_document, - ); - - if !validation_result.is_valid() { - return Self::failed_per_transition_action( - document_purchase_transition.base(), - owner_id, - validation_result.errors, - ); - } - } - - let (document_purchase_action, fee_result) = - DocumentPurchaseTransitionAction::try_from_borrowed_document_purchase_transition( - document_purchase_transition, - owner_id, - original_document.clone(), //todo: find a way to not have to use cloning - owner_id, - block_info, - user_fee_increase, - |_identifier| Ok(data_contract_fetch_info.clone()), - )?; - - execution_context - .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - Ok(document_purchase_action.map(BatchedTransitionActionV1::Batched)) - } - } - } - - fn find_replaced_document_v1<'a>( - document_transition: DocumentTransitionRefV1<'_>, - fetched_documents: &'a [Document], - ) -> ConsensusValidationResult<&'a Document> { - let maybe_fetched_document = fetched_documents - .iter() - .find(|d| d.id() == document_transition.base().id()); - - if let Some(document) = maybe_fetched_document { - ConsensusValidationResult::new_with_data(document) - } else { - ConsensusValidationResult::new_with_error(ConsensusError::StateError( - StateError::DocumentNotFoundError(DocumentNotFoundError::new( - document_transition.base().id(), - )), - )) - } - } - - fn check_ownership_of_old_replaced_document_v1( - document_id: Identifier, - fetched_document: &Document, - owner_id: &Identifier, - ) -> SimpleConsensusValidationResult { - let mut result = SimpleConsensusValidationResult::default(); - if fetched_document.owner_id() != owner_id { - result.add_error(ConsensusError::StateError( - StateError::DocumentOwnerIdMismatchError(DocumentOwnerIdMismatchError::new( - document_id, - owner_id.to_owned(), - fetched_document.owner_id(), - )), - )); - } - result - } - fn check_revision_is_bumped_by_one_during_replace_v1( - transition_revision: Revision, - document_id: Identifier, - original_document: &Document, - ) -> SimpleConsensusValidationResult { - let mut result = SimpleConsensusValidationResult::default(); - - // If there was no previous revision this means that the document_type is not update-able - // However this should have been caught earlier - let Some(previous_revision) = original_document.revision() else { - result.add_error(ConsensusError::StateError( - StateError::InvalidDocumentRevisionError(InvalidDocumentRevisionError::new( - document_id, - None, - transition_revision, - )), - )); - return result; - }; - // no need to check bounds here, because it would be impossible to hit the end on a u64 - let expected_revision = previous_revision + 1; - if transition_revision != expected_revision { - result.add_error(ConsensusError::StateError( - StateError::InvalidDocumentRevisionError(InvalidDocumentRevisionError::new( - document_id, - Some(previous_revision), - transition_revision, - )), - )) - } - result - } - - fn failed_per_transition_action( - base_transition: &DocumentBaseTransition, - owner_id: Identifier, - errors: Vec, - ) -> Result, Error> { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition( - base_transition, - owner_id, - 0, - ); - Ok(ConsensusValidationResult::new_with_data_and_errors( - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), - ), - errors, - )) - } -} diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index 00bb6fec1db..fb51b793486 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -24,7 +24,6 @@ use drive::error::proof::ProofError; use drive::error::Error as DriveError; use drive::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use drive::state_transition_action::batch::v1::BatchedTransitionActionV1; use drive::state_transition_action::StateTransitionAction; use drive_abci::execution::validation::state_transition::transformer::StateTransitionActionTransformer; use drive_abci::platform_types::platform::PlatformRef; @@ -452,33 +451,13 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( ); } } - StateTransitionAction::BatchAction(_) | StateTransitionAction::BatchActionV1(_) => { - // Both batch action formats are checked the same way: a - // format 1 action wraps the format 0 items and may add an - // erase, which strategies never generate. - let (batch_owner_id, transition_action) = match action { - StateTransitionAction::BatchAction(batch_transition) => ( - batch_transition.owner_id(), - batch_transition - .transitions() - .first() - .cloned() - .map(BatchedTransitionActionV1::Batched), - ), - StateTransitionAction::BatchActionV1(batch_transition) => ( - batch_transition.owner_id(), - batch_transition.transitions().first().cloned(), - ), - _ => unreachable!("the outer match only admits batch actions here"), - }; - let Some(transition_action) = transition_action else { + StateTransitionAction::BatchAction(batch_transition) => { + if batch_transition.transitions().is_empty() { + panic!("we should have at least one transition"); + } + + let Some(transition_action) = batch_transition.transitions().first() else { panic!("we should have at least one transition"); - }; - let transition_action = match &transition_action { - BatchedTransitionActionV1::Batched(transition_action) => transition_action, - BatchedTransitionActionV1::DocumentErase(_) => { - panic!("strategies do not generate erases"); - } }; match transition_action { BatchedTransitionAction::DocumentAction(document_action) => { @@ -557,7 +536,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( // &document, // Document::try_from_create_transition( // creation_action, - // documents_batch_owner_id, + // documents_batch_transition.owner_id(), // platform_version, // ) // .expect("expected to get document") @@ -565,7 +544,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let mut expected_document = Document::try_from_create_transition_action( creation_action, - batch_owner_id, + batch_transition.owner_id(), platform_version, ) .expect("expected to get document"); @@ -597,7 +576,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let mut expected_document = Document::try_from_replace_transition_action( replace_action, - batch_owner_id, + batch_transition.owner_id(), platform_version, ) .expect("expected to get document"); @@ -621,7 +600,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( document, Document::try_from_replace_transition_action( replace_action, - batch_owner_id, + batch_transition.owner_id(), platform_version, ) .expect("expected to get document") @@ -634,6 +613,14 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( // we expect no document assert!(document.is_none()); } + DocumentTransitionAction::EraseAction(_) => { + // An erase acts on a document that was + // already deleted, so the by-id query this + // harness proves against returns nothing + // whether or not the erase executed; + // strategies do not generate erases today. + assert!(document.is_none()); + } DocumentTransitionAction::IndexOnlyDeleteAction(_) => { // indexOnly documents never have a // primary-storage row, so the by-id query @@ -727,7 +714,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( document_type_name, document_type_keeps_history: false, document_id: token_transition_action - .historical_document_id(batch_owner_id) + .historical_document_id(batch_transition.owner_id()) .to_buffer(), block_time_ms: None, //None because we want latest contested_status: @@ -757,7 +744,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( let expected_document = token_transition_action .build_historical_document( token_id, - batch_owner_id, + batch_transition.owner_id(), token_transition_action.base().identity_contract_nonce(), platform .state diff --git a/packages/rs-drive/src/prove/prove_state_transition/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/mod.rs index 18274c076b7..707bf78cbf0 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -35,10 +34,9 @@ impl Drive { ) -> Result>, Error> { match platform_version.drive.methods.prove.prove_state_transition { 0 => self.prove_state_transition_v0(state_transition, transaction, platform_version), - 1 => self.prove_state_transition_v1(state_transition, transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "prove_state_transition".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs deleted file mode 100644 index d508183cc66..00000000000 --- a/packages/rs-drive/src/prove/prove_state_transition/v1/mod.rs +++ /dev/null @@ -1,555 +0,0 @@ -use crate::drive::{Drive, RootTree}; -use crate::error::proof::ProofError; -use crate::error::Error; -use crate::prove::prove_state_transition::ProofCreationResult; -use crate::query::{ - IdentityBasedVoteDriveQuery, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus, -}; -use crate::verify::state_transition::state_transition_execution_path_queries::TryTransitionIntoPathQuery; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::identifier::Identifier; -use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; -use dpp::state_transition::address_funding_from_asset_lock_transition::accessors::AddressFundingFromAssetLockTransitionAccessorsV0; -use dpp::state_transition::address_funds_transfer_transition::accessors::AddressFundsTransferTransitionAccessorsV0; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransitionRefV1, DocumentTransitionRefV1, -}; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; -use dpp::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0; -use dpp::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; -use dpp::state_transition::identity_credit_transfer_to_addresses_transition::accessors::IdentityCreditTransferToAddressesTransitionAccessorsV0; -use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; -use dpp::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; -use dpp::state_transition::identity_topup_from_addresses_transition::accessors::IdentityTopUpFromAddressesTransitionAccessorsV0; -use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; -use dpp::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0; -use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; -use dpp::state_transition::StateTransitionIdentityIdFromInputs; -use dpp::state_transition::StateTransitionWitnessSigned; -use dpp::state_transition::{StateTransition, StateTransitionLike, StateTransitionOwned}; -use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; -use dpp::voting::votes::Vote; -use grovedb::{PathQuery, TransactionArg}; -use platform_version::version::PlatformVersion; - -fn contract_ids_to_non_historical_path_query(contract_ids: &[Identifier]) -> PathQuery { - let contract_ids: Vec<_> = contract_ids.iter().map(|id| id.to_buffer()).collect(); - - let mut path_query = Drive::fetch_non_historical_contracts_query(&contract_ids); - path_query.query.limit = None; - path_query -} - -fn contract_ids_to_historical_path_query(contract_ids: &[Identifier]) -> PathQuery { - let contract_ids: Vec<_> = contract_ids.iter().map(|id| id.to_buffer()).collect(); - - let mut path_query = Drive::fetch_historical_contracts_query(&contract_ids); - path_query.query.limit = None; - path_query -} - -impl Drive { - /// Generation 1 sees a batch of any wire format through the shell that - /// knows the erase kind. An erase acts on a document that is already - /// deleted, so it is proved by the same by-id query as a delete: the proof - /// shows the document absent. - pub(super) fn prove_state_transition_v1( - &self, - state_transition: &StateTransition, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - let path_query = match state_transition { - StateTransition::DataContractCreate(st) => { - if st.data_contract().config().keeps_history() { - contract_ids_to_historical_path_query(&st.modified_data_ids()) - } else { - contract_ids_to_non_historical_path_query(&st.modified_data_ids()) - } - } - StateTransition::DataContractUpdate(st) => { - if st.data_contract().config().keeps_history() { - contract_ids_to_historical_path_query(&st.modified_data_ids()) - } else { - contract_ids_to_non_historical_path_query(&st.modified_data_ids()) - } - } - StateTransition::Batch(st) => { - if st.transitions_len_v1() > 1 { - return Ok(ProofCreationResult::new_with_error( - ProofError::InvalidTransition( - "batch state transition must have only one batched transition" - .to_string(), - ), - )); - } - let Some(transition) = st.first_transition_v1() else { - return Ok(ProofCreationResult::new_with_error( - ProofError::InvalidTransition( - "batch state transition must have one batched transition".to_string(), - ), - )); - }; - - let owner_id = st.owner_id(); - - match transition { - BatchedTransitionRefV1::Document(document_transition) => { - let data_contract_id = document_transition.data_contract_id(); - - let Some(contract_fetch_info) = self.get_contract_with_fetch_info( - data_contract_id.to_buffer(), - false, - None, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::UnknownContract(format!( - "unknown contract with id {} in document proving", - data_contract_id - )))); - }; - - let contract = &contract_fetch_info.contract; - - let document_type = contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| { - Error::Proof(ProofError::UnknownContract(format!( - "cannot fetch contract for document {} with id {}: {}", - document_transition.document_type_name(), - document_transition.data_contract_id(), - e - ))) - })?; - - // indexOnly documents have no primary row to prove by - // id: the executed transition is proven against the - // entry its values produce under the proof index — - // the same single-entry path query the verifier - // rebuilds from the transition. - { - use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; - use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; - if document_type.index_only() { - let values = match document_transition { - DocumentTransitionRefV1::Create(create_transition) => { - create_transition.data().clone() - } - // The indexOnlyDelete kind always carries - // its values — no fallible access needed. - DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { - delete_transition.data().clone() - } - _ => { - return Ok(ProofCreationResult::new_with_error( - ProofError::InvalidTransition( - "indexOnly documents only support create and \ - indexOnlyDelete" - .to_string(), - ), - )); - } - }; - crate::query::index_only_synthesis::index_only_transition_entry_path_query( - contract.id(), - document_type, - &values, - owner_id, - platform_version, - )? - } else { - let contested_status = - if let DocumentTransitionRefV1::Create(create_transition) = - document_transition - { - if create_transition.prefunded_voting_balance().is_some() { - SingleDocumentDriveQueryContestedStatus::Contested - } else { - SingleDocumentDriveQueryContestedStatus::NotContested - } - } else { - SingleDocumentDriveQueryContestedStatus::NotContested - }; - - let query = SingleDocumentDriveQuery { - contract_id: document_transition - .data_contract_id() - .into_buffer(), - document_type_name: document_transition - .document_type_name() - .clone(), - document_type_keeps_history: document_type - .documents_keep_history(), - document_id: document_transition.base().id().into_buffer(), - block_time_ms: None, //None because we want latest - contested_status, - }; - - let mut path_query = - query.construct_path_query(platform_version)?; - path_query.query.limit = None; - path_query - } - } - } - BatchedTransitionRefV1::Token(token_transition) => { - let data_contract_id = token_transition.data_contract_id(); - - let Some(contract_fetch_info) = self.get_contract_with_fetch_info( - data_contract_id.to_buffer(), - false, - None, - platform_version, - )? - else { - return Ok(ProofCreationResult::new_with_error( - ProofError::UnknownContract(format!( - "unknown contract with id {} in token proving", - data_contract_id - )), - )); - }; - - let contract = &contract_fetch_info.contract; - token_transition.try_transition_into_path_query_with_contract( - contract, - owner_id, - platform_version, - )? - } - } - } - StateTransition::IdentityCreate(st) => Drive::full_identity_query( - &st.identity_id().into_buffer(), - &platform_version.drive.grove_version, - )?, - StateTransition::IdentityTopUp(st) => { - // we expect to get a new balance and revision - Drive::revision_and_balance_path_query( - st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )? - } - StateTransition::IdentityCreditWithdrawal(st) => { - Drive::identity_balance_query(&st.identity_id().to_buffer()) - } - StateTransition::IdentityUpdate(st) => Drive::identity_all_keys_query( - &st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )?, - StateTransition::IdentityCreditTransfer(st) => { - let sender_query = Drive::identity_balance_query(&st.identity_id().into_buffer()); - let recipient_query = - Drive::identity_balance_query(&st.recipient_id().into_buffer()); - - PathQuery::merge( - vec![&sender_query, &recipient_query], - &platform_version.drive.grove_version, - )? - } - StateTransition::MasternodeVote(st) => { - let pro_tx_hash = st.pro_tx_hash(); - - match st.vote() { - Vote::ResourceVote(resource_vote) => { - let query = IdentityBasedVoteDriveQuery { - identity_id: pro_tx_hash, - vote_poll: resource_vote.vote_poll().clone(), - }; - - // The path query construction can only fail if the serialization fails. - // Because the serialization will pretty much never fail, we can do this. - let mut path_query = query.construct_path_query()?; - path_query.query.limit = None; - path_query - } - } - } - StateTransition::IdentityCreditTransferToAddresses(st) => { - let identity_query = Drive::revision_and_balance_path_query( - st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )?; - let mut addresses_query = - Drive::balances_for_clear_addresses_query(st.recipient_addresses().keys()); - - // TODO: fix this limit setting - "can not merge pathqueries with limits, consider setting the limit after the merge" - addresses_query.query.limit = None; - - PathQuery::merge( - vec![&identity_query, &addresses_query], - &platform_version.drive.grove_version, - )? - } - StateTransition::IdentityCreateFromAddresses(st) => { - let identity_id = st.identity_id_from_inputs().map_err(|e| { - Error::Proof(ProofError::CorruptedProof(format!( - "Failed to calculate identity_id from inputs: {}", - e - ))) - })?; - let identity_query = Drive::full_identity_query( - &identity_id.into_buffer(), - &platform_version.drive.grove_version, - )?; - let change_output = st.output().into_iter().map(|(address, _)| address); - let addresses_to_check = st.inputs().keys().chain(change_output); - - let mut addresses_query = - Drive::balances_for_clear_addresses_query(addresses_to_check); - - // TODO: fix this limit setting - "can not merge pathqueries with limits, consider setting the limit after the merge" - addresses_query.query.limit = None; - - PathQuery::merge( - vec![&identity_query, &addresses_query], - &platform_version.drive.grove_version, - )? - } - StateTransition::IdentityTopUpFromAddresses(st) => { - // we expect to get a new balance and revision - let identity_query = Drive::revision_and_balance_path_query( - st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )?; - let change_output = st.output().into_iter().map(|(address, _)| address); - let addresses_to_check = st.inputs().keys().chain(change_output); - let addresses_query = Drive::balances_for_clear_addresses_query(addresses_to_check); - - // TODO: not sure if just setting this to unlimited is correct - let mut addresses_query = addresses_query; - addresses_query.query.limit = None; - - PathQuery::merge( - vec![&identity_query, &addresses_query], - &platform_version.drive.grove_version, - )? - } - StateTransition::AddressFundsTransfer(st) => Drive::balances_for_clear_addresses_query( - st.inputs().keys().chain(st.outputs().keys()), - ), - StateTransition::AddressFundingFromAssetLock(st) => { - Drive::balances_for_clear_addresses_query( - st.inputs().keys().chain(st.outputs().keys()), - ) - } - StateTransition::AddressCreditWithdrawal(st) => { - let addresses_to_check = st - .inputs() - .keys() - .chain(st.output().into_iter().map(|(address, _)| address)); - Drive::balances_for_clear_addresses_query(addresses_to_check) - } - StateTransition::ShieldedTransfer(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::state_transition::shielded_transfer_transition::accessors::ShieldedTransferTransitionAccessorsV0; - - let nullifier_keys: Vec> = st.nullifiers(); - let count = nullifier_keys.len() as u16; - - let mut query = grovedb::Query::new(); - query.insert_keys(nullifier_keys); - - PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(query, Some(count), None), - ) - } - StateTransition::Shield(st) => { - Drive::balances_for_clear_addresses_query(st.inputs().keys()) - } - StateTransition::Unshield(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::state_transition::unshield_transition::accessors::UnshieldTransitionAccessorsV0; - - let nullifier_keys: Vec> = st.nullifiers(); - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys); - let nullifier_pq = PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let mut address_pq = - Drive::balances_for_clear_addresses_query(std::iter::once(st.output_address())); - address_pq.query.limit = None; - - PathQuery::merge( - vec![&nullifier_pq, &address_pq], - &platform_version.drive.grove_version, - )? - } - StateTransition::ShieldedWithdrawal(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::data_contracts::withdrawals_contract; - use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; - use dpp::document::Document; - use dpp::state_transition::shielded_withdrawal_transition::accessors::ShieldedWithdrawalTransitionAccessorsV0; - - let nullifier_keys: Vec> = st.nullifiers(); - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys.clone()); - let nullifier_pq = PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - // Compute withdrawal document ID deterministically - let first_nullifier = nullifier_keys.first().ok_or_else(|| { - Error::Proof(ProofError::InvalidTransition( - "shielded withdrawal has no nullifiers".to_string(), - )) - })?; - let mut entropy = Vec::new(); - entropy.extend_from_slice(first_nullifier); - entropy.extend_from_slice(st.output_script().as_bytes()); - let document_id = Document::generate_document_id_v0( - &withdrawals_contract::ID, - &withdrawals_contract::OWNER_ID, - withdrawal::NAME, - &entropy, - ); - - let doc_query = SingleDocumentDriveQuery { - contract_id: withdrawals_contract::ID.to_buffer(), - document_type_name: withdrawal::NAME.to_string(), - document_type_keeps_history: false, - document_id: document_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - }; - let mut doc_pq = doc_query.construct_path_query(platform_version)?; - doc_pq.query.limit = None; - - PathQuery::merge( - vec![&nullifier_pq, &doc_pq], - &platform_version.drive.grove_version, - )? - } - StateTransition::ShieldFromAssetLock(st) => { - use dpp::identity::state_transition::AssetLockProved; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; - - let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { - Error::Proof(ProofError::InvalidTransition( - "shield from asset lock has no outpoint".to_string(), - )) - })?; - let outpoint_bytes: [u8; 36] = outpoint.into(); - - let mut query = grovedb::Query::new(); - query.insert_key(outpoint_bytes.to_vec()); - - let outpoint_pq = PathQuery::new( - vec![vec![RootTree::SpentAssetLockTransactions as u8]], - grovedb::SizedQuery::new(query, Some(1), None), - ); - - // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. - let ShieldFromAssetLockTransition::V0(v0) = st; - match &v0.surplus_output { - Some(surplus_address) => { - // Mirror the Unshield arm: also prove the balance of the signed - // surplus-output address so a light client can confirm the surplus - // credit landed there. `PathQuery::merge` rejects sub-queries that carry - // limits, so clear both before merging. The verifier rebuilds this exact - // merged query (same sub-queries, same cleared limits, same merge) and - // verifies it STRICTLY, so the proof cannot carry any extra data beyond - // {outpoint, surplus-address}. - let mut outpoint_pq = outpoint_pq; - outpoint_pq.query.limit = None; - - let mut address_pq = Drive::balances_for_clear_addresses_query( - std::iter::once(surplus_address), - ); - address_pq.query.limit = None; - - PathQuery::merge( - vec![&outpoint_pq, &address_pq], - &platform_version.drive.grove_version, - )? - } - None => outpoint_pq, - } - } - StateTransition::IdentityCreateFromShieldedPool(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::state_transition::identity_create_from_shielded_pool_transition::accessors::IdentityCreateFromShieldedPoolTransitionAccessorsV0; - - // Prove BOTH the spent nullifiers AND the newly-created identity in a single merged - // multi-root proof. Built STRICT from day one (per #3812): the verifier rebuilds this - // exact merged query and verifies it with `verify_query` (succinctness on), so the - // proof cannot carry any branch beyond {nullifiers, identity}. The absence-proof - // variant is unusable here: it enumerates the query's terminal keys, which is - // impossible for `full_identity_query`'s unbounded all-keys range. - let nullifier_keys: Vec> = st.nullifiers(); - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys); - // `PathQuery::merge` rejects sub-queries that carry a limit, so leave it None. - let nullifier_pq = PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let mut identity_pq = Drive::full_identity_query( - &st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )?; - identity_pq.query.limit = None; - - PathQuery::merge( - vec![&nullifier_pq, &identity_pq], - &platform_version.drive.grove_version, - )? - } - StateTransition::IdentityTopUpFromShieldedPool(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::state_transition::identity_top_up_from_shielded_pool_transition::accessors::IdentityTopUpFromShieldedPoolTransitionAccessorsV0; - - // Spent nullifiers AND the credited identity in one STRICT merged proof, - // exactly the IdentityCreateFromShieldedPool shape. - let nullifier_keys: Vec> = st.nullifiers(); - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys); - let nullifier_pq = PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let mut identity_pq = Drive::full_identity_query( - &st.identity_id().to_buffer(), - &platform_version.drive.grove_version, - )?; - identity_pq.query.limit = None; - - PathQuery::merge( - vec![&nullifier_pq, &identity_pq], - &platform_version.drive.grove_version, - )? - } - StateTransition::ShieldFromIdentity(st) => { - // The identity's post-debit balance; the shielded note is not proven - // (the client learns it through shielded sync, as after `Shield`). - use dpp::state_transition::shield_from_identity_transition::accessors::ShieldFromIdentityTransitionAccessorsV0; - Drive::identity_balance_query(&st.identity_id().to_buffer()) - } - }; - - let proof = self.grove_get_proved_path_query( - &path_query, - transaction, - &mut vec![], - &platform_version.drive, - )?; - - Ok(ProofCreationResult::new_with_data(proof)) - } -} diff --git a/packages/rs-drive/src/query/filter.rs b/packages/rs-drive/src/query/filter.rs index 5913805cf02..8fc9bd6fa8c 100644 --- a/packages/rs-drive/src/query/filter.rs +++ b/packages/rs-drive/src/query/filter.rs @@ -324,6 +324,10 @@ impl DriveDocumentQueryFilter<'_> { TransitionCheckResult::Fail } } + // An erase carries no document values and acts on a document that + // is already invisible to every document query, so no + // content-based subscription filter can describe it. + DocumentTransition::Erase(_) => TransitionCheckResult::Fail, DocumentTransition::IndexOnlyDelete(index_only_delete) => { if let DocumentActionMatchClauses::Delete { original_document_clauses, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs deleted file mode 100644 index fc8cda86fcc..00000000000 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/batch_transition_v1.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::error::drive::DriveError; -use crate::error::Error; -use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; -use crate::state_transition_action::batch::v1::{ - BatchTransitionActionV1, BatchedTransitionActionV1, -}; -use crate::util::batch::DriveOperation; -use dpp::block::epoch::Epoch; -use dpp::prelude::Identifier; -use dpp::version::PlatformVersion; - -impl DriveHighLevelBatchOperationConverter for BatchedTransitionActionV1 { - fn into_high_level_batch_drive_operations<'b>( - self, - epoch: &Epoch, - owner_id: Identifier, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - match self { - BatchedTransitionActionV1::Batched(batched_action) => batched_action - .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), - BatchedTransitionActionV1::DocumentErase(erase_action) => erase_action - .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), - } - } -} - -/// Generation 1 of the batch conversion: consumes batch action format 1, whose -/// items may include the erase of a keep-history document, and emits the -/// operations of every item in the order the batch listed them. -impl DriveHighLevelOperationConverter for BatchTransitionActionV1 { - fn into_high_level_drive_operations<'b>( - self, - epoch: &Epoch, - platform_version: &PlatformVersion, - ) -> Result>, Error> { - match platform_version - .drive - .methods - .state_transitions - .convert_to_high_level_operations - .documents_batch_transition - { - 1 => { - let owner_id = self.owner_id(); - let transitions = self.transitions_owned(); - Ok(transitions - .into_iter() - .map(|transition| { - transition.into_high_level_batch_drive_operations( - epoch, - owner_id, - platform_version, - ) - }) - .collect::>, Error>>()? - .into_iter() - .flatten() - .collect()) - } - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { - method: "BatchTransitionActionV1::into_high_level_drive_operations".to_string(), - known_versions: vec![1], - received: version, - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; - use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{ - BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0, - }; - use crate::util::batch::IdentityOperationType; - - fn bump(nonce: u64) -> BatchedTransitionActionV1 { - BatchedTransitionActionV1::Batched(BatchedTransitionAction::BumpIdentityDataContractNonce( - BumpIdentityDataContractNonceAction::V0(BumpIdentityDataContractNonceActionV0 { - identity_id: Identifier::from([0xAA; 32]), - data_contract_id: Identifier::from([0xBB; 32]), - identity_contract_nonce: nonce, - user_fee_increase: 0, - }), - )) - } - - #[test] - fn should_emit_the_operations_of_every_item_in_batch_order() { - let action = BatchTransitionActionV1 { - owner_id: Identifier::from([0xCC; 32]), - transitions: vec![bump(7), bump(8)], - user_fee_increase: 0, - }; - let epoch = Epoch::new(0).expect("epoch"); - - let operations = action - .into_high_level_drive_operations(&epoch, PlatformVersion::latest()) - .expect("operations"); - - let nonces: Vec = operations - .iter() - .map(|operation| match operation { - DriveOperation::IdentityOperation( - IdentityOperationType::UpdateIdentityContractNonce { nonce, .. }, - ) => *nonce, - other => panic!("expected a nonce update, got {other:?}"), - }) - .collect(); - assert_eq!(nonces, vec![7, 8]); - } - - #[test] - fn should_refuse_a_protocol_version_whose_converter_only_knows_format_0() { - let action = BatchTransitionActionV1 { - owner_id: Identifier::from([0xCC; 32]), - transitions: vec![bump(7)], - user_fee_increase: 0, - }; - let epoch = Epoch::new(0).expect("epoch"); - - let result = action.into_high_level_drive_operations( - &epoch, - PlatformVersion::get(14).expect("protocol 14"), - ); - - assert!(matches!( - result, - Err(Error::Drive(DriveError::UnknownVersionMismatch { - received: 0, - .. - })) - )); - } -} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs index 6a6deca3152..2178cdb16a3 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transition.rs @@ -56,6 +56,13 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransitionAction { platform_version, ) } + DocumentTransitionAction::EraseAction(document_erase_transition) => { + document_erase_transition.into_high_level_batch_drive_operations( + epoch, + owner_id, + platform_version, + ) + } DocumentTransitionAction::IndexOnlyDeleteAction( document_index_only_delete_transition, ) => document_index_only_delete_transition.into_high_level_batch_drive_operations( diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs index 07729a1862e..99c1c40408a 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/mod.rs @@ -5,7 +5,6 @@ use dpp::platform_value::Identifier; use dpp::version::PlatformVersion; mod batch_transition; -mod batch_transition_v1; mod document; mod token; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs index db8279bdfb8..a3a676e7851 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs @@ -44,9 +44,6 @@ impl DriveHighLevelOperationConverter for StateTransitionAction { StateTransitionAction::BatchAction(documents_batch_transition) => { documents_batch_transition.into_high_level_drive_operations(epoch, platform_version) } - StateTransitionAction::BatchActionV1(documents_batch_transition) => { - documents_batch_transition.into_high_level_drive_operations(epoch, platform_version) - } StateTransitionAction::IdentityCreateAction(identity_create_transition) => { identity_create_transition.into_high_level_drive_operations(epoch, platform_version) } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs index b98a4618e1d..d603791944a 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/transformer.rs @@ -1,7 +1,7 @@ use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::{DocumentEraseTransitionAction, DocumentEraseTransitionActionV0}; -use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; +use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; use dpp::fee::fee_result::FeeResult; use dpp::platform_value::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; @@ -18,7 +18,7 @@ impl DocumentEraseTransitionAction { get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, ) -> Result< ( - ConsensusValidationResult, + ConsensusValidationResult, FeeResult, ), Error, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs index 5a1dada3c15..5f1262561ae 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_erase_transition_action/v0/transformer.rs @@ -2,8 +2,8 @@ use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; +use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::consensus::basic::document::InvalidDocumentTransitionActionError; use dpp::consensus::basic::BasicError; @@ -26,7 +26,7 @@ impl DocumentEraseTransitionActionV0 { get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, ) -> Result< ( - ConsensusValidationResult, + ConsensusValidationResult, FeeResult, ), Error, @@ -48,9 +48,7 @@ impl DocumentEraseTransitionActionV0 { ); return Ok(( ConsensusValidationResult::new_with_data_and_errors( - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), - ), + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), vec![ConsensusError::BasicError( BasicError::InvalidDocumentTransitionActionError( InvalidDocumentTransitionActionError::new(format!( @@ -83,9 +81,8 @@ impl DocumentEraseTransitionActionV0 { owner_id, user_fee_increase, ); - let batched_action = BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), - ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); return Ok(( ConsensusValidationResult::new_with_data_and_errors( @@ -98,9 +95,9 @@ impl DocumentEraseTransitionActionV0 { }; Ok(( - BatchedTransitionActionV1::DocumentErase( + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::EraseAction( DocumentEraseTransitionActionV0 { base }.into(), - ) + )) .into(), FeeResult::default(), )) diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs index 4a36b30372a..3a88a2ab849 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_transition_action_type.rs @@ -8,6 +8,7 @@ impl DocumentTransitionActionTypeGetter for DocumentTransitionAction { match self { DocumentTransitionAction::CreateAction(_) => DocumentTransitionActionType::Create, DocumentTransitionAction::DeleteAction(_) => DocumentTransitionActionType::Delete, + DocumentTransitionAction::EraseAction(_) => DocumentTransitionActionType::Erase, DocumentTransitionAction::ReplaceAction(_) => DocumentTransitionActionType::Replace, DocumentTransitionAction::TransferAction(_) => DocumentTransitionActionType::Transfer, DocumentTransitionAction::PurchaseAction(_) => DocumentTransitionActionType::Purchase, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs index 7f984f2de1b..65cbdff1911 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/mod.rs @@ -28,6 +28,8 @@ use crate::state_transition_action::batch::batched_transition::document_transiti use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::v0::DocumentIndexOnlyDeleteTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::{DocumentTransferTransitionAction, DocumentTransferTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::{DocumentUpdatePriceTransitionAction, DocumentUpdatePriceTransitionActionAccessorsV0}; @@ -54,6 +56,9 @@ pub enum DocumentTransitionAction { /// indexOnly delete-by-values — carries the document's property /// values, since there is no primary-storage row to fetch them from IndexOnlyDeleteAction(DocumentIndexOnlyDeleteTransitionAction), + /// Removes the retained revisions of an already deleted keep-history + /// document, a chunk at a time. + EraseAction(DocumentEraseTransitionAction), } impl DocumentTransitionAction { @@ -67,6 +72,7 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base(), DocumentTransitionAction::UpdatePriceAction(d) => d.base(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base(), + DocumentTransitionAction::EraseAction(d) => d.base(), } } @@ -80,6 +86,7 @@ impl DocumentTransitionAction { DocumentTransitionAction::PurchaseAction(d) => d.base_owned(), DocumentTransitionAction::UpdatePriceAction(d) => d.base_owned(), DocumentTransitionAction::IndexOnlyDeleteAction(d) => d.base_owned(), + DocumentTransitionAction::EraseAction(d) => d.base_owned(), } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/mod.rs b/packages/rs-drive/src/state_transition_action/batch/mod.rs index 5bc3ad969bc..2bbc3ca7a38 100644 --- a/packages/rs-drive/src/state_transition_action/batch/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/mod.rs @@ -14,8 +14,6 @@ use crate::state_transition_action::batch::batched_transition::document_transiti pub mod batched_transition; /// v0 pub mod v0; -/// v1 -pub mod v1; #[cfg(test)] mod tests; diff --git a/packages/rs-drive/src/state_transition_action/batch/tests.rs b/packages/rs-drive/src/state_transition_action/batch/tests.rs index 8ffacdeb10b..263b626d827 100644 --- a/packages/rs-drive/src/state_transition_action/batch/tests.rs +++ b/packages/rs-drive/src/state_transition_action/batch/tests.rs @@ -3039,3 +3039,90 @@ fn should_stamp_fetched_contract_version_on_replace_conversion() { .expect("owned replace conversion"); assert_eq!(owned.contract_version(), Some(STAMP_TEST_CONTRACT_VERSION)); } + +// ============================================================ +// Erase action inside the batch action +// ============================================================ + +mod erase_action { + use super::*; + use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; + use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; + use crate::state_transition_action::batch::v0::BatchTransitionActionV0; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + use dpp::identity::SecurityLevel; + + fn erase() -> BatchedTransitionAction { + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::EraseAction( + DocumentEraseTransitionAction::V0(DocumentEraseTransitionActionV0 { + base: test_document_base(), + }), + )) + } + + fn delete() -> BatchedTransitionAction { + BatchedTransitionAction::DocumentAction(DocumentTransitionAction::DeleteAction( + DocumentDeleteTransitionAction::V0(DocumentDeleteTransitionActionV0 { + base: test_document_base(), + }), + )) + } + + fn batch(transitions: Vec) -> BatchTransitionAction { + BatchTransitionAction::V0(BatchTransitionActionV0 { + owner_id: Identifier::from([0x11; 32]), + transitions, + user_fee_increase: 10, + }) + } + + /// An erase is signed like any other action on its document type: the + /// key it demands is the one the type demands, exactly as for a delete. + #[test] + fn should_require_the_erased_types_security_level() { + let domain_level = test_dpns_contract_info() + .contract + .document_type_for_name("domain") + .expect("the DPNS contract has a domain type") + .security_level_requirement(); + assert_ne!( + domain_level, + SecurityLevel::MASTER, + "the test needs a type whose requirement spans more than one level" + ); + + let expected: Vec = (SecurityLevel::CRITICAL as u8..=domain_level as u8) + .map(|level| SecurityLevel::try_from(level).unwrap()) + .collect(); + assert_eq!( + batch(vec![erase()]) + .combined_security_level_requirement() + .unwrap(), + expected + ); + assert_eq!( + batch(vec![erase()]) + .combined_security_level_requirement() + .unwrap(), + batch(vec![delete()]) + .combined_security_level_requirement() + .unwrap(), + "an erase and a delete of the same type demand the same key" + ); + } + + /// The credits a batch commits up front decide whether the signer can + /// afford it; an erase commits none, so its presence changes no sum. + #[test] + fn should_commit_no_credits_up_front() { + let erase_only = batch(vec![erase()]); + assert_eq!(erase_only.all_purchases_amount().unwrap(), None); + assert_eq!( + erase_only + .all_conflicting_index_collateral_voting_funds() + .unwrap(), + None + ); + assert_eq!(erase_only.all_used_balances().unwrap(), None); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs b/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs deleted file mode 100644 index 2fc94f740cb..00000000000 --- a/packages/rs-drive/src/state_transition_action/batch/v1/mod.rs +++ /dev/null @@ -1,275 +0,0 @@ -use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; -use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use crate::state_transition_action::batch::BatchTransitionAction; -use crate::state_transition_action::batch::v0::BatchTransitionActionV0; -use derive_more::From; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::fee::Credits; -use dpp::identifier::Identifier; -use dpp::identity::SecurityLevel; -use dpp::prelude::UserFeeIncrease; -use dpp::ProtocolError; - -#[cfg(test)] -mod tests; - -/// One item of a batch action in format 1. -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, From)] -pub enum BatchedTransitionActionV1 { - /// An action that batch action format 0 can express: a document, token or - /// identity contract nonce bump action. - Batched(BatchedTransitionAction), - /// The erase of an already deleted keep-history document, which only - /// batch action format 1 can express. - DocumentErase(DocumentEraseTransitionAction), -} - -impl BatchedTransitionActionV1 { - /// The data contract the item acts on. - pub fn data_contract_id(&self) -> Identifier { - match self { - BatchedTransitionActionV1::Batched(action) => action.data_contract_id(), - BatchedTransitionActionV1::DocumentErase(action) => action.base().data_contract_id(), - } - } - - /// The item as a format 0 action, when it is one. - pub fn as_batched_action(&self) -> Option<&BatchedTransitionAction> { - match self { - BatchedTransitionActionV1::Batched(action) => Some(action), - BatchedTransitionActionV1::DocumentErase(_) => None, - } - } - - /// The item as an erase action, when it is one. - pub fn as_document_erase_action(&self) -> Option<&DocumentEraseTransitionAction> { - match self { - BatchedTransitionActionV1::Batched(_) => None, - BatchedTransitionActionV1::DocumentErase(action) => Some(action), - } - } -} - -/// Batch action format 1: what a batch transition becomes once it has been -/// transformed into an action for a protocol version whose batch wire format -/// can carry an erase. -/// -/// It is a sibling of [`BatchTransitionAction`] rather than a variant of it. The -/// typed accessors of [`BatchTransitionAction`] hand out format 0 items, and -/// the validation generations selected by earlier protocol versions consume -/// them; a format that carries an erase cannot satisfy those signatures, so it -/// reaches those consumers through its own state transition action arm instead -/// and is only ever produced by the generations that know it. -#[derive(Default, Debug, Clone)] -pub struct BatchTransitionActionV1 { - /// The owner making the transitions - pub owner_id: Identifier, - /// The inner transitions, in the order the batch listed them - pub transitions: Vec, - /// fee multiplier - pub user_fee_increase: UserFeeIncrease, -} - -impl From for BatchTransitionActionV1 { - fn from(value: BatchTransitionActionV0) -> Self { - let BatchTransitionActionV0 { - owner_id, - transitions, - user_fee_increase, - } = value; - Self { - owner_id, - transitions: transitions - .into_iter() - .map(BatchedTransitionActionV1::Batched) - .collect(), - user_fee_increase, - } - } -} - -impl From for BatchTransitionActionV1 { - fn from(value: BatchTransitionAction) -> Self { - match value { - BatchTransitionAction::V0(v0) => v0.into(), - } - } -} - -impl BatchTransitionActionV1 { - /// owner id - pub fn owner_id(&self) -> Identifier { - self.owner_id - } - - /// transitions - pub fn transitions(&self) -> &Vec { - &self.transitions - } - - /// transitions - pub fn transitions_mut(&mut self) -> &mut Vec { - &mut self.transitions - } - - /// transitions - pub fn transitions_take(&mut self) -> Vec { - std::mem::take(&mut self.transitions) - } - - /// transitions owned - pub fn transitions_owned(self) -> Vec { - self.transitions - } - - /// set transitions - pub fn set_transitions(&mut self, transitions: Vec) { - self.transitions = transitions - } - - /// fee multiplier - pub fn user_fee_increase(&self) -> UserFeeIncrease { - self.user_fee_increase - } - - /// The sum of all purchases amount and all conflicting index collateral voting funds - pub fn all_used_balances(&self) -> Result, ProtocolError> { - Ok(match (self.all_purchases_amount()?, self.all_conflicting_index_collateral_voting_funds()?) { - (Some(all_purchases_amount), Some(all_conflicting_index_collateral_voting_funds)) => Some(all_purchases_amount.checked_add(all_conflicting_index_collateral_voting_funds).ok_or(ProtocolError::Overflow("overflow between all_purchases_amount and all_conflicting_index_collateral_voting_funds"))?), - (Some(all_purchases_amount), None) => Some(all_purchases_amount), - (None, Some(all_conflicting_index_collateral_voting_funds)) => Some(all_conflicting_index_collateral_voting_funds), - (None, None) => None, - }) - } - - /// The sum of all purchases amounts for all purchase transitions in the batch. - /// An erase purchases nothing. - pub fn all_purchases_amount(&self) -> Result, ProtocolError> { - let (total, any_purchases): (Option, bool) = self - .transitions - .iter() - .filter_map(|transition| match transition { - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( - DocumentTransitionAction::PurchaseAction(document_purchase), - )) => Some(document_purchase.price()), - BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction( - TokenTransitionAction::DirectPurchaseAction(token_purchase), - )) => Some(token_purchase.total_agreed_price()), - _ => None, - }) - .fold((None, false), |(acc, _), price| match acc { - Some(acc_val) => acc_val - .checked_add(price) - .map_or((None, true), |sum| (Some(sum), true)), - None => (Some(price), true), - }); - - match (total, any_purchases) { - (Some(total), _) => Ok(Some(total)), - (None, true) => Err(ProtocolError::Overflow("overflow in all purchases amount")), - _ => Ok(None), - } - } - - /// The sum of all conflicting index collateral voting funds for all document create transitions in the batch. - /// An erase prefunds nothing. - pub fn all_conflicting_index_collateral_voting_funds( - &self, - ) -> Result, ProtocolError> { - let (total, any_voting_funds): (Option, bool) = self - .transitions - .iter() - .filter_map(|transition| match transition { - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( - DocumentTransitionAction::CreateAction(document_create_transition_action), - )) => document_create_transition_action - .prefunded_voting_balance() - .iter() - .try_fold(0u64, |acc, &(_, val)| acc.checked_add(val)), - _ => None, - }) - .fold((None, false), |(acc, _), price| match acc { - Some(acc_val) => acc_val - .checked_add(price) - .map_or((None, true), |sum| (Some(sum), true)), - None => (Some(price), true), - }); - - match (total, any_voting_funds) { - (Some(total), _) => Ok(Some(total)), - (None, true) => Err(ProtocolError::Overflow( - "overflow in all voting funds amount", - )), - _ => Ok(None), - } - } - - /// The security levels a key must have to sign this batch: the highest - /// level any document type or token action in the batch requires. - /// - /// An erase acts on a document type like any other document action, so it - /// contributes that type's requirement. - pub fn combined_security_level_requirement(&self) -> Result, ProtocolError> { - let mut highest_security_level = SecurityLevel::lowest_level(); - - for transition in self.transitions.iter() { - let document_security_level = match transition { - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction( - document_transition, - )) => { - let document_type_name = document_transition.base().document_type_name(); - let data_contract_info = document_transition.base().data_contract_fetch_info(); - - data_contract_info - .contract - .document_type_for_name(document_type_name)? - .security_level_requirement() - } - BatchedTransitionActionV1::DocumentErase(erase_transition) => { - let document_type_name = erase_transition.base().document_type_name(); - let data_contract_info = erase_transition.base().data_contract_fetch_info(); - - data_contract_info - .contract - .document_type_for_name(document_type_name)? - .security_level_requirement() - } - BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction(_)) => { - SecurityLevel::CRITICAL - } - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(_), - ) => continue, - }; - - // lower enum representation means higher in security - if document_security_level < highest_security_level { - highest_security_level = document_security_level - } - } - - Ok(if highest_security_level == SecurityLevel::MASTER { - vec![SecurityLevel::MASTER] - } else { - // this might seem wrong until you realize that master is 0, critical 1, etc - (SecurityLevel::CRITICAL as u8..=highest_security_level as u8) - .map(|security_level| { - SecurityLevel::try_from(security_level).map_err(|_| { - ProtocolError::CorruptedCodeExecution(format!( - "security level {security_level} is between two known levels" - )) - }) - }) - .collect::, _>>()? - }) - } -} diff --git a/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs b/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs deleted file mode 100644 index 20207a570cb..00000000000 --- a/packages/rs-drive/src/state_transition_action/batch/v1/tests.rs +++ /dev/null @@ -1,316 +0,0 @@ -//! Batch action format 1 must agree with format 0 on every item format 0 can -//! express, and treat an erase as a document action that moves no credits. - -use std::collections::BTreeMap; -use std::sync::Arc; - -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::document_type::DocumentType; -use dpp::document::{Document, DocumentV0}; -use dpp::identifier::Identifier; -use dpp::identity::SecurityLevel; -use dpp::platform_value::platform_value; -use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; -use dpp::version::PlatformVersion; -use grovedb_costs::OperationCost; - -use crate::drive::contract::DataContractFetchInfo; -use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{ - DocumentBaseTransitionAction, DocumentBaseTransitionActionV0, -}; -use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionV0; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::DocumentEraseTransitionAction; -use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{ - DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionV0, -}; -use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ - TokenBaseTransitionAction, TokenBaseTransitionActionV0, -}; -use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::{ - TokenDirectPurchaseTransitionAction, TokenDirectPurchaseTransitionActionV0, -}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; -use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use crate::state_transition_action::batch::v0::BatchTransitionActionV0; -use crate::state_transition_action::batch::v1::{BatchTransitionActionV1, BatchedTransitionActionV1}; -use crate::state_transition_action::batch::BatchTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{ - BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0, -}; - -const OWNER_ID: [u8; 32] = [0x11; 32]; -const DOCUMENT_ID: [u8; 32] = [0xAA; 32]; -const PURCHASE_PRICE: u64 = 5_000; -const TOKEN_PRICE: u64 = 10_000; - -fn dpns_contract_info() -> Arc { - Arc::new(DataContractFetchInfo::dpns_contract_fixture( - PlatformVersion::latest().protocol_version, - )) -} - -/// The DPNS contract with one extra document type whose signature must come -/// from a master key. -fn contract_info_with_master_level_type() -> Arc { - let platform_version = PlatformVersion::latest(); - let mut contract = dpns_contract_info().contract.clone(); - let config = *contract.config(); - let schema = platform_value!({ - "type": "object", - "properties": { - "note": { - "type": "string", - "position": 0, - } - }, - "signatureSecurityLevelRequirement": SecurityLevel::MASTER as u8, - "additionalProperties": false, - }); - let document_type = DocumentType::try_from_schema( - contract.id(), - 1, - config.version(), - "vault", - schema, - None, - &BTreeMap::new(), - &config, - false, - &mut Vec::new(), - platform_version, - ) - .expect("a document type with a master-level signature requirement"); - contract - .document_types_mut() - .insert("vault".to_string(), document_type); - Arc::new(DataContractFetchInfo { - contract, - storage_flags: None, - cost: OperationCost::default(), - fee: None, - }) -} - -fn document_base( - document_type_name: &str, - data_contract: Arc, -) -> DocumentBaseTransitionAction { - DocumentBaseTransitionAction::V0(DocumentBaseTransitionActionV0 { - id: Identifier::from(DOCUMENT_ID), - identity_contract_nonce: 1, - document_type_name: document_type_name.to_string(), - data_contract, - token_cost: None, - gas_fees_paid_by: GasFeesPaidBy::default(), - }) -} - -fn erase( - document_type_name: &str, - data_contract: Arc, -) -> BatchedTransitionActionV1 { - BatchedTransitionActionV1::DocumentErase(DocumentEraseTransitionAction::V0( - DocumentEraseTransitionActionV0 { - base: document_base(document_type_name, data_contract), - }, - )) -} - -fn domain_delete() -> BatchedTransitionAction { - BatchedTransitionAction::DocumentAction(DocumentTransitionAction::DeleteAction( - DocumentDeleteTransitionAction::V0(DocumentDeleteTransitionActionV0 { - base: document_base("domain", dpns_contract_info()), - }), - )) -} - -fn domain_purchase() -> BatchedTransitionAction { - BatchedTransitionAction::DocumentAction(DocumentTransitionAction::PurchaseAction( - DocumentPurchaseTransitionAction::V0(DocumentPurchaseTransitionActionV0 { - base: document_base("domain", dpns_contract_info()), - document: Document::V0(DocumentV0::default()), - original_owner_id: Identifier::from([0xDD; 32]), - price: PURCHASE_PRICE, - }), - )) -} - -fn token_purchase() -> BatchedTransitionAction { - BatchedTransitionAction::TokenAction(TokenTransitionAction::DirectPurchaseAction( - TokenDirectPurchaseTransitionAction::V0(TokenDirectPurchaseTransitionActionV0 { - base: TokenBaseTransitionAction::V0(TokenBaseTransitionActionV0 { - token_id: Identifier::from([0xBB; 32]), - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract: dpns_contract_info(), - store_in_group: None, - perform_action: true, - }), - token_count: 50, - total_agreed_price: TOKEN_PRICE, - }), - )) -} - -fn nonce_bump() -> BatchedTransitionAction { - BatchedTransitionAction::BumpIdentityDataContractNonce(BumpIdentityDataContractNonceAction::V0( - BumpIdentityDataContractNonceActionV0 { - identity_id: Identifier::from(OWNER_ID), - data_contract_id: Identifier::from([0x22; 32]), - identity_contract_nonce: 5, - user_fee_increase: 0, - }, - )) -} - -fn format_0_batch(transitions: Vec) -> BatchTransitionAction { - BatchTransitionAction::V0(BatchTransitionActionV0 { - owner_id: Identifier::from(OWNER_ID), - transitions, - user_fee_increase: 10, - }) -} - -fn format_1_batch(transitions: Vec) -> BatchTransitionActionV1 { - BatchTransitionActionV1 { - owner_id: Identifier::from(OWNER_ID), - transitions, - user_fee_increase: 10, - } -} - -#[test] -fn should_upcast_a_format_0_batch_keeping_its_items_in_order() { - let upcast: BatchTransitionActionV1 = - format_0_batch(vec![domain_delete(), token_purchase(), nonce_bump()]).into(); - - assert_eq!(upcast.owner_id(), Identifier::from(OWNER_ID)); - assert_eq!(upcast.user_fee_increase(), 10); - let kinds: Vec<_> = upcast - .transitions() - .iter() - .map(|item| match item { - BatchedTransitionActionV1::Batched(BatchedTransitionAction::DocumentAction(_)) => { - "document" - } - BatchedTransitionActionV1::Batched(BatchedTransitionAction::TokenAction(_)) => "token", - BatchedTransitionActionV1::Batched( - BatchedTransitionAction::BumpIdentityDataContractNonce(_), - ) => "bump", - BatchedTransitionActionV1::DocumentErase(_) => "erase", - }) - .collect(); - assert_eq!(kinds, ["document", "token", "bump"]); -} - -/// The credits a batch commits up front decide whether the signer can afford -/// it; an erase commits none, so its presence must not change the sums. -#[test] -fn should_sum_the_same_credits_as_format_0_with_an_erase_alongside() { - let format_0 = format_0_batch(vec![domain_purchase(), token_purchase()]); - let with_erase = format_1_batch(vec![ - erase("domain", dpns_contract_info()), - BatchedTransitionActionV1::Batched(domain_purchase()), - BatchedTransitionActionV1::Batched(token_purchase()), - ]); - - assert_eq!( - with_erase.all_purchases_amount().unwrap(), - format_0.all_purchases_amount().unwrap() - ); - assert_eq!( - with_erase.all_purchases_amount().unwrap(), - Some(PURCHASE_PRICE + TOKEN_PRICE) - ); - assert_eq!( - with_erase - .all_conflicting_index_collateral_voting_funds() - .unwrap(), - format_0 - .all_conflicting_index_collateral_voting_funds() - .unwrap() - ); - assert_eq!( - with_erase.all_used_balances().unwrap(), - format_0.all_used_balances().unwrap() - ); - - let erase_only = format_1_batch(vec![erase("domain", dpns_contract_info())]); - assert_eq!(erase_only.all_purchases_amount().unwrap(), None); - assert_eq!( - erase_only - .all_conflicting_index_collateral_voting_funds() - .unwrap(), - None - ); - assert_eq!(erase_only.all_used_balances().unwrap(), None); -} - -/// An erase is signed like any other action on its document type. -#[test] -fn should_require_the_erased_types_security_level() { - let domain_level = dpns_contract_info() - .contract - .document_type_for_name("domain") - .expect("the DPNS contract has a domain type") - .security_level_requirement(); - assert_ne!( - domain_level, - SecurityLevel::MASTER, - "the test needs a type whose requirement spans more than one level" - ); - - let erase_only = format_1_batch(vec![erase("domain", dpns_contract_info())]); - let delete_only = format_0_batch(vec![domain_delete()]); - - let expected: Vec = (SecurityLevel::CRITICAL as u8..=domain_level as u8) - .map(|level| SecurityLevel::try_from(level).unwrap()) - .collect(); - assert_eq!( - erase_only.combined_security_level_requirement().unwrap(), - expected - ); - assert_eq!( - erase_only.combined_security_level_requirement().unwrap(), - delete_only.combined_security_level_requirement().unwrap(), - "an erase and a delete of the same type demand the same key" - ); -} - -#[test] -fn should_require_a_critical_key_for_a_token_action() { - let batch = format_1_batch(vec![ - erase("domain", dpns_contract_info()), - BatchedTransitionActionV1::Batched(token_purchase()), - ]); - assert_eq!( - batch.combined_security_level_requirement().unwrap(), - vec![SecurityLevel::CRITICAL] - ); -} - -#[test] -fn should_require_a_master_key_alone_when_an_erased_type_demands_it() { - let batch = format_1_batch(vec![ - BatchedTransitionActionV1::Batched(domain_delete()), - erase("vault", contract_info_with_master_level_type()), - ]); - assert_eq!( - batch.combined_security_level_requirement().unwrap(), - vec![SecurityLevel::MASTER] - ); -} - -#[test] -fn should_agree_with_format_0_on_a_batch_of_nonce_bumps() { - let format_0 = format_0_batch(vec![nonce_bump(), nonce_bump()]); - let format_1: BatchTransitionActionV1 = format_0.clone().into(); - assert_eq!( - format_1.combined_security_level_requirement().unwrap(), - format_0.combined_security_level_requirement().unwrap() - ); -} diff --git a/packages/rs-drive/src/state_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/mod.rs index 9c3719e8615..ee88bc1cf2e 100644 --- a/packages/rs-drive/src/state_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/mod.rs @@ -17,7 +17,6 @@ pub mod shielded; use crate::state_transition_action::address_funds::address_credit_withdrawal::AddressCreditWithdrawalTransitionAction; use crate::state_transition_action::address_funds::address_funding_from_asset_lock::AddressFundingFromAssetLockTransitionAction; use crate::state_transition_action::address_funds::address_funds_transfer::AddressFundsTransferTransitionAction; -use crate::state_transition_action::batch::v1::BatchTransitionActionV1; use crate::state_transition_action::batch::BatchTransitionAction; use crate::state_transition_action::contract::data_contract_create::DataContractCreateTransitionAction; use crate::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; @@ -63,10 +62,6 @@ pub enum StateTransitionAction { DataContractUpdateAction(DataContractUpdateTransitionAction), /// batch BatchAction(BatchTransitionAction), - /// batch, in the action format that can carry an erase of a keep-history - /// document; produced only for protocol versions whose batch wire format - /// carries one - BatchActionV1(BatchTransitionActionV1), /// identity create IdentityCreateAction(IdentityCreateTransitionAction), /// identity create from addresses @@ -130,7 +125,6 @@ impl StateTransitionAction { StateTransitionAction::DataContractCreateAction(action) => action.user_fee_increase(), StateTransitionAction::DataContractUpdateAction(action) => action.user_fee_increase(), StateTransitionAction::BatchAction(action) => action.user_fee_increase(), - StateTransitionAction::BatchActionV1(action) => action.user_fee_increase(), StateTransitionAction::IdentityCreateAction(action) => action.user_fee_increase(), StateTransitionAction::IdentityTopUpAction(action) => action.user_fee_increase(), StateTransitionAction::IdentityCreditWithdrawalAction(action) => { diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs index 2a746d36839..d20d7616265 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs @@ -1,7 +1,7 @@ use dpp::platform_value::Identifier; use dpp::prelude::UserFeeIncrease; use dpp::ProtocolError; -use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRef, BatchedTransitionRefV1}; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; @@ -9,10 +9,8 @@ use dpp::state_transition::batch_transition::document_base_transition::DocumentB use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction; -use crate::state_transition_action::batch::v1::BatchedTransitionActionV1; use crate::state_transition_action::contract::data_contract_update::DataContractUpdateTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionAction; -use crate::state_transition_action::batch::batched_transition::document_transition::document_erase_transition_action::v0::DocumentEraseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::{BumpIdentityDataContractNonceAction, BumpIdentityDataContractNonceActionV0}; @@ -39,52 +37,6 @@ impl BumpIdentityDataContractNonceAction { } } - /// from a borrowed transition of any batch format - pub fn from_batched_transition_ref_v1( - value: BatchedTransitionRefV1, - identity_id: Identifier, - user_fee_increase: UserFeeIncrease, - ) -> Self { - match value { - BatchedTransitionRefV1::Document(document) => { - Self::from_borrowed_document_base_transition( - document.base(), - identity_id, - user_fee_increase, - ) - } - BatchedTransitionRefV1::Token(token) => Self::from_borrowed_token_base_transition( - token.base(), - identity_id, - user_fee_increase, - ), - } - } - - /// helper method for an item of batch action format 1 - pub fn try_from_borrowed_batched_transition_action_v1( - value: &BatchedTransitionActionV1, - identity_id: Identifier, - user_fee_increase: UserFeeIncrease, - ) -> Result { - match value { - BatchedTransitionActionV1::Batched(batched_action) => { - Self::try_from_borrowed_batched_transition_action( - batched_action, - identity_id, - user_fee_increase, - ) - } - BatchedTransitionActionV1::DocumentErase(erase_action) => { - Ok(Self::from_borrowed_document_base_transition_action( - erase_action.base(), - identity_id, - user_fee_increase, - )) - } - } - } - /// helper method pub fn try_from_batched_transition_action( value: BatchedTransitionAction, diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs index 80ba10a04b7..3863413a0e2 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/mod.rs @@ -11,7 +11,6 @@ use dpp::state_transition::StateTransition; use dpp::version::PlatformVersion; mod v0; -mod v1; impl Drive { /// Verifies a state transition against a provided proof, returning the @@ -71,16 +70,9 @@ impl Drive { known_contracts_provider_fn, platform_version, ), - 1 => Drive::verify_state_transition_was_executed_with_proof_v1( - state_transition, - block_info, - proof, - known_contracts_provider_fn, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "verify_state_transition_was_executed_with_proof".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 47a467910e8..6151389904b 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -503,6 +503,24 @@ impl Drive { .to_string(), ))) } + DocumentTransition::Erase(erase_transition) => { + // An erase acts on a document that was already + // deleted, so the by-id proof shows it absent + // both before and after. The absence is the + // state the transition affected, not evidence + // that this erase ran; the classifier below + // reports it as such. + if document.is_some() { + return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained a current document for erased id {}", erase_transition.base().id())))); + } + Ok(( + root_hash, + VerifiedDocuments(BTreeMap::from([( + erase_transition.base().id(), + None, + )])), + )) + } } } BatchedTransitionRef::Token(token_transition) => { @@ -2339,10 +2357,17 @@ impl Drive { data_contract_id ))), )?; - !contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? - .index_only() + // EXCEPT an erase: the document it acts on was already + // deleted, so the by-id proof shows the same absence + // whether or not this erase ran. + if matches!(document_transition, DocumentTransition::Erase(_)) { + false + } else { + !contract + .document_type_for_name(document_transition.document_type_name()) + .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? + .index_only() + } } Some(BatchedTransitionRef::Token(token_transition)) => { let data_contract_id = token_transition.data_contract_id(); @@ -3417,6 +3442,160 @@ mod tests { } } + /// An erase acts on a document that was already deleted, so the by-id + /// proof shows the same absence whether or not it ran. The classifier must + /// report that as affected state, while a delete over the very same proof + /// stays execution-proved: a document's absence by id is read as the + /// delete's outcome, which is what the SDK's strict wait relies on. + #[test] + fn verify_batch_document_erase_is_affected_state_not_execution_proved() { + use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; + use dpp::document::DocumentV0Setters; + use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::{ + DocumentEraseTransition, DocumentEraseTransitionV0, + }; + use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::document_delete_transition::{ + DocumentDeleteTransition, DocumentDeleteTransitionV0, + }; + use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV0}; + use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(None); + let contract = json_document_to_contract( + "tests/supporting_files/contract/family/family-contract-with-history.json", + false, + platform_version, + ) + .expect("expected the family history contract"); + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + None, + None, + platform_version, + ) + .expect("expected to apply the contract"); + let document_type = contract + .document_type_for_name("person") + .expect("expected the person type"); + let mut document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some([4u8; 32].into()), + document_type, + platform_version, + ) + .expect("expected a person"); + for revision in 1..=2u64 { + document.set_revision(Some(revision)); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + platform_version, + None, + ) + .expect("expected to write a revision"); + } + let doc_id = document.id(); + drive + .delete_document_for_contract( + doc_id, + &contract, + "person", + BlockInfo::default_with_time(5_000), + true, + None, + platform_version, + None, + ) + .expect("expected the delete to succeed"); + + let path_query = SingleDocumentDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".to_string(), + document_type_keeps_history: true, + document_id: doc_id.to_buffer(), + block_time_ms: None, + contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, + } + .construct_path_query(platform_version) + .expect("expected a by-id path query"); + let proof = drive + .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) + .expect("expected an absence proof"); + + let base = || { + DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: doc_id, + identity_contract_nonce: 1, + document_type_name: "person".to_string(), + data_contract_id: contract.id(), + }) + }; + let batch = |transition: DocumentTransition| { + StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { + owner_id: Default::default(), + transitions: vec![transition], + ..Default::default() + })) + }; + let contract_arc = Arc::new(contract.clone()); + let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); + let verify = |state_transition: StateTransition| { + Drive::verify_state_transition_was_executed_with_proof( + &state_transition, + &BlockInfo::default(), + &proof, + known_contracts_provider_fn, + platform_version, + ) + .expect("expected verification to succeed") + .1 + }; + + let erase_outcome = verify(batch(DocumentTransition::Erase( + DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: base() }), + ))); + match erase_outcome { + StateTransitionProofOutcome::AffectedState( + StateTransitionProofResult::VerifiedDocuments(documents), + ) => { + assert_eq!(documents.len(), 1); + let (id, found) = documents.into_iter().next().unwrap(); + assert_eq!(id, doc_id); + assert!(found.is_none(), "the document is absent by id"); + } + other => panic!("an erase must be classified as affected state, got {other:?}"), + } + + let delete_outcome = verify(batch(DocumentTransition::Delete( + DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { base: base() }), + ))); + assert!( + matches!( + delete_outcome, + StateTransitionProofOutcome::ExecutionProved(_) + ), + "a delete over the same proof stays execution-proved, got {delete_outcome:?}" + ); + } + // ----------------------------------------------------------------------- // Batch: document create happy path // ----------------------------------------------------------------------- diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs deleted file mode 100644 index 9b3e594bb47..00000000000 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v1/mod.rs +++ /dev/null @@ -1,6410 +0,0 @@ -use std::collections::BTreeMap; -use dpp::address_funds::PlatformAddress; -use dpp::balances::credits::TokenAmount; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::accessors::v1::DataContractV1Getters; -use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dpp::data_contract::associated_token::token_keeps_history_rules::accessors::v0::TokenKeepsHistoryRulesV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dpp::data_contract::serialized_version::DataContractInSerializationFormat; -use dpp::document::{Document, DocumentV0Getters}; -use dpp::document::document_methods::DocumentMethodsV0; -use dpp::document::property_names::PRICE; -use dpp::fee::Credits; -use dpp::group::group_action_status::GroupActionStatus; -use dpp::identity::accessors::IdentityGettersV0; -use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dpp::identity::{IdentityPublicKey, KeyID, PartialIdentity}; -use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; -use dpp::prelude::{AddressNonce, Identifier}; -use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; -use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::{BatchedTransitionRefV1, DocumentTransitionRefV1}; -use dpp::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0; -use dpp::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0; -use dpp::state_transition::identity_credit_transfer_to_addresses_transition::accessors::IdentityCreditTransferToAddressesTransitionAccessorsV0; -use dpp::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0; -use dpp::state_transition::{StateTransition, StateTransitionOwned, StateTransitionWitnessSigned}; -use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; -use dpp::state_transition::batch_transition::document_create_transition::DocumentFromCreateTransition; -use dpp::state_transition::batch_transition::document_replace_transition::DocumentFromReplaceTransition; -use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::token_transition::{TokenTransition, TokenTransitionV0Methods}; -use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; -use dpp::state_transition::batch_transition::token_freeze_transition::v0::v0_methods::TokenFreezeTransitionV0Methods; -use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_methods::TokenMintTransitionV0Methods; -use dpp::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; -use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; -use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; -use dpp::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; -use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; -use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; -use dpp::state_transition::proof_result::StateTransitionProofOutcome; -use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule}; -use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; -use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; -use dpp::voting::vote_polls::VotePoll; -use dpp::voting::votes::resource_vote::accessors::v0::ResourceVoteGettersV0; -use dpp::voting::votes::Vote; -use platform_version::TryIntoPlatformVersioned; -use platform_version::version::PlatformVersion; -use crate::drive::Drive; -use crate::drive::identity::key::fetch::IdentityKeysRequest; -use crate::verify::RootHash; -use crate::error::Error; -use crate::error::proof::ProofError; -use crate::query::{ContractLookupFn, SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; - -impl Drive { - #[inline(always)] - pub(super) fn verify_state_transition_was_executed_with_proof_v1( - state_transition: &StateTransition, - block_info: &BlockInfo, - proof: &[u8], - known_contracts_provider_fn: &ContractLookupFn, - platform_version: &PlatformVersion, - ) -> Result<(RootHash, StateTransitionProofOutcome), Error> { - let (root_hash, result) = match state_transition { - StateTransition::DataContractCreate(data_contract_create) => { - // we expect to get a contract that matches the state transition - let keeps_history = data_contract_create - .data_contract() - .config() - .keeps_history(); - let (root_hash, contract) = Drive::verify_contract( - proof, - Some(keeps_history), - false, - true, - data_contract_create.data_contract().id().into_buffer(), - platform_version, - )?; - let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (create)", data_contract_create.data_contract().id()))))?; - let contract_for_serialization: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version)?; - - if let Some(mismatch) = - contract_for_serialization.first_mismatch(data_contract_create.data_contract()) - { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after create with id {}: {}", data_contract_create.data_contract().id(), mismatch)))); - } - - Ok((root_hash, VerifiedDataContract(contract))) - } - StateTransition::DataContractUpdate(data_contract_update) => { - // we expect to get a contract that matches the state transition - let keeps_history = data_contract_update - .data_contract() - .config() - .keeps_history(); - let (root_hash, contract) = Drive::verify_contract( - proof, - Some(keeps_history), - false, - true, - data_contract_update.data_contract().id().into_buffer(), - platform_version, - )?; - let contract = contract.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain contract with id {} expected to exist because of state transition (update", data_contract_update.data_contract().id()))))?; - let contract_for_serialization: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version)?; - if let Some(mismatch) = - contract_for_serialization.first_mismatch(data_contract_update.data_contract()) - { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain exact expected contract after update with id {}: {}", data_contract_update.data_contract().id(), mismatch)))); - } - Ok((root_hash, VerifiedDataContract(contract))) - } - StateTransition::Batch(documents_batch_transition) => { - if documents_batch_transition.transitions_len_v1() > 1 { - return Err(Error::Proof(ProofError::InvalidTransition(format!("version {} does not support more than one document in a document batch transition", platform_version.protocol_version)))); - } - let Some(transition) = documents_batch_transition.first_transition_v1() else { - return Err(Error::Proof(ProofError::InvalidTransition( - "no transition in a document batch transition".to_string(), - ))); - }; - - let owner_id = documents_batch_transition.owner_id(); - - match transition { - BatchedTransitionRefV1::Document(document_transition) => { - let data_contract_id = document_transition.data_contract_id(); - - let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( - Error::Proof(ProofError::UnknownContract(format!( - "unknown contract with id {} in document verification", - data_contract_id - ))), - )?; - - let document_type = contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| { - Error::Proof(ProofError::UnknownContract(format!( - "cannot fetch contract for document {} with id {}: {}", - document_transition.document_type_name(), - document_transition.data_contract_id(), - e - ))) - })?; - - // indexOnly documents have no primary row: the executed - // transition is verified against the single entry its - // values produce under the proof index — rebuilt here - // from the transition through the same builder the - // prover used. - { - use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; - use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; - if document_type.index_only() { - let values = match document_transition { - DocumentTransitionRefV1::Create(create_transition) => { - create_transition.data().clone() - } - // The indexOnlyDelete kind always - // carries its values. - DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { - delete_transition.data().clone() - } - _ => { - return Err(Error::Proof(ProofError::IncorrectProof( - "indexOnly documents only support create and \ - indexOnlyDelete" - .to_string(), - ))); - } - }; - let path_query = crate::query::index_only_synthesis::index_only_transition_entry_path_query( - contract.id(), - document_type, - &values, - documents_batch_transition.owner_id(), - platform_version, - )?; - let (root_hash, mut proved) = grovedb::GroveDb::verify_query( - proof, - &path_query, - &platform_version.drive.grove_version, - )?; - let entry_element = - proved.pop().and_then(|(_path, _key, element)| element); - - let (root_hash, result) = match document_transition { - DocumentTransitionRefV1::Create(create_transition) => { - let expected_document = - Document::try_from_create_transition( - create_transition, - documents_batch_transition.owner_id(), - block_info, - &contract, - &document_type, - platform_version, - )?; - // The entry's element shape follows the - // proof index's sum axis: a summable - // index stores `ItemWithSumItem( - // commitment, amount)`, a plain one - // stores `Item(commitment)`. The shapes - // are checked strictly — an element of - // the wrong shape is a wrong proof, and - // for the sum-bearing shape the proved - // amount must equal the created - // document's contribution (the value - // the write path froze into the - // element). - let proof_index = crate::query::index_only_synthesis::index_only_proof_index(&document_type)?; - let payload = match ( - entry_element, - proof_index.summable.as_deref(), - ) { - (Some(grovedb::Element::Item(payload, _)), None) => { - payload - } - ( - Some(grovedb::Element::ItemWithSumItem( - payload, - sum_value, - _, - )), - Some(sum_property), - ) => { - let expected_sum = - crate::drive::document::read_document_sum_contribution( - &expected_document, - sum_property, - )?; - if sum_value != expected_sum { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "the proved indexOnly entry's sum contribution does not match the created document {}", - create_transition.base().id() - )))); - } - payload - } - _ => { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "proof did not contain the indexOnly entry item expected to exist after create of {}", - create_transition.base().id() - )))); - } - }; - // Entry presence alone only proves that - // SOME row projects onto this position; - // the stored row commitment binds the - // entry to its document's full tuple, - // so a pre-existing row with the same - // projection but different values in - // other indexes cannot masquerade as - // this create. (`$createdAt`, when the - // type requires it, enters the - // commitment from the caller-supplied - // block info — the same execution-block - // assumption the expected document is - // built under.) - let expected_commitment = - crate::drive::document::index_only_row_commitment( - &expected_document, - document_type, - platform_version, - )?; - if payload != expected_commitment { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "the proved indexOnly entry's row commitment does not match the created document {}: the entry belongs to a different row", - create_transition.base().id() - )))); - } - ( - root_hash, - VerifiedDocuments(BTreeMap::from([( - expected_document.id(), - Some(expected_document), - )])), - ) - } - DocumentTransitionRefV1::IndexOnlyDelete(delete_transition) => { - if entry_element.is_some() { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "proof still contained the indexOnly entry after delete of {}", - delete_transition.base().id() - )))); - } - ( - root_hash, - VerifiedDocuments(BTreeMap::from([( - delete_transition.base().id(), - None, - )])), - ) - } - _ => { - return Err(Error::Proof(ProofError::IncorrectProof( - "indexOnly documents only support create and \ - indexOnlyDelete" - .to_string(), - ))) - } - }; - - // The classifier below is the single - // authority on binding, and it returns false - // for every indexOnly document transition: a - // second create with identical owner/values - // (different entropy) shares the proven entry - // while only one of them executed, and a - // delete's absence proof may describe state - // that was already absent — the proof attests - // the resulting STATE (`AffectedState`), not - // the execution. - let outcome = if Self::state_transition_proof_binds_execution_v1( - state_transition, - known_contracts_provider_fn, - )? { - StateTransitionProofOutcome::ExecutionProved(result) - } else { - StateTransitionProofOutcome::AffectedState(result) - }; - return Ok((root_hash, outcome)); - } - } - - let contested_status = - if let DocumentTransitionRefV1::Create(create_transition) = - document_transition - { - if create_transition.prefunded_voting_balance().is_some() { - SingleDocumentDriveQueryContestedStatus::Contested - } else { - SingleDocumentDriveQueryContestedStatus::NotContested - } - } else { - SingleDocumentDriveQueryContestedStatus::NotContested - }; - - let query = SingleDocumentDriveQuery { - contract_id: document_transition.data_contract_id().into_buffer(), - document_type_name: document_transition.document_type_name().clone(), - document_type_keeps_history: document_type.documents_keep_history(), - document_id: document_transition.base().id().into_buffer(), - block_time_ms: None, //None because we want latest - contested_status, - }; - let (root_hash, document) = - query.verify_proof(false, proof, document_type, platform_version)?; - - match document_transition { - DocumentTransitionRefV1::Create(create_transition) => { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (create)", create_transition.base().id()))))?; - let expected_document = Document::try_from_create_transition( - create_transition, - documents_batch_transition.owner_id(), - block_info, - &contract, - &document_type, - platform_version, - )?; - - let transient_fields = document_type - .transient_fields() - .iter() - .map(|a| a.as_str()) - .collect(); - - if !document.is_equal_ignoring_time_based_fields( - &expected_document, - Some(transient_fields), - platform_version, - )? { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document (time fields were not checked) after create, got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, create_transition)))); - } - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - document.id(), - Some(document), - )])), - )) - } - DocumentTransitionRefV1::Replace(replace_transition) => { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (replace)", replace_transition.base().id()))))?; - let expected_document = Document::try_from_replace_transition( - replace_transition, - documents_batch_transition.owner_id(), - document.created_at(), //we can trust the created at (as we don't care) - document.created_at_block_height(), //we can trust the created at block height (as we don't care) - document.created_at_core_block_height(), //we can trust the created at core block height (as we don't care) - document.created_at(), //we can trust the created at (as we don't care) - document.created_at_block_height(), //we can trust the created at block height (as we don't care) - document.created_at_core_block_height(), //we can trust the created at core block height (as we don't care) - document.creator_id(), - block_info, - &document_type, - platform_version, - )?; - - let transient_fields = document_type - .transient_fields() - .iter() - .map(|a| a.as_str()) - .collect(); - - if !document.is_equal_ignoring_time_based_fields( - &expected_document, - Some(transient_fields), - platform_version, - )? { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document (time fields were not checked) after replace, got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, replace_transition)))); - } - - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - document.id(), - Some(document), - )])), - )) - } - DocumentTransitionRefV1::Transfer(transfer_transition) => { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (transfer)", transfer_transition.base().id()))))?; - let recipient_owner_id = transfer_transition.recipient_owner_id(); - - if document.owner_id() != recipient_owner_id { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not have the transfer executed after expected transfer with id {}", transfer_transition.base().id())))); - } - - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - document.id(), - Some(document), - )])), - )) - } - DocumentTransitionRefV1::Delete(delete_transition) => { - if document.is_some() { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained document after delete with id {}", delete_transition.base().id())))); - } - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - delete_transition.base().id(), - None, - )])), - )) - } - DocumentTransitionRefV1::UpdatePrice(update_price_transition) => { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (update price)", update_price_transition.base().id()))))?; - let new_document_price : Credits = document.properties().get_integer(PRICE).map_err(|e| Error::Proof(ProofError::IncorrectProof(format!("proof did not contain a document that contained a price field with id {} expected to exist because of state transition (update price): {}", update_price_transition.base().id(), e))))?; - if new_document_price != update_price_transition.price() { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not contain expected document update of price after price update with id {}", update_price_transition.base().id())))); - } - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - document.id(), - Some(document), - )])), - )) - } - DocumentTransitionRefV1::Purchase(purchase_transition) => { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document with id {} expected to exist because of state transition (purchase)", purchase_transition.base().id()))))?; - - if document.owner_id() != owner_id { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution did not have the transfer executed after expected transfer with id {}", purchase_transition.base().id())))); - } - - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - document.id(), - Some(document), - )])), - )) - } - DocumentTransitionRefV1::IndexOnlyDelete(_) => { - // Only reachable when the doctype is NOT - // indexOnly (the indexOnly branch above - // returns first): an indexOnlyDelete aimed at - // a stored type can never have executed, so - // there is nothing a proof could attest. - Err(Error::Proof(ProofError::InvalidTransition( - "an indexOnlyDelete cannot execute against a stored \ - document type" - .to_string(), - ))) - } - DocumentTransitionRefV1::Erase(erase_transition) => { - // An erase acts on a document that was already - // deleted, so the by-id proof shows it absent - // both before and after. The absence is the - // state the transition affected, not evidence - // that this erase ran; the classifier below - // reports it as such. - if document.is_some() { - return Err(Error::Proof(ProofError::IncorrectProof(format!("proof of state transition execution contained a current document for erased id {}", erase_transition.base().id())))); - } - Ok(( - root_hash, - VerifiedDocuments(BTreeMap::from([( - erase_transition.base().id(), - None, - )])), - )) - } - } - } - BatchedTransitionRefV1::Token(token_transition) => { - let data_contract_id = token_transition.data_contract_id(); - let token_id = token_transition.token_id(); - - let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( - Error::Proof(ProofError::UnknownContract(format!( - "unknown contract with id {} in token verification", - data_contract_id - ))), - )?; - - let identity_contract_nonce = - token_transition.base().identity_contract_nonce(); - - let token_history_document_type_name = - token_transition.historical_document_type_name().to_string(); - - let token_history_contract = load_system_data_contract( - SystemDataContract::TokenHistory, - platform_version, - )?; - - let token_history_document_type = - token_transition.historical_document_type(&token_history_contract)?; - - let token_config = contract.expected_token_configuration( - token_transition.base().token_contract_position(), - )?; - let keeps_historical_document = token_config.keeps_history(); - - let historical_query = || { - let query = SingleDocumentDriveQuery { - contract_id: token_history_contract.id().into_buffer(), - document_type_name: token_history_document_type_name, - document_type_keeps_history: false, - document_id: token_transition - .historical_document_id(owner_id) - .to_buffer(), - block_time_ms: None, //None because we want latest - contested_status: - SingleDocumentDriveQueryContestedStatus::NotContested, - }; - - let is_group_action = - token_transition.base().using_group_info().is_some(); - - let (root_hash, document) = query.verify_proof( - is_group_action, // it will be a subset if it is a group action - proof, - token_history_document_type, - platform_version, - )?; - - if let Some(document) = &document { - let expected_document = token_transition - .build_historical_document( - token_id, - owner_id, - identity_contract_nonce, - &BlockInfo::default(), - token_config, - platform_version, - )?; - - // Some fields are populated by the drive, - // so we need to ignore them - let ignore_fields = match token_transition { - TokenTransition::DestroyFrozenFunds(_) => { - Some(vec!["destroyedAmount", "note"]) - } - TokenTransition::Burn(_) => Some(vec!["burnFromId", "note"]), - TokenTransition::Claim(_) => Some(vec!["amount"]), - TokenTransition::DirectPurchase(_) => { - let purchase_cost: Credits = - document.properties().get_integer("purchaseCost")?; - let agreed_to_purchase_cost: Credits = expected_document - .properties() - .get_integer("purchaseCost")?; - if purchase_cost > agreed_to_purchase_cost { - return Err(Error::Proof(ProofError::UnexpectedResultProof(format!("proof of state transition execution showed a purchase price of {}, whereas we only agreed to {}, state transition is [{}]", purchase_cost, agreed_to_purchase_cost, token_transition)))); - } - Some(vec!["purchaseCost"]) - } - TokenTransition::Mint(_) - | TokenTransition::Freeze(_) - | TokenTransition::Unfreeze(_) - | TokenTransition::EmergencyAction(_) - | TokenTransition::ConfigUpdate(_) - | TokenTransition::SetPriceForDirectPurchase(_) - if token_transition.base().using_group_info().is_some() => - { - Some(vec!["note"]) - } - _ => None, - }; - - if !document.is_equal_ignoring_time_based_fields( - &expected_document, - ignore_fields, - platform_version, - )? { - return Err(Error::Proof(ProofError::UnexpectedResultProof(format!("proof of state transition execution did not show the correct historical document got: [{}] vs expected: [{}], state transition is [{}]", document, expected_document, token_transition)))); - } - } - - if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let action_status = if document.is_some() { - GroupActionStatus::ActionClosed - } else { - GroupActionStatus::ActionActive - }; - let sum_power = Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - Some(action_status), - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )? - .2; - Ok(( - root_hash, - VerifiedTokenGroupActionWithDocument(sum_power, document), - )) - } else { - let document = document.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain document of type `{}` expected to exist because the token keeps historical documents", token_transition.historical_document_type_name()))))?; - Ok((root_hash, VerifiedTokenActionWithDocument(document))) - } - }; - match token_transition { - TokenTransition::Burn(_) => { - if keeps_historical_document.keeps_burning_history() { - historical_query() - } else if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let (_root_hash, status, sum_power) = - Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - None, - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )?; - - let (root_hash, balance) = - Drive::verify_token_balance_for_identity_id( - proof, - token_id.into_buffer(), - owner_id.into_buffer(), - true, - platform_version, - )?; - if status == GroupActionStatus::ActionClosed - && balance.is_none() - { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token burn)", owner_id)))); - }; - - Ok(( - root_hash, - VerifiedTokenGroupActionWithTokenBalance( - sum_power, status, balance, - ), - )) - } else { - { - let (root_hash, Some(balance)) = - Drive::verify_token_balance_for_identity_id( - proof, - token_id.into_buffer(), - owner_id.into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token burn)", owner_id)))); - }; - Ok((root_hash, VerifiedTokenBalance(owner_id, balance))) - } - } - } - TokenTransition::Mint(token_mint_transition) => { - if keeps_historical_document.keeps_minting_history() { - historical_query() - } else if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let (_root_hash, status, sum_power) = - Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - None, - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )?; - - let recipient_id = - token_mint_transition.recipient_id(token_config)?; - - let (root_hash, balance) = - Drive::verify_token_balance_for_identity_id( - proof, - token_id.into_buffer(), - recipient_id.into_buffer(), - true, - platform_version, - )?; - if status == GroupActionStatus::ActionClosed - && balance.is_none() - { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token mint)", owner_id)))); - }; - - Ok(( - root_hash, - VerifiedTokenGroupActionWithTokenBalance( - sum_power, status, balance, - ), - )) - } else { - { - let recipient_id = - token_mint_transition.recipient_id(token_config)?; - let (root_hash, Some(balance)) = - Drive::verify_token_balance_for_identity_id( - proof, - token_id.into_buffer(), - recipient_id.into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token mint)", recipient_id)))); - }; - Ok((root_hash, VerifiedTokenBalance(recipient_id, balance))) - } - } - } - TokenTransition::Transfer(token_transfer_transition) => { - if keeps_historical_document.keeps_transfer_history() { - historical_query() - } else { - { - let recipient_id = token_transfer_transition.recipient_id(); - let identity_ids = - [owner_id.to_buffer(), recipient_id.to_buffer()]; - let (root_hash, balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_token_balances_for_identity_ids( - proof, - token_id.into_buffer(), - &identity_ids, - false, - platform_version, - )?; - - let balances = balances.into_iter().map(|(id, maybe_balance)| { - let balance = maybe_balance.ok_or(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token transfer)", id))))?; - Ok((id, balance)) - }).collect::>()?; - - Ok((root_hash, VerifiedTokenIdentitiesBalances(balances))) - } - } - } - TokenTransition::Freeze(token_freeze_transition) => { - if keeps_historical_document.keeps_freezing_history() { - historical_query() - } else if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let (_root_hash, status, sum_power) = - Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - None, - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )?; - - let (root_hash, identity_token_info) = - Drive::verify_token_info_for_identity_id( - proof, - token_id.into_buffer(), - token_freeze_transition - .frozen_identity_id() - .into_buffer(), - true, - platform_version, - )?; - if status == GroupActionStatus::ActionClosed - && identity_token_info.is_none() - { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token freeze)", owner_id)))); - }; - - Ok(( - root_hash, - VerifiedTokenGroupActionWithTokenIdentityInfo( - sum_power, - status, - identity_token_info, - ), - )) - } else { - let (root_hash, Some(identity_token_info)) = - Drive::verify_token_info_for_identity_id( - proof, - token_id.into_buffer(), - token_freeze_transition - .frozen_identity_id() - .into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token info for identity {} expected to exist because of state transition (token freeze)", token_freeze_transition.frozen_identity_id())))); - }; - if !identity_token_info.frozen() { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof contained token info saying this token was not frozen for identity {}", token_freeze_transition.frozen_identity_id())))); - } - Ok(( - root_hash, - VerifiedTokenIdentityInfo(owner_id, identity_token_info), - )) - } - } - TokenTransition::Unfreeze(token_unfreeze_transition) => { - if keeps_historical_document.keeps_freezing_history() { - historical_query() - } else if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let (_root_hash, status, sum_power) = - Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - None, - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )?; - - let (root_hash, identity_token_info) = - Drive::verify_token_info_for_identity_id( - proof, - token_id.into_buffer(), - token_unfreeze_transition - .frozen_identity_id() - .into_buffer(), - true, - platform_version, - )?; - if status == GroupActionStatus::ActionClosed - && identity_token_info.is_none() - { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token unfreeze)", owner_id)))); - }; - - Ok(( - root_hash, - VerifiedTokenGroupActionWithTokenIdentityInfo( - sum_power, - status, - identity_token_info, - ), - )) - } else { - let (root_hash, Some(identity_token_info)) = - Drive::verify_token_info_for_identity_id( - proof, - token_id.into_buffer(), - token_unfreeze_transition - .frozen_identity_id() - .into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token info for identity {} expected to exist because of state transition (token freeze)", token_unfreeze_transition.frozen_identity_id())))); - }; - if identity_token_info.frozen() { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof contained token info saying this token was frozen for identity {} when we just unfroze it", token_unfreeze_transition.frozen_identity_id())))); - } - Ok(( - root_hash, - VerifiedTokenIdentityInfo(owner_id, identity_token_info), - )) - } - } - TokenTransition::DirectPurchase(_) => { - if keeps_historical_document.keeps_direct_purchase_history() { - historical_query() - } else { - let (root_hash, Some(balance)) = - Drive::verify_token_balance_for_identity_id( - proof, - token_id.into_buffer(), - owner_id.into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token balance for identity {} expected to exist because of state transition (token direct purchase)", owner_id)))); - }; - Ok((root_hash, VerifiedTokenBalance(owner_id, balance))) - } - } - TokenTransition::SetPriceForDirectPurchase(_) => { - if keeps_historical_document.keeps_direct_pricing_history() { - historical_query() - } else if let Some(group_state_transition_info) = - token_transition.base().using_group_info() - { - let (_root_hash, status, sum_power) = - Drive::verify_action_signer_and_total_power( - proof, - data_contract_id, - group_state_transition_info.group_contract_position, - None, - group_state_transition_info.action_id, - owner_id, - true, - platform_version, - )?; - - let (root_hash, token_pricing_schedule) = - Drive::verify_token_direct_selling_price( - proof, - token_id.into_buffer(), - true, - platform_version, - )?; - if status == GroupActionStatus::ActionClosed - && token_pricing_schedule.is_none() - { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain token identity info for identity {} expected to exist because of state transition (token set price for direct purchase)", owner_id)))); - }; - - Ok(( - root_hash, - VerifiedTokenGroupActionWithTokenPricingSchedule( - sum_power, - status, - token_pricing_schedule, - ), - )) - } else { - let (root_hash, token_pricing_schedule) = - Drive::verify_token_direct_selling_price( - proof, - token_id.into_buffer(), - false, - platform_version, - )?; - Ok(( - root_hash, - VerifiedTokenPricingSchedule( - owner_id, - token_pricing_schedule, - ), - )) - } - } - TokenTransition::DestroyFrozenFunds(_) - | TokenTransition::EmergencyAction(_) - | TokenTransition::ConfigUpdate(_) - | TokenTransition::Claim(_) => historical_query(), - } - } - } - } - StateTransition::IdentityCreate(identity_create_transition) => { - // we expect to get an identity that matches the state transition - let (root_hash, identity) = Drive::verify_full_identity_by_identity_id( - proof, - false, - identity_create_transition.identity_id().into_buffer(), - platform_version, - )?; - let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain identity {} expected to exist because of state transition (create)", identity_create_transition.identity_id()))))?; - let expected_keys: BTreeMap = identity_create_transition - .public_keys() - .iter() - .map(|key| { - let stored_key: IdentityPublicKey = key.into(); - (stored_key.id(), stored_key) - }) - .collect(); - if expected_keys.len() != identity_create_transition.public_keys().len() { - return Err(Error::Proof(ProofError::InvalidTransition( - "identity create transition contains duplicate public key ids".to_string(), - ))); - } - if identity.public_keys() != &expected_keys { - return Err(Error::Proof(ProofError::IncorrectProof( - "identity create proof contains public keys that do not match the transition" - .to_string(), - ))); - } - Ok((root_hash, VerifiedIdentity(identity))) - } - StateTransition::IdentityTopUp(identity_top_up_transition) => { - // snapshot of the identity's balance and revision at the proof's block - let identity_id = identity_top_up_transition.identity_id(); - let (root_hash, Some((balance, revision))) = - Drive::verify_identity_balance_and_revision_for_identity_id( - proof, - identity_id.into_buffer(), - false, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain balance for identity {} expected to exist because of state transition (top up)", identity_id)))); - }; - Ok(( - root_hash, - VerifiedPartialIdentity(PartialIdentity { - id: *identity_top_up_transition.identity_id(), - loaded_public_keys: Default::default(), - balance: Some(balance), - revision: Some(revision), - not_found_public_keys: Default::default(), - }), - )) - } - StateTransition::IdentityCreditWithdrawal(identity_credit_withdrawal_transition) => { - { - // snapshot of the identity's balance at the proof's block - let (root_hash, balance) = Drive::verify_identity_balance_for_identity_id( - proof, - identity_credit_withdrawal_transition - .identity_id() - .into_buffer(), - false, - platform_version, - )?; - let balance = balance.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity {} expected to exist because of state transition (withdrawal)", identity_credit_withdrawal_transition.identity_id()))))?; - Ok(( - root_hash, - VerifiedPartialIdentity(PartialIdentity { - id: identity_credit_withdrawal_transition.identity_id(), - loaded_public_keys: Default::default(), - balance: Some(balance), - revision: None, - not_found_public_keys: Default::default(), - }), - )) - } - } - StateTransition::IdentityUpdate(identity_update_transition) => { - // we expect to get an identity that matches the state transition - let (root_hash, identity) = Drive::verify_identity_keys_by_identity_id( - proof, - IdentityKeysRequest::new_all_keys_query( - &identity_update_transition.identity_id().into_buffer(), - None, - ), - true, - false, - false, - platform_version, - )?; - let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain update for identity {} expected to exist because of state transition (update)", identity_update_transition.identity_id()))))?; - - if identity.revision != Some(identity_update_transition.revision()) { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "identity update proof contains revision {:?}, expected {}", - identity.revision, - identity_update_transition.revision() - )))); - } - - for key in identity_update_transition.public_keys_to_add() { - let expected_key: IdentityPublicKey = key.into(); - if identity.loaded_public_keys.get(&expected_key.id()) != Some(&expected_key) { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "identity update proof does not contain the expected state for added key {}", - expected_key.id() - )))); - } - } - - for key_id in identity_update_transition.public_key_ids_to_disable() { - let Some(proved_key) = identity.loaded_public_keys.get(key_id) else { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "identity update proof does not contain disabled key {}", - key_id - )))); - }; - // The proof may be generated at a block later than the one that - // executed the update; `disabled_at` never changes afterwards, so - // require the key to be disabled at or before the proof's block - // rather than exactly at it. - match proved_key.disabled_at() { - Some(disabled_at) if disabled_at <= block_info.time_ms => {} - _ => { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "identity update proof contains an unexpected disabled timestamp for key {}", - key_id - )))); - } - } - } - Ok((root_hash, VerifiedPartialIdentity(identity))) - } - StateTransition::IdentityCreditTransfer(identity_credit_transfer) => { - // snapshot of the sender's and recipient's balances at the proof's block - let (root_hash_identity, balance_identity) = - Drive::verify_identity_balance_for_identity_id( - proof, - identity_credit_transfer.identity_id().into_buffer(), - true, - platform_version, - )?; - - let (root_hash_recipient, balance_recipient) = - Drive::verify_identity_balance_for_identity_id( - proof, - identity_credit_transfer.recipient_id().into_buffer(), - true, - platform_version, - )?; - - if root_hash_identity != root_hash_recipient { - return Err(Error::Proof(ProofError::CorruptedProof("proof is expected to have same root hash for all subsets (identity transfer)".to_string()))); - } - - let balance_identity = balance_identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity sender {} expected to exist because of state transition (transfer)", identity_credit_transfer.identity_id()))))?; - let balance_recipient = balance_recipient.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain balance for identity recipient {} expected to exist because of state transition (transfer)", identity_credit_transfer.recipient_id()))))?; - - Ok(( - root_hash_identity, - VerifiedBalanceTransfer( - PartialIdentity { - id: identity_credit_transfer.identity_id(), - loaded_public_keys: Default::default(), - balance: Some(balance_identity), - revision: None, - not_found_public_keys: Default::default(), - }, - PartialIdentity { - id: identity_credit_transfer.recipient_id(), - loaded_public_keys: Default::default(), - balance: Some(balance_recipient), - revision: None, - not_found_public_keys: Default::default(), - }, - ), - )) - } - StateTransition::MasternodeVote(masternode_vote) => { - let pro_tx_hash = masternode_vote.pro_tx_hash(); - let vote = masternode_vote.vote(); - let contract = match vote { - Vote::ResourceVote(resource_vote) => match resource_vote.vote_poll() { - VotePoll::ContestedDocumentResourceVotePoll( - contested_document_resource_vote_poll, - ) => known_contracts_provider_fn( - &contested_document_resource_vote_poll.contract_id, - )? - .ok_or(Error::Proof( - ProofError::UnknownContract(format!( - "unknown contract with id {} in resource vote verification", - contested_document_resource_vote_poll.contract_id - )), - ))?, - }, - }; - - // we expect to get a vote that matches the state transition - let (root_hash, vote) = Drive::verify_masternode_vote( - proof, - pro_tx_hash.to_buffer(), - vote, - &contract, - false, - platform_version, - )?; - let vote = vote.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain actual vote for masternode {} expected to exist because of state transition (masternode vote)", masternode_vote.pro_tx_hash()))))?; - Ok((root_hash, VerifiedMasternodeVote(vote))) - } - StateTransition::IdentityCreditTransferToAddresses(st) => { - let identity_id = st.identity_id(); - // verify_subset_of_proof=true because we verify identity balance/revision - // and address balances as separate subsets of the same merged proof - let (root_hash_identity, Some((balance, revision)), address_balances) = - Drive::verify_identity_balance_revision_and_addresses_from_inputs( - proof, - identity_id.to_buffer(), - st.recipient_addresses().keys(), - true, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain balance for identity {} expected to exist because of state transition (identity credit transfer to addresses)", identity_id) - ))); - }; - - Ok(( - root_hash_identity, - VerifiedIdentityWithAddressInfos( - PartialIdentity { - id: identity_id, - loaded_public_keys: Default::default(), - balance: Some(balance), - revision: Some(revision), - not_found_public_keys: Default::default(), - }, - address_balances, - ), - )) - } - StateTransition::IdentityCreateFromAddresses(st) => { - use dpp::state_transition::StateTransitionIdentityIdFromInputs; - let identity_id = st.identity_id_from_inputs().map_err(|e| { - Error::Proof(ProofError::CorruptedProof(format!( - "Failed to calculate identity id from inputs: {}", - e - ))) - })?; - let (root_hash_identity, identity) = Drive::verify_full_identity_by_identity_id( - proof, - true, - identity_id.into_buffer(), - platform_version, - )?; - let identity = identity.ok_or(Error::Proof(ProofError::IncorrectProof(format!("proof did not contain identity {} expected to exist because of state transition (create from addresses)", identity_id))))?; - - let expected_keys: BTreeMap = st - .public_keys() - .iter() - .map(|key| { - let stored_key: IdentityPublicKey = key.into(); - (stored_key.id(), stored_key) - }) - .collect(); - if expected_keys.len() != st.public_keys().len() { - return Err(Error::Proof(ProofError::InvalidTransition( - "identity create from addresses transition contains duplicate public key ids" - .to_string(), - ))); - } - if identity.public_keys() != &expected_keys { - return Err(Error::Proof(ProofError::IncorrectProof( - "identity create from addresses proof contains public keys that do not match the transition" - .to_string(), - ))); - } - - let addresses_to_check = st - .inputs() - .keys() - .chain(st.output().into_iter().map(|(address, _)| address)); - - let (root_hash_addresses, address_balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_addresses_infos( - proof, - addresses_to_check, - true, - platform_version, - )?; - - if root_hash_identity != root_hash_addresses { - return Err(Error::Proof(ProofError::CorruptedProof( - "proof is expected to have same root hash for identity and address infos" - .to_string(), - ))); - } - - Ok(( - root_hash_identity, - VerifiedIdentityFullWithAddressInfos(identity, address_balances), - )) - } - StateTransition::IdentityTopUpFromAddresses(st) => { - // Verify revision and balance for the identity - use dpp::state_transition::identity_topup_from_addresses_transition::accessors::IdentityTopUpFromAddressesTransitionAccessorsV0; - let identity_id = st.identity_id(); - let addresses_to_check = st - .inputs() - .keys() - .chain(st.output().into_iter().map(|(address, _)| address)); - // verify_subset_of_proof=true because we verify identity balance/revision - // and address balances as separate subsets of the same merged proof - let (root_hash_identity, Some((balance, revision)), address_balances) = - Drive::verify_identity_balance_revision_and_addresses_from_inputs( - proof, - identity_id.to_buffer(), - addresses_to_check, - true, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - format!("proof did not contain balance for identity {} expected to exist because of state transition (top up from addresses)", identity_id)))); - }; - - Ok(( - root_hash_identity, - VerifiedIdentityWithAddressInfos( - PartialIdentity { - id: *identity_id, - loaded_public_keys: Default::default(), - balance: Some(balance), - revision: Some(revision), - not_found_public_keys: Default::default(), - }, - address_balances, - ), - )) - } - StateTransition::AddressFundsTransfer(st) => { - // snapshot of the input and output addresses at the proof's block - use dpp::state_transition::address_funds_transfer_transition::accessors::AddressFundsTransferTransitionAccessorsV0; - use dpp::state_transition::StateTransitionWitnessSigned; - let (root_hash, address_balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_addresses_infos( - proof, - st.inputs().keys().chain(st.outputs().keys()), - false, - platform_version, - )?; - - Ok((root_hash, VerifiedAddressInfos(address_balances))) - } - StateTransition::AddressFundingFromAssetLock(st) => { - // snapshot of the input and output addresses at the proof's block - use dpp::state_transition::address_funding_from_asset_lock_transition::accessors::AddressFundingFromAssetLockTransitionAccessorsV0; - let (root_hash, balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_addresses_infos( - proof, - st.inputs().keys().chain(st.outputs().keys()), - false, - platform_version, - )?; - - Ok((root_hash, VerifiedAddressInfos(balances))) - } - StateTransition::AddressCreditWithdrawal(st) => { - // snapshot of the input and output addresses at the proof's block - use dpp::state_transition::StateTransitionWitnessSigned; - let addresses_to_check = st - .inputs() - .keys() - .chain(st.output().into_iter().map(|(address, _)| address)); - let (root_hash, balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_addresses_infos( - proof, - addresses_to_check, - false, - platform_version, - )?; - - Ok((root_hash, VerifiedAddressInfos(balances))) - } - StateTransition::Shield(st) => { - // snapshot of the input addresses at the proof's block - use dpp::state_transition::StateTransitionWitnessSigned; - let (root_hash, balances): ( - RootHash, - BTreeMap>, - ) = Drive::verify_addresses_infos( - proof, - st.inputs().keys(), - false, - platform_version, - )?; - Ok((root_hash, VerifiedAddressInfos(balances))) - } - StateTransition::Unshield(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos; - use dpp::state_transition::unshield_transition::accessors::UnshieldTransitionAccessorsV0; - use grovedb::Element; - - let nullifier_keys: Vec> = st.nullifiers(); - - // Reconstruct the prove side's merged query — nullifier spend-status ∪ - // output-address balance — and verify it strictly. See - // `verify_merged_query_strict` for why a single strict merged verify is - // sound and rejects proofs padded with extra subtree branches. - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys); - let nullifier_pq = grovedb::PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let address_pq = - Drive::balances_for_clear_addresses_query(std::iter::once(st.output_address())); - - let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( - proof, - vec![nullifier_pq, address_pq], - platform_version, - )?; - - // Partition the proved key/values by path: entries under the nullifiers tree are - // spend statuses, entries under the clear-address pool are address balances. - let nullifiers_path = shielded_credit_pool_nullifiers_path_vec(); - let addresses_path = Drive::clear_addresses_path(); - - let mut statuses: Vec<(Vec, bool)> = Vec::new(); - let mut balances: BTreeMap> = - BTreeMap::new(); - - for (path, key, element) in proved_key_values { - if path == nullifiers_path { - // A present element means the nullifier is spent; absence means unspent. - statuses.push((key, element.is_some())); - } else if path == addresses_path { - // Mirror `verify_addresses_infos_v0`: reconstruct the address from the key - // and decode the `ItemWithSumItem` (nonce, balance) element. - let address = PlatformAddress::from_bytes(&key).map_err(|e| { - Error::Proof(ProofError::CorruptedProof(format!( - "failed to deserialize output PlatformAddress: {}", - e - ))) - })?; - - let balance_info = element - .map(|element| { - let Element::ItemWithSumItem(nonce_vec, balance_i64, _) = element - else { - return Err(Error::Proof(ProofError::CorruptedProof( - "expected an item with sum item element".to_string(), - ))); - }; - - let nonce_bytes: [u8; 4] = nonce_vec.try_into().map_err(|_| { - Error::Proof(ProofError::IncorrectValueSize( - "nonce should be 4 bytes", - )) - })?; - let nonce = AddressNonce::from_be_bytes(nonce_bytes); - - if balance_i64 < 0 { - return Err(Error::Proof(ProofError::CorruptedProof( - "balance cannot be negative".to_string(), - ))); - } - - Ok((nonce, balance_i64 as Credits)) - }) - .transpose()?; - - // Mirror ShieldedWithdrawal's singleton-subtree invariant: the - // address sub-query targets exactly one key, so a second entry under - // the clear-address pool is a malformed proof, not last-write-wins. - if balances.contains_key(&address) { - return Err(Error::Proof(ProofError::CorruptedProof( - "unshield proof contained more than one output-address entry" - .to_string(), - ))); - } - balances.insert(address, balance_info); - } else { - return Err(Error::Proof(ProofError::CorruptedProof( - "unshield proof contained an entry outside the nullifier and address subtrees".to_string(), - ))); - } - } - - // Every nullifier must be present and marked spent. - for (nf, is_spent) in &statuses { - if !is_spent { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "nullifier {} was not found as spent in the unshield proof", - hex::encode(nf) - )))); - } - } - - Ok(( - root_hash, - VerifiedShieldedNullifiersWithAddressInfos(statuses, balances), - )) - } - StateTransition::ShieldedTransfer(st) => { - use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiers; - use dpp::state_transition::shielded_transfer_transition::accessors::ShieldedTransferTransitionAccessorsV0; - - let nullifier_keys: Vec> = st.nullifiers(); - - let (root_hash, statuses) = Drive::verify_shielded_nullifiers( - proof, - &nullifier_keys, - false, - platform_version, - )?; - - // All nullifiers must be marked as spent - for (nf, is_spent) in &statuses { - if !is_spent { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "nullifier {} was not found as spent in the proof", - hex::encode(nf) - )))); - } - } - - Ok((root_hash, VerifiedShieldedNullifiers(statuses))) - } - StateTransition::ShieldedWithdrawal(st) => { - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::data_contracts::withdrawals_contract; - use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; - use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; - use dpp::document::Document; - use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiersWithWithdrawalDocument; - use dpp::state_transition::shielded_withdrawal_transition::accessors::ShieldedWithdrawalTransitionAccessorsV0; - use grovedb::Element; - - let nullifier_keys: Vec> = st.nullifiers(); - - // Compute withdrawal document ID deterministically (same as prove side). - let first_nullifier = nullifier_keys.first().ok_or_else(|| { - Error::Proof(ProofError::InvalidTransition( - "shielded withdrawal has no nullifiers".to_string(), - )) - })?; - let mut entropy = Vec::new(); - entropy.extend_from_slice(first_nullifier); - entropy.extend_from_slice(st.output_script().as_bytes()); - let document_id = Document::generate_document_id_v0( - &withdrawals_contract::ID, - &withdrawals_contract::OWNER_ID, - withdrawal::NAME, - &entropy, - ); - - let contract = - known_contracts_provider_fn(&withdrawals_contract::ID)?.ok_or_else(|| { - Error::Proof(ProofError::UnknownContract( - "withdrawals contract not available for shielded withdrawal verification" - .to_string(), - )) - })?; - let document_type = - contract - .document_type_for_name(withdrawal::NAME) - .map_err(|e| { - Error::Proof(ProofError::UnknownContract(format!( - "cannot fetch withdrawal document type: {}", - e - ))) - })?; - - let doc_query = SingleDocumentDriveQuery { - contract_id: withdrawals_contract::ID.to_buffer(), - document_type_name: withdrawal::NAME.to_string(), - document_type_keeps_history: false, - document_id: document_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - }; - - // Reconstruct the prove side's merged query — nullifier spend-status ∪ - // withdrawal document — and verify it strictly. See - // `verify_merged_query_strict` for why a single strict merged verify is - // sound and rejects proofs padded with extra subtree branches. - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys); - let nullifier_pq = grovedb::PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let doc_pq = doc_query.construct_path_query(platform_version)?; - let document_path = doc_pq.path.clone(); - - let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( - proof, - vec![nullifier_pq, doc_pq], - platform_version, - )?; - - // Partition the proved key/values by path: entries under the nullifiers tree are - // spend statuses; the single entry under the withdrawal-document tree is the - // proven document. - let nullifiers_path = shielded_credit_pool_nullifiers_path_vec(); - - let mut statuses: Vec<(Vec, bool)> = Vec::new(); - let mut document_element: Option> = None; - - for (path, key, element) in proved_key_values { - if path == nullifiers_path { - statuses.push((key, element.is_some())); - } else if path == document_path { - if document_element.is_some() { - return Err(Error::Proof(ProofError::CorruptedProof( - "shielded withdrawal proof contained more than one withdrawal document".to_string(), - ))); - } - document_element = Some(element); - } else { - return Err(Error::Proof(ProofError::CorruptedProof( - "shielded withdrawal proof contained an entry outside the nullifier and document subtrees".to_string(), - ))); - } - } - - // Every nullifier must be present and marked spent. - for (nf, is_spent) in &statuses { - if !is_spent { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "nullifier {} was not found as spent in the shielded withdrawal proof", - hex::encode(nf) - )))); - } - } - - let document_element = document_element.ok_or_else(|| { - Error::Proof(ProofError::CorruptedProof( - "shielded withdrawal document key absent from proof".to_string(), - )) - })?; - - let doc = match document_element { - Some(Element::Item(serialized, _)) => Document::from_bytes( - serialized.as_slice(), - document_type, - platform_version, - )?, - Some(_) => { - return Err(Error::Proof(ProofError::CorruptedProof( - "expected an item element for withdrawal document".to_string(), - ))); - } - None => { - return Err(Error::Proof(ProofError::CorruptedProof( - "shielded withdrawal was executed but withdrawal document is missing from proof".to_string(), - ))); - } - }; - let documents = BTreeMap::from([(document_id, Some(doc))]); - - Ok(( - root_hash, - VerifiedShieldedNullifiersWithWithdrawalDocument(statuses, documents), - )) - } - StateTransition::ShieldFromAssetLock(st) => { - use crate::drive::RootTree; - use dpp::asset_lock::reduced_asset_lock_value::AssetLockValue; - use dpp::asset_lock::StoredAssetLockInfo; - use dpp::identity::state_transition::AssetLockProved; - use dpp::serialization::PlatformDeserializableUntrusted; - use dpp::state_transition::proof_result::StateTransitionProofResult::{ - VerifiedAssetLockConsumed, VerifiedAssetLockConsumedWithAddressInfos, - }; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; - use grovedb::Element; - - let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { - Error::Proof(ProofError::InvalidTransition( - "shield from asset lock has no outpoint".to_string(), - )) - })?; - let outpoint_bytes: [u8; 36] = outpoint.into(); - - // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. - let ShieldFromAssetLockTransition::V0(v0) = st; - let surplus_output = &v0.surplus_output; - - // Build the outpoint sub-query exactly as the prove side does (same path, same key). - let mut outpoint_query = grovedb::Query::new(); - outpoint_query.insert_key(outpoint_bytes.to_vec()); - let outpoint_pq = grovedb::PathQuery::new( - vec![vec![RootTree::SpentAssetLockTransactions as u8]], - grovedb::SizedQuery::new(outpoint_query, Some(1), None), - ); - - match surplus_output { - Some(surplus_address) => { - // Reconstruct the prove side's merged query — asset-lock outpoint ∪ - // surplus-address balance — and verify it strictly. See - // `verify_merged_query_strict` for why a single strict merged verify - // is sound and rejects proofs padded with extra subtree branches. - let address_pq = Drive::balances_for_clear_addresses_query( - std::iter::once(surplus_address), - ); - - let (root_hash, proved_key_values) = Self::verify_merged_query_strict_v1( - proof, - vec![outpoint_pq, address_pq], - platform_version, - )?; - - // Partition the proved key/values: exactly one entry is the asset-lock - // outpoint (36-byte key), every other entry is a surplus-address balance - // (21-byte key). Anything else is a corrupted proof. - let outpoint_key = outpoint_bytes.to_vec(); - let mut outpoint_element: Option> = None; - let mut balances: BTreeMap< - PlatformAddress, - Option<(AddressNonce, Credits)>, - > = BTreeMap::new(); - - for (_path, key, element) in proved_key_values { - if key == outpoint_key { - outpoint_element = Some(element); - continue; - } - - // Mirror `verify_addresses_infos_v0`: reconstruct the address from the - // key and decode the `ItemWithSumItem` (nonce, balance) element. - let address = PlatformAddress::from_bytes(&key).map_err(|e| { - Error::Proof(ProofError::CorruptedProof(format!( - "failed to deserialize surplus PlatformAddress: {}", - e - ))) - })?; - - let balance_info = element - .map(|element| { - let Element::ItemWithSumItem(nonce_vec, balance_i64, _) = - element - else { - return Err(Error::Proof(ProofError::CorruptedProof( - "expected an item with sum item element".to_string(), - ))); - }; - - let nonce_bytes: [u8; 4] = - nonce_vec.try_into().map_err(|_| { - Error::Proof(ProofError::IncorrectValueSize( - "nonce should be 4 bytes", - )) - })?; - let nonce = AddressNonce::from_be_bytes(nonce_bytes); - - if balance_i64 < 0 { - return Err(Error::Proof(ProofError::CorruptedProof( - "balance cannot be negative".to_string(), - ))); - } - let balance = balance_i64 as Credits; - - Ok((nonce, balance)) - }) - .transpose()?; - - balances.insert(address, balance_info); - } - - // The asset-lock outpoint MUST be present in the merged proof. - let outpoint_element = outpoint_element.ok_or_else(|| { - Error::Proof(ProofError::CorruptedProof( - "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), - )) - })?; - - let info = match outpoint_element { - Some(Element::Item(bytes, _)) => { - if bytes.is_empty() { - StoredAssetLockInfo::FullyConsumed - } else { - StoredAssetLockInfo::PartiallyConsumed( - AssetLockValue::deserialize_from_bytes_untrusted(&bytes)?, - ) - } - } - Some(_) => { - return Err(Error::Proof(ProofError::CorruptedProof( - "expected an item element for asset lock outpoint".to_string(), - ))); - } - None => { - return Err(Error::Proof(ProofError::CorruptedProof( - "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), - ))); - } - }; - - Ok(( - root_hash, - VerifiedAssetLockConsumedWithAddressInfos(info, balances), - )) - } - None => { - // No surplus output: the proof covers only the outpoint, verified strictly - // with the `Some(1)` limit exactly as before (unchanged behavior). - let (root_hash, mut proved_key_values) = - grovedb::GroveDb::verify_query_with_absence_proof( - proof, - &outpoint_pq, - &platform_version.drive.grove_version, - )?; - - if proved_key_values.len() > 1 { - return Err(Error::Proof(ProofError::TooManyElements( - "expected at most 1 element for asset lock outpoint", - ))); - } - - let info = if let Some(proved) = proved_key_values.pop() { - match proved.2 { - Some(Element::Item(bytes, _)) => { - if bytes.is_empty() { - StoredAssetLockInfo::FullyConsumed - } else { - StoredAssetLockInfo::PartiallyConsumed( - AssetLockValue::deserialize_from_bytes_untrusted( - &bytes, - )?, - ) - } - } - Some(_) => { - return Err(Error::Proof(ProofError::CorruptedProof( - "expected an item element for asset lock outpoint" - .to_string(), - ))); - } - None => { - return Err(Error::Proof(ProofError::CorruptedProof( - "shield from asset lock was executed but asset lock outpoint is absent from proof".to_string(), - ))); - } - } - } else { - return Err(Error::Proof(ProofError::CorruptedProof( - "shield from asset lock was executed but no proved key values returned" - .to_string(), - ))); - }; - - Ok((root_hash, VerifiedAssetLockConsumed(info))) - } - } - } - StateTransition::IdentityCreateFromShieldedPool(st) => { - use crate::drive::balances::balance_path; - use crate::drive::identity::IdentityRootStructure::IdentityTreeRevision; - use crate::drive::identity::{identity_key_tree_path, identity_path}; - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{IdentityPublicKey, IdentityV0, KeyID}; - use dpp::prelude::Revision; - use dpp::serialization::PlatformDeserializableUntrusted; - use dpp::state_transition::identity_create_from_shielded_pool_transition::accessors::IdentityCreateFromShieldedPoolTransitionAccessorsV0; - use dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions; - use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers; - use std::collections::{BTreeMap, BTreeSet}; - - // Recompute the id from the actions (the canonical value) instead of trusting the - // wire field, and reject a tampered transition whose wire id doesn't match: so a - // client verifying a proof cannot be fed a transition that reuses these nullifiers - // while pointing `identity_id` at a different identity. (Consensus enforces the same - // equality in `validate_structure`; this independently re-checks it here so the - // SDK proof path is sound even on a hand-constructed transition object.) - let derived_id = derive_identity_id_from_actions(st.actions()); - if st.identity_id() != derived_id { - return Err(Error::Proof(ProofError::IncorrectProof( - "identity create from shielded pool: identity_id does not match the value derived from the spend nullifiers".to_string(), - ))); - } - let identity_id = derived_id.to_buffer(); - let nullifier_keys: Vec> = st.nullifiers(); - - // Rebuild the BYTE-IDENTICAL merged query the prove side built: the nullifier - // sub-query over the nullifier tree + the full-identity sub-query, each with its - // limit cleared (PathQuery::merge rejects limited sub-queries). - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys.clone()); - let nullifier_pq = grovedb::PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let mut identity_pq = Drive::full_identity_query( - &identity_id, - &platform_version.drive.grove_version, - )?; - identity_pq.query.limit = None; - - let merged_pq = grovedb::PathQuery::merge( - vec![&nullifier_pq, &identity_pq], - &platform_version.drive.grove_version, - )?; - - // STRICT verification via `verify_query` (succinctness on). Unlike the other - // shielded merged queries (which target only explicit keys and go through - // `verify_merged_query_strict`), this one embeds `full_identity_query`, whose - // all-keys sub-query is an unbounded RangeFull: and - // `verify_query_with_absence_proof` enumerates the query's terminal keys, which - // is impossible for unbounded ranges ("terminal keys are not supported with - // unbounded ranges"). Absence synthesis isn't needed here anyway: every queried - // element (the spent nullifiers and the created identity) must be PRESENT, so - // presence is checked directly against the result set below. The succinctness - // check still rejects proofs padded with branches beyond {nullifiers, identity} - // (the strict-from-day-one guarantee of #3812), and the limit stays None exactly - // as the prove side built it, so no layer's result loop can break early. - let (root_hash, proved_key_values) = grovedb::GroveDb::verify_query( - proof, - &merged_pq, - &platform_version.drive.grove_version, - )?; - - // Partition the proved key/values by PATH (NOT key length: nullifier keys and the - // identity id are both 32 bytes): nullifier-tree entries vs the identity subtrees - // (balance / revision / keys). Reconstruct the identity exactly as - // `verify_full_identity_by_identity_id_v0` does. - let nullifier_path = shielded_credit_pool_nullifiers_path_vec(); - let balance_path = balance_path(); - let identity_path = identity_path(identity_id.as_slice()); - let identity_keys_path = identity_key_tree_path(identity_id.as_slice()); - - let mut spent_nullifiers = BTreeSet::>::new(); - let mut balance: Option = None; - let mut revision: Option = None; - let mut keys = BTreeMap::::new(); - - for (path, key, maybe_element) in proved_key_values { - if path == nullifier_path { - if !nullifier_keys.contains(&key) { - return Err(Error::Proof(ProofError::CorruptedProof( - "identity create from shielded pool proof contains a nullifier \ - entry that was not requested" - .to_string(), - ))); - } - if maybe_element.is_some() { - spent_nullifiers.insert(key); - } - } else if path == balance_path && key == identity_id { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::IncompleteProof( - "balance wasn't provided for the created identity", - )) - })?; - let signed_balance = element.as_sum_item_value().map_err(Error::from)?; - if signed_balance < 0 { - return Err(Error::Proof(ProofError::Overflow( - "balance can't be negative", - ))); - } - balance = Some(signed_balance as Credits); - } else if path == identity_path && key == vec![IdentityTreeRevision as u8] { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::IncompleteProof( - "revision wasn't provided for the created identity", - )) - })?; - let item_bytes = element.into_item_bytes().map_err(Error::from)?; - revision = Some(Revision::from_be_bytes(item_bytes.try_into().map_err( - |_| { - Error::Proof(ProofError::IncorrectValueSize( - "revision should be 8 bytes", - )) - }, - )?)); - } else if path == identity_keys_path { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::CorruptedProof( - "received an absence proof for a key but didn't request one" - .to_string(), - )) - })?; - let item_bytes = element.into_item_bytes().map_err(Error::from)?; - let public_key = - IdentityPublicKey::deserialize_from_bytes_untrusted(&item_bytes)?; - keys.insert(public_key.id(), public_key); - } else { - return Err(Error::Proof(ProofError::TooManyElements( - "identity create from shielded pool proof contains an element outside \ - the nullifier tree and the created identity", - ))); - } - } - - // Without absence synthesis an unspent nullifier yields no result entry (or a - // bare absence entry), so each expected nullifier's spend status is its - // membership in the proved-present set. - let statuses: Vec<(Vec, bool)> = nullifier_keys - .iter() - .map(|nf| (nf.clone(), spent_nullifiers.contains(nf))) - .collect(); - - // Every funding nullifier must be present (spent) in the post-execution state. - for (nf, is_spent) in &statuses { - if !is_spent { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "nullifier {} was not found as spent in the identity-create-from-shielded-pool proof", - hex::encode(nf) - )))); - } - } - - // The created identity MUST be fully present. - let (balance, revision) = match (balance, revision, keys.is_empty()) { - (Some(balance), Some(revision), false) => (balance, revision), - _ => { - return Err(Error::Proof(ProofError::IncompleteProof( - "identity create from shielded pool was executed but the created identity is absent or incomplete in the proof", - ))) - } - }; - - // Bind the proof to the transition's declared key set: the proven identity must hold - // EXACTLY the keys the transition created (the same conversion the action transformer - // used to build the identity). This stops a tampered transition from swapping in a - // different key set while reusing a valid {nullifiers, identity} proof. - // - // The balance is deliberately NOT checked against `denomination`: the identity holds - // `denomination - total_fee`, and `total_fee` is metered at execution and not - // recoverable here, so a balance/denomination equality check would reject every - // honest proof. (`denomination` is bound into the Orchard `extra_sighash_data` at - // consensus, which is where that binding is enforced.) - let expected_keys: BTreeMap = st - .public_keys() - .iter() - .map(|key| { - let public_key: IdentityPublicKey = key.into(); - (public_key.id(), public_key) - }) - .collect(); - if keys != expected_keys { - return Err(Error::Proof(ProofError::IncorrectProof( - "identity create from shielded pool: the proven identity's keys do not match the transition's declared public keys".to_string(), - ))); - } - - let identity: dpp::prelude::Identity = IdentityV0 { - id: Identifier::from(identity_id), - public_keys: keys, - balance, - revision, - } - .into(); - - Ok(( - root_hash, - VerifiedIdentityWithShieldedNullifiers(identity, statuses), - )) - } - StateTransition::IdentityTopUpFromShieldedPool(st) => { - use crate::drive::balances::balance_path; - use crate::drive::identity::IdentityRootStructure::IdentityTreeRevision; - use crate::drive::identity::{identity_key_tree_path, identity_path}; - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; - use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; - use dpp::identity::{IdentityPublicKey, IdentityV0, KeyID}; - use dpp::prelude::Revision; - use dpp::serialization::PlatformDeserializableUntrusted; - use dpp::state_transition::identity_top_up_from_shielded_pool_transition::accessors::IdentityTopUpFromShieldedPoolTransitionAccessorsV0; - use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedIdentityWithShieldedNullifiers; - use std::collections::{BTreeMap, BTreeSet}; - - // The credited identity is the transition's declared `identity_id`; consensus - // binds it into the Orchard sighash, so a proof for these nullifiers can only - // have been produced for a transition crediting this identity. - let identity_id = st.identity_id().to_buffer(); - let nullifier_keys: Vec> = st.nullifiers(); - - // Rebuild the BYTE-IDENTICAL merged query the prove side built: the nullifier - // sub-query over the nullifier tree + the full-identity sub-query, each with its - // limit cleared (PathQuery::merge rejects limited sub-queries). - let mut nf_query = grovedb::Query::new(); - nf_query.insert_keys(nullifier_keys.clone()); - let nullifier_pq = grovedb::PathQuery::new( - shielded_credit_pool_nullifiers_path_vec(), - grovedb::SizedQuery::new(nf_query, None, None), - ); - - let mut identity_pq = Drive::full_identity_query( - &identity_id, - &platform_version.drive.grove_version, - )?; - identity_pq.query.limit = None; - - let merged_pq = grovedb::PathQuery::merge( - vec![&nullifier_pq, &identity_pq], - &platform_version.drive.grove_version, - )?; - - // STRICT verification via `verify_query` (succinctness on). Unlike the other - // shielded merged queries (which target only explicit keys and go through - // `verify_merged_query_strict`), this one embeds `full_identity_query`, whose - // all-keys sub-query is an unbounded RangeFull: and - // `verify_query_with_absence_proof` enumerates the query's terminal keys, which - // is impossible for unbounded ranges ("terminal keys are not supported with - // unbounded ranges"). Absence synthesis isn't needed here anyway: every queried - // element (the spent nullifiers and the credited identity) must be PRESENT, so - // presence is checked directly against the result set below. The succinctness - // check still rejects proofs padded with branches beyond {nullifiers, identity} - // (the strict-from-day-one guarantee of #3812), and the limit stays None exactly - // as the prove side built it, so no layer's result loop can break early. - let (root_hash, proved_key_values) = grovedb::GroveDb::verify_query( - proof, - &merged_pq, - &platform_version.drive.grove_version, - )?; - - // Partition the proved key/values by PATH (NOT key length: nullifier keys and the - // identity id are both 32 bytes): nullifier-tree entries vs the identity subtrees - // (balance / revision / keys). Reconstruct the identity exactly as - // `verify_full_identity_by_identity_id_v0` does. - let nullifier_path = shielded_credit_pool_nullifiers_path_vec(); - let balance_path = balance_path(); - let identity_path = identity_path(identity_id.as_slice()); - let identity_keys_path = identity_key_tree_path(identity_id.as_slice()); - - let mut spent_nullifiers = BTreeSet::>::new(); - let mut balance: Option = None; - let mut revision: Option = None; - let mut keys = BTreeMap::::new(); - - for (path, key, maybe_element) in proved_key_values { - if path == nullifier_path { - if !nullifier_keys.contains(&key) { - return Err(Error::Proof(ProofError::CorruptedProof( - "identity top up from shielded pool proof contains a nullifier \ - entry that was not requested" - .to_string(), - ))); - } - if maybe_element.is_some() { - spent_nullifiers.insert(key); - } - } else if path == balance_path && key == identity_id { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::IncompleteProof( - "balance wasn't provided for the topped-up identity", - )) - })?; - let signed_balance = element.as_sum_item_value().map_err(Error::from)?; - if signed_balance < 0 { - return Err(Error::Proof(ProofError::Overflow( - "balance can't be negative", - ))); - } - balance = Some(signed_balance as Credits); - } else if path == identity_path && key == vec![IdentityTreeRevision as u8] { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::IncompleteProof( - "revision wasn't provided for the topped-up identity", - )) - })?; - let item_bytes = element.into_item_bytes().map_err(Error::from)?; - revision = Some(Revision::from_be_bytes(item_bytes.try_into().map_err( - |_| { - Error::Proof(ProofError::IncorrectValueSize( - "revision should be 8 bytes", - )) - }, - )?)); - } else if path == identity_keys_path { - let element = maybe_element.ok_or_else(|| { - Error::Proof(ProofError::CorruptedProof( - "received an absence proof for a key but didn't request one" - .to_string(), - )) - })?; - let item_bytes = element.into_item_bytes().map_err(Error::from)?; - let public_key = - IdentityPublicKey::deserialize_from_bytes_untrusted(&item_bytes)?; - keys.insert(public_key.id(), public_key); - } else { - return Err(Error::Proof(ProofError::TooManyElements( - "identity top up from shielded pool proof contains an element outside \ - the nullifier tree and the topped-up identity", - ))); - } - } - - // Without absence synthesis an unspent nullifier yields no result entry (or a - // bare absence entry), so each expected nullifier's spend status is its - // membership in the proved-present set. - let statuses: Vec<(Vec, bool)> = nullifier_keys - .iter() - .map(|nf| (nf.clone(), spent_nullifiers.contains(nf))) - .collect(); - - // Every funding nullifier must be present (spent) in the post-execution state. - for (nf, is_spent) in &statuses { - if !is_spent { - return Err(Error::Proof(ProofError::IncorrectProof(format!( - "nullifier {} was not found as spent in the identity-top-up-from-shielded-pool proof", - hex::encode(nf) - )))); - } - } - - // The credited identity MUST be fully present (it existed before the top-up). - let (balance, revision) = match (balance, revision, keys.is_empty()) { - (Some(balance), Some(revision), false) => (balance, revision), - _ => { - return Err(Error::Proof(ProofError::IncompleteProof( - "identity top up from shielded pool was executed but the topped-up identity is absent or incomplete in the proof", - ))) - } - }; - - // The balance is deliberately NOT checked against `top_up_amount`: the proof is a - // post-execution snapshot of an identity that already held credits, so the - // pre-top-up balance and the flat fee are not recoverable here. (`top_up_amount` - // is bound into the Orchard `extra_sighash_data` at consensus.) - let identity: dpp::prelude::Identity = IdentityV0 { - id: Identifier::from(identity_id), - public_keys: keys, - balance, - revision, - } - .into(); - - Ok(( - root_hash, - VerifiedIdentityWithShieldedNullifiers(identity, statuses), - )) - } - StateTransition::ShieldFromIdentity(st) => { - use dpp::state_transition::shield_from_identity_transition::accessors::ShieldFromIdentityTransitionAccessorsV0; - // snapshot of the identity's balance at the proof's block - let identity_id = st.identity_id(); - let (root_hash, balance) = Drive::verify_identity_balance_for_identity_id( - proof, - identity_id.into_buffer(), - false, - platform_version, - )?; - let balance = balance.ok_or(Error::Proof(ProofError::IncorrectProof(format!( - "proof did not contain balance for identity {} expected to exist because of state transition (shield from identity)", - identity_id - ))))?; - Ok(( - root_hash, - VerifiedPartialIdentity(PartialIdentity { - id: identity_id, - loaded_public_keys: Default::default(), - balance: Some(balance), - revision: None, - not_found_public_keys: Default::default(), - }), - )) - } - }?; - - let outcome = if Self::state_transition_proof_binds_execution_v1( - state_transition, - known_contracts_provider_fn, - )? { - StateTransitionProofOutcome::ExecutionProved(result) - } else { - StateTransitionProofOutcome::AffectedState(result) - }; - - Ok((root_hash, outcome)) - } - - /// Whether a valid proof for this state transition binds the execution of - /// this specific transition (`ExecutionProved`) or can only authenticate - /// the state the transition affects at the committed block - /// (`AffectedState`). - /// - /// This classifier is deliberately an exhaustive `match` over every - /// transition family — and, for tokens, over the contract's keeps-history - /// configuration and group usage — so adding a new variant fails to - /// compile until it is classified here: no transition can silently - /// inherit the stronger `ExecutionProved` guarantee. - fn state_transition_proof_binds_execution_v1( - state_transition: &StateTransition, - known_contracts_provider_fn: &ContractLookupFn, - ) -> Result { - let binds = match state_transition { - // The created contract's id derives from its owner and entropy, - // and the proven body must equal the transition's declared - // contract, which cannot exist without this create executing. - StateTransition::DataContractCreate(_) => true, - // The proven contract body equaling the update's target does not - // prove that THIS update executed: the proven version may predate - // the request, whose signature and nonce are not in the proof. - StateTransition::DataContractUpdate(_) => false, - StateTransition::Batch(batch) => match batch.first_transition_v1() { - // The verifier errors on an empty batch before the tag is - // consulted; classify fail-closed regardless. - None => false, - // Document proofs bind the exact document (or its absence - // after deletion), including contested status and history — - // EXCEPT indexOnly types: their snapshot authenticates the - // commitment-checked entry (or its absence), which carries - // neither id, entropy nor nonce and so cannot bind one - // specific transition's execution. - Some(BatchedTransitionRefV1::Document(document_transition)) => { - use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; - let data_contract_id = document_transition.data_contract_id(); - let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( - Error::Proof(ProofError::UnknownContract(format!( - "unknown contract with id {} in document verification", - data_contract_id - ))), - )?; - // EXCEPT an erase: the document it acts on was already - // deleted, so the by-id proof shows the same absence - // whether or not this erase ran. - if matches!(document_transition, DocumentTransitionRefV1::Erase(_)) { - false - } else { - !contract - .document_type_for_name(document_transition.document_type_name()) - .map_err(|e| Error::Proof(ProofError::UnknownContract(e.to_string())))? - .index_only() - } - } - Some(BatchedTransitionRefV1::Token(token_transition)) => { - let data_contract_id = token_transition.data_contract_id(); - let contract = known_contracts_provider_fn(&data_contract_id)?.ok_or( - Error::Proof(ProofError::UnknownContract(format!( - "unknown contract with id {} in token verification", - data_contract_id - ))), - )?; - let token_config = contract.expected_token_configuration( - token_transition.base().token_contract_position(), - )?; - let keeps = token_config.keeps_history(); - // A group action's proof binds the action id and the - // signer's recorded participation; a history-keeping - // token's proof binds the exact historical document. - // Without either, only the resulting state is proven. - let grouped = token_transition.base().using_group_info().is_some(); - match token_transition { - TokenTransition::Burn(_) => keeps.keeps_burning_history() || grouped, - TokenTransition::Mint(_) => keeps.keeps_minting_history() || grouped, - TokenTransition::Transfer(_) => keeps.keeps_transfer_history(), - TokenTransition::Freeze(_) | TokenTransition::Unfreeze(_) => { - keeps.keeps_freezing_history() || grouped - } - TokenTransition::DirectPurchase(_) => keeps.keeps_direct_purchase_history(), - TokenTransition::SetPriceForDirectPurchase(_) => { - keeps.keeps_direct_pricing_history() || grouped - } - TokenTransition::DestroyFrozenFunds(_) - | TokenTransition::EmergencyAction(_) - | TokenTransition::ConfigUpdate(_) - | TokenTransition::Claim(_) => true, - } - } - }, - // The proven identity holds exactly the transition's declared - // key set, which cannot exist without this create executing. - StateTransition::IdentityCreate(_) => true, - // Balance/revision snapshots: values as of the proof's block, - // not bindable to one specific top-up/withdrawal/transfer. - StateTransition::IdentityTopUp(_) => false, - StateTransition::IdentityCreditWithdrawal(_) => false, - StateTransition::IdentityCreditTransfer(_) => false, - // Binds the transition's revision and its exact key additions - // and disabling timestamps. - StateTransition::IdentityUpdate(_) => true, - // The proven vote is stored under the masternode's identity and - // must equal the transition's declared vote. - StateTransition::MasternodeVote(_) => true, - // Current identity and address balances, without the requested - // amounts, nonces, or the submitted request itself. - StateTransition::IdentityCreditTransferToAddresses(_) => false, - StateTransition::IdentityTopUpFromAddresses(_) => false, - // Binds the created identity's declared public keys. - StateTransition::IdentityCreateFromAddresses(_) => true, - // Address funds families: post-state address balances only. - StateTransition::AddressFundsTransfer(_) => false, - StateTransition::AddressFundingFromAssetLock(_) => false, - StateTransition::AddressCreditWithdrawal(_) => false, - StateTransition::Shield(_) => false, - // Nullifier-spend proofs bind the exact Orchard actions of this - // transition. - StateTransition::Unshield(_) => true, - StateTransition::ShieldedTransfer(_) => true, - StateTransition::ShieldedWithdrawal(_) => true, - // Only the consumed outpoint (and current surplus-address state) - // is proven; competing shields over the same outpoint share the - // same query and result. - StateTransition::ShieldFromAssetLock(_) => false, - // Spent nullifiers plus the resulting identity do not bind the - // complete Orchard request, denomination context, or fallback - // address. - StateTransition::IdentityCreateFromShieldedPool(_) => false, - // Only the identity's post-debit balance is proven; the shielded note - // and the requested amount are not bound. - StateTransition::ShieldFromIdentity(_) => false, - // A spent nullifier is stored as an empty item shared by every - // spend family, so its presence cannot tell this top-up apart from - // a competing spend of the same notes that credits another - // identity, and the credited identity's balance is a snapshot at - // the proof's block. - StateTransition::IdentityTopUpFromShieldedPool(_) => false, - }; - - Ok(binds) - } - - /// Reconstruct the prove side's merged multi-root query and verify it STRICTLY. - /// - /// The shielded prove paths merge several sub-queries into a SINGLE multi-root - /// proof. [`grovedb::PathQuery::merge`] rejects sub-queries that still carry a - /// limit, so every sub-query's limit is cleared here before the merge. The strict - /// [`grovedb::GroveDb::verify_query_with_absence_proof`] in turn *requires* a - /// limit, but it must never be exhausted by the legitimate result set: the - /// per-layer succinctness check that rejects extra proof branches runs AFTER that - /// layer's result loop, and the result loop only breaks early once the limit hits - /// 0 — so a limit smaller than the real result count could break before a layer's - /// succinctness check runs and falsely reject an honest proof. Every shielded - /// merged query targets a fixed, tiny set of explicit keys ({nullifiers} plus one - /// address/document/outpoint), so an unreachable `u16::MAX` limit is sound: it - /// guarantees every layer is fully traversed while the (limit-independent) - /// succinctness check still rejects any proof padded with extra subtree branches. - /// - /// Returns the proof root hash and every proved `(path, key, element)` trio, left - /// for the caller to partition against the sub-query paths. - #[allow(clippy::type_complexity)] - fn verify_merged_query_strict_v1( - proof: &[u8], - mut sub_queries: Vec, - platform_version: &PlatformVersion, - ) -> Result< - ( - RootHash, - Vec, - ), - Error, - > { - for sub_query in &mut sub_queries { - sub_query.query.limit = None; - } - - let mut merged_pq = grovedb::PathQuery::merge( - sub_queries.iter().collect(), - &platform_version.drive.grove_version, - )?; - merged_pq.query.limit = Some(u16::MAX); - - Ok(grovedb::GroveDb::verify_query_with_absence_proof( - proof, - &merged_pq, - &platform_version.drive.grove_version, - )?) - } -} - -#[cfg(feature = "server")] -#[cfg(test)] -mod tests { - use super::*; - use crate::drive::Drive; - use crate::query::ContractLookupFn; - use crate::util::object_size_info::DocumentInfo::DocumentRefInfo; - use crate::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; - use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; - use dpp::block::block_info::BlockInfo; - use dpp::data_contract::accessors::v0::DataContractV0Getters; - use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; - use dpp::data_contract::document_type::random_document::CreateRandomDocument; - use dpp::data_contract::serialized_version::DataContractInSerializationFormat; - use dpp::document::DocumentV0Getters; - use dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0}; - use dpp::identity::Identity; - use dpp::prelude::DataContract; - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; - use dpp::state_transition::proof_result::{ - StateTransitionProofOutcome, StateTransitionProofResult, - }; - use dpp::state_transition::StateTransition; - use dpp::tests::fixtures::get_dpns_data_contract_fixture; - use dpp::version::PlatformVersion; - use platform_version::TryIntoPlatformVersioned; - use std::sync::Arc; - - /// Helper: set up drive, insert a DPNS contract, return (drive, contract) - fn setup_drive_and_contract() -> (Drive, DataContract) { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - let created_contract = - get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); - let contract = created_contract.data_contract_owned(); - drive - .insert_contract( - &contract, - BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to insert contract"); - (drive, contract) - } - - /// Helper: set up drive and add an identity, return (drive, identity) - fn setup_drive_and_identity() -> (Drive, Identity) { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - let identity = Identity::random_identity(3, Some(14), platform_version) - .expect("expected a platform identity"); - drive - .add_new_identity( - identity.clone(), - false, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to add identity"); - (drive, identity) - } - - // ----------------------------------------------------------------------- - // DataContractCreate - // ----------------------------------------------------------------------- - - #[test] - fn verify_data_contract_create_happy_path() { - let (drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - let contract_id = contract.id().to_buffer(); - - // Generate a proof for this contract - let proof = drive - .prove_contract(contract_id, None, platform_version) - .expect("expected to prove contract"); - - // Build the DataContractCreate state transition from the contract - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; - let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( - DataContractCreateTransitionV0 { - data_contract: data_contract_serialized, - identity_nonce: 0, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (root_hash, proof_result) = result.unwrap(); - assert_ne!(root_hash, [0u8; 32], "root hash should not be all zeros"); - match proof_result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedDataContract(verified_contract), - ) => { - assert_eq!( - verified_contract.id(), - contract.id(), - "verified contract id should match" - ); - } - other => panic!("expected VerifiedDataContract, got {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // DataContractUpdate - // ----------------------------------------------------------------------- - - #[test] - fn verify_data_contract_update_happy_path() { - let (drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - let contract_id = contract.id().to_buffer(); - - // For the update transition, we use the same contract (version hasn't changed - // in the fixture, but the proof verifies the contract is as expected). - let proof = drive - .prove_contract(contract_id, None, platform_version) - .expect("expected to prove contract"); - - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; - let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( - DataContractUpdateTransitionV0 { - identity_contract_nonce: 0, - data_contract: data_contract_serialized, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (_root_hash, proof_result) = result.unwrap(); - // The proven contract body equaling the update's target does not - // prove that THIS update executed (the version may predate the - // request), so the outcome is a snapshot, not execution evidence. - match proof_result { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedDataContract(verified_contract), - ) => { - assert_eq!(verified_contract.id(), contract.id()); - } - other => panic!( - "expected AffectedState(VerifiedDataContract), got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // IdentityCreate - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_create_happy_path() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - - let proof = drive - .prove_full_identity(identity_id, None, &platform_version.drive) - .expect("expected to prove full identity"); - - use dpp::state_transition::identity_create_transition::IdentityCreateTransition; - use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; - let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0 { - identity_id: identity.id(), - public_keys: identity.public_keys().values().map(Into::into).collect(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (_root_hash, proof_result) = result.unwrap(); - match proof_result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedIdentity(verified_identity), - ) => { - assert_eq!(verified_identity.id(), identity.id()); - } - other => panic!("expected VerifiedIdentity, got {:?}", other), - } - } - - #[test] - fn verify_identity_create_rejects_unrelated_public_keys() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let proof = drive - .prove_full_identity(identity.id().to_buffer(), None, &platform_version.drive) - .expect("expected to prove full identity"); - - use dpp::state_transition::identity_create_transition::IdentityCreateTransition; - use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; - let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0 { - identity_id: identity.id(), - public_keys: vec![], - ..Default::default() - }, - )); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - &|_id| Ok(None), - platform_version, - ); - - assert!(matches!( - result, - Err(Error::Proof(ProofError::IncorrectProof(_))) - )); - } - - // ----------------------------------------------------------------------- - // IdentityTopUp - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_top_up_returns_affected_state_snapshot() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - - let proof = drive - .prove_identity_balance_and_revision(identity_id, None, &platform_version.drive) - .expect("expected to prove identity balance and revision"); - - use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; - use dpp::state_transition::state_transitions::identity::identity_topup_transition::v0::IdentityTopUpTransitionV0; - let st = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0( - IdentityTopUpTransitionV0 { - identity_id: identity.id(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - // A top-up proof cannot be bound to the transition's execution, so - // the outcome must be tagged as an affected-state snapshot, never as - // execution evidence. - let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ) - .expect("expected verification to succeed"); - - match outcome { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), - ) => { - assert_eq!(partial_identity.id, identity.id()); - assert!(partial_identity.balance.is_some()); - assert!(partial_identity.revision.is_some()); - } - other => panic!( - "expected AffectedState(VerifiedPartialIdentity), got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // IdentityCreditWithdrawal - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_credit_withdrawal_returns_affected_state_snapshot() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - - let proof = drive - .prove_identity_balance(identity_id, None, &platform_version.drive) - .expect("expected to prove identity balance"); - - use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; - use dpp::state_transition::state_transitions::identity::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; - let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( - IdentityCreditWithdrawalTransitionV0 { - identity_id: identity.id(), - amount: 100, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ) - .expect("expected verification to succeed"); - - match outcome { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), - ) => { - assert_eq!(partial_identity.id, identity.id()); - assert!(partial_identity.balance.is_some()); - } - other => panic!( - "expected AffectedState(VerifiedPartialIdentity), got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // IdentityUpdate - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_update_happy_path() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - - // The verify function for IdentityUpdate uses verify_identity_keys_by_identity_id - // with with_revision=true, which creates a merged query of keys + revision. - // We need a proof that contains both. Use the same path queries merged together. - use crate::drive::identity::key::fetch::IdentityKeysRequest; - let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); - let keys_path_query = key_request.into_path_query(); - let revision_path_query = Drive::identity_revision_query(&identity_id); - let merged = grovedb::PathQuery::merge( - vec![&keys_path_query, &revision_path_query], - &platform_version.drive.grove_version, - ) - .expect("expected to merge path queries"); - let proof = drive - .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) - .expect("expected to prove identity keys and revision"); - - use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; - use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; - let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( - IdentityUpdateTransitionV0 { - identity_id: identity.id(), - revision: identity.revision(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (_root_hash, proof_result) = result.unwrap(); - match proof_result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), - ) => { - assert_eq!(partial_identity.id, identity.id()); - assert!( - !partial_identity.loaded_public_keys.is_empty(), - "loaded public keys should not be empty" - ); - } - other => panic!("expected VerifiedPartialIdentity, got {:?}", other), - } - } - - #[test] - fn verify_identity_update_rejects_unrelated_revision() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); - let keys_path_query = key_request.into_path_query(); - let revision_path_query = Drive::identity_revision_query(&identity_id); - let merged = grovedb::PathQuery::merge( - vec![&keys_path_query, &revision_path_query], - &platform_version.drive.grove_version, - ) - .expect("expected to merge path queries"); - let proof = drive - .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) - .expect("expected to prove identity keys and revision"); - - use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; - use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; - let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( - IdentityUpdateTransitionV0 { - identity_id: identity.id(), - revision: identity.revision() + 1, - ..Default::default() - }, - )); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - &|_id| Ok(None), - platform_version, - ); - - assert!(matches!( - result, - Err(Error::Proof(ProofError::IncorrectProof(_))) - )); - } - - #[test] - fn verify_identity_update_rejects_active_key_as_disabled() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); - let keys_path_query = key_request.into_path_query(); - let revision_path_query = Drive::identity_revision_query(&identity_id); - let merged = grovedb::PathQuery::merge( - vec![&keys_path_query, &revision_path_query], - &platform_version.drive.grove_version, - ) - .expect("expected to merge path queries"); - let proof = drive - .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) - .expect("expected to prove identity keys and revision"); - let key_id = *identity - .public_keys() - .keys() - .next() - .expect("identity should have a public key"); - - use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; - use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; - let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( - IdentityUpdateTransitionV0 { - identity_id: identity.id(), - revision: identity.revision(), - disable_public_keys: vec![key_id], - ..Default::default() - }, - )); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - &|_id| Ok(None), - platform_version, - ); - - assert!(matches!( - result, - Err(Error::Proof(ProofError::IncorrectProof(_))) - )); - } - - #[test] - fn verify_identity_update_accepts_key_disabled_before_proof_block() { - let (drive, identity) = setup_drive_and_identity(); - let platform_version = PlatformVersion::latest(); - let identity_id = identity.id().to_buffer(); - let key_id = *identity - .public_keys() - .keys() - .next() - .expect("identity should have a public key"); - - // Disable the key at time 50, then verify against a proof served at a - // later block: `disabled_at` never changes afterwards, so retried or - // later proofs must still verify. - let disabled_at: u64 = 50; - drive - .disable_identity_keys( - identity_id, - vec![key_id], - disabled_at, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to disable identity key"); - - let key_request = IdentityKeysRequest::new_all_keys_query(&identity_id, None); - let keys_path_query = key_request.into_path_query(); - let revision_path_query = Drive::identity_revision_query(&identity_id); - let merged = grovedb::PathQuery::merge( - vec![&keys_path_query, &revision_path_query], - &platform_version.drive.grove_version, - ) - .expect("expected to merge path queries"); - let proof = drive - .grove_get_proved_path_query(&merged, None, &mut vec![], &platform_version.drive) - .expect("expected to prove identity keys and revision"); - - use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; - use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; - let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( - IdentityUpdateTransitionV0 { - identity_id: identity.id(), - revision: identity.revision(), - disable_public_keys: vec![key_id], - ..Default::default() - }, - )); - - let later_block = BlockInfo { - time_ms: disabled_at + 10_000, - ..Default::default() - }; - let (_root_hash, result) = Drive::verify_state_transition_was_executed_with_proof( - &st, - &later_block, - &proof, - &|_id| Ok(None), - platform_version, - ) - .expect("expected verification to accept a key disabled before the proof block"); - - match result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedPartialIdentity(partial_identity), - ) => { - assert_eq!(partial_identity.id, identity.id()); - } - other => panic!( - "expected ExecutionProved(VerifiedPartialIdentity), got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // IdentityCreditTransfer - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_credit_transfer_returns_affected_state_snapshot() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - - let sender = Identity::random_identity(3, Some(14), platform_version) - .expect("expected a sender identity"); - let recipient = Identity::random_identity(3, Some(15), platform_version) - .expect("expected a recipient identity"); - - drive - .add_new_identity( - sender.clone(), - false, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to add sender identity"); - drive - .add_new_identity( - recipient.clone(), - false, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to add recipient identity"); - - let proof = drive - .prove_many_identity_balances( - &[sender.id().to_buffer(), recipient.id().to_buffer()], - None, - &platform_version.drive, - ) - .expect("expected to prove both balances"); - - use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; - use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; - let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( - IdentityCreditTransferTransitionV0 { - identity_id: sender.id(), - recipient_id: recipient.id(), - amount: 50, - nonce: 1, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let (_root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ) - .expect("expected verification to succeed"); - - match outcome { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedBalanceTransfer( - sender_identity, - recipient_identity, - ), - ) => { - assert_eq!(sender_identity.id, sender.id()); - assert_eq!(recipient_identity.id, recipient.id()); - assert!(sender_identity.balance.is_some()); - assert!(recipient_identity.balance.is_some()); - } - other => panic!( - "expected AffectedState(VerifiedBalanceTransfer), got {:?}", - other - ), - } - } - - // ----------------------------------------------------------------------- - // Batch: empty transitions error - // ----------------------------------------------------------------------- - - #[test] - fn verify_batch_empty_transitions_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof, got: {:?}", - result - ); - let err = result.unwrap_err(); - match &err { - Error::Proof(ProofError::InvalidTransition(msg)) => { - assert!( - msg.contains("no transition"), - "expected error about no transition, got: {}", - msg - ); - } - other => panic!("expected InvalidTransition error, got: {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // Batch: too many transitions error - // ----------------------------------------------------------------------- - - #[test] - fn verify_batch_too_many_transitions_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base1 = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - let base2 = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 2, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![ - DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base: base1 }, - )), - DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base: base2 }, - )), - ], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof, got: {:?}", - result - ); - let err = result.unwrap_err(); - match &err { - Error::Proof(ProofError::InvalidTransition(msg)) => { - assert!( - msg.contains("does not support more than one document"), - "expected error about too many transitions, got: {}", - msg - ); - } - other => panic!("expected InvalidTransition error, got: {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // Batch: document delete happy path - // ----------------------------------------------------------------------- - - #[test] - fn verify_batch_document_delete_happy_path() { - let (drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - - let document_type = contract - .document_type_for_name("preorder") - .expect("expected preorder document type"); - - let document = document_type - .random_document(Some(99), platform_version) - .expect("expected a random document"); - let doc_id = document.id(); - - // Insert the document - drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - false, - BlockInfo::default(), - true, - None, - platform_version, - None, - ) - .expect("expected to insert document"); - - // Delete the document - drive - .delete_document_for_contract( - doc_id, - &contract, - "preorder", - BlockInfo::default(), - true, - None, - platform_version, - None, - ) - .expect("expected to delete document"); - - // Generate a proof for the now-absent document - use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; - let single_query = SingleDocumentDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "preorder".to_string(), - document_type_keeps_history: document_type.documents_keep_history(), - document_id: doc_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - }; - let path_query = single_query - .construct_path_query(platform_version) - .expect("expected to build path query"); - let proof = drive - .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) - .expect("expected to get proof"); - - // Build a document delete batch transition - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: doc_id, - identity_contract_nonce: 1, - document_type_name: "preorder".to_string(), - data_contract_id: contract.id(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base }, - ))], - ..Default::default() - })); - - let contract_arc = Arc::new(contract.clone()); - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (_root_hash, proof_result) = result.unwrap(); - match proof_result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedDocuments(docs), - ) => { - assert_eq!(docs.len(), 1, "expected exactly one document entry"); - let (returned_id, maybe_doc) = docs.into_iter().next().unwrap(); - assert_eq!(returned_id, doc_id); - assert!( - maybe_doc.is_none(), - "document should be None after deletion" - ); - } - other => panic!("expected VerifiedDocuments, got {:?}", other), - } - } - - /// An erase acts on a document that was already deleted, so the by-id - /// proof shows the same absence whether or not it ran. The classifier must - /// report that as affected state, while a delete over the very same proof - /// keeps the classification generation 0 gives a delete: a document's - /// absence by id is read as the delete's outcome, which is what the SDK's - /// strict wait relies on. - #[test] - fn verify_batch_document_erase_is_affected_state_not_execution_proved() { - use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; - use dpp::document::DocumentV0Setters; - use dpp::state_transition::batch_transition::batched_transition::document_erase_transition::{ - DocumentEraseTransition, DocumentEraseTransitionV0, - }; - use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransitionV1, DocumentTransitionV1, - }; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::{ - DocumentDeleteTransition, DocumentDeleteTransitionV0, - }; - use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV2}; - use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; - - let platform_version = PlatformVersion::latest(); - let drive = setup_drive_with_initial_state_structure(None); - let contract = json_document_to_contract( - "tests/supporting_files/contract/family/family-contract-with-history.json", - false, - platform_version, - ) - .expect("expected the family history contract"); - drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - None, - None, - platform_version, - ) - .expect("expected to apply the contract"); - let document_type = contract - .document_type_for_name("person") - .expect("expected the person type"); - let mut document = json_document_to_document( - "tests/supporting_files/contract/family/person0.json", - Some([4u8; 32].into()), - document_type, - platform_version, - ) - .expect("expected a person"); - for revision in 1..=2u64 { - document.set_revision(Some(revision)); - drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - revision > 1, - BlockInfo::default_with_time(1_000 + revision), - true, - None, - platform_version, - None, - ) - .expect("expected to write a revision"); - } - let doc_id = document.id(); - drive - .delete_document_for_contract( - doc_id, - &contract, - "person", - BlockInfo::default_with_time(5_000), - true, - None, - platform_version, - None, - ) - .expect("expected the delete to succeed"); - - let path_query = SingleDocumentDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "person".to_string(), - document_type_keeps_history: true, - document_id: doc_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - } - .construct_path_query(platform_version) - .expect("expected a by-id path query"); - let proof = drive - .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) - .expect("expected an absence proof"); - - let base = || { - DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: doc_id, - identity_contract_nonce: 1, - document_type_name: "person".to_string(), - data_contract_id: contract.id(), - }) - }; - let batch = |transition: DocumentTransitionV1| { - StateTransition::Batch(BatchTransition::V2(BatchTransitionV2 { - owner_id: Default::default(), - transitions: vec![BatchedTransitionV1::Document(transition)], - ..Default::default() - })) - }; - let contract_arc = Arc::new(contract.clone()); - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); - let verify = |state_transition: StateTransition| { - Drive::verify_state_transition_was_executed_with_proof( - &state_transition, - &BlockInfo::default(), - &proof, - known_contracts_provider_fn, - platform_version, - ) - .expect("expected verification to succeed") - .1 - }; - - let erase_outcome = verify(batch(DocumentTransitionV1::Erase( - DocumentEraseTransition::V0(DocumentEraseTransitionV0 { base: base() }), - ))); - match erase_outcome { - StateTransitionProofOutcome::AffectedState( - StateTransitionProofResult::VerifiedDocuments(documents), - ) => { - assert_eq!(documents.len(), 1); - let (id, found) = documents.into_iter().next().unwrap(); - assert_eq!(id, doc_id); - assert!(found.is_none(), "the document is absent by id"); - } - other => panic!("an erase must be classified as affected state, got {other:?}"), - } - - let delete_outcome = verify(batch(DocumentTransitionV1::Delete( - DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { base: base() }), - ))); - assert!( - matches!( - delete_outcome, - StateTransitionProofOutcome::ExecutionProved(_) - ), - "a delete over the same proof stays execution-proved, got {delete_outcome:?}" - ); - } - - // ----------------------------------------------------------------------- - // Batch: document create happy path - // ----------------------------------------------------------------------- - - #[test] - fn verify_batch_document_create_happy_path() { - let (drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - - let document_type = contract - .document_type_for_name("preorder") - .expect("expected preorder document type"); - - let document = document_type - .random_document(Some(99), platform_version) - .expect("expected a random document"); - let doc_id = document.id(); - let owner_id = document.owner_id(); - - let block_info = BlockInfo::default(); - - // Insert the document - drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentRefInfo((&document, None)), - owner_id: Some(owner_id.to_buffer()), - }, - contract: &contract, - document_type, - }, - false, - block_info, - true, - None, - platform_version, - None, - ) - .expect("expected to insert document"); - - // Generate a proof for the existing document - use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; - let single_query = SingleDocumentDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "preorder".to_string(), - document_type_keeps_history: document_type.documents_keep_history(), - document_id: doc_id.to_buffer(), - block_time_ms: None, - contested_status: SingleDocumentDriveQueryContestedStatus::NotContested, - }; - let path_query = single_query - .construct_path_query(platform_version) - .expect("expected to build path query"); - let proof = drive - .grove_get_proved_path_query(&path_query, None, &mut vec![], &platform_version.drive) - .expect("expected to get proof"); - - // Build a document create batch transition - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; - use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: doc_id, - identity_contract_nonce: 1, - document_type_name: "preorder".to_string(), - data_contract_id: contract.id(), - }); - - // Build the create transition from document properties - let create_transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { - base, - entropy: [100u8; 32], - data: document.properties().clone(), - prefunded_voting_balance: None, - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id, - transitions: vec![DocumentTransition::Create(create_transition)], - ..Default::default() - })); - - let contract_arc = Arc::new(contract.clone()); - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &block_info, - &proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - result.is_ok(), - "expected verification to succeed, got: {:?}", - result.err() - ); - let (_root_hash, proof_result) = result.unwrap(); - match proof_result { - StateTransitionProofOutcome::ExecutionProved( - StateTransitionProofResult::VerifiedDocuments(docs), - ) => { - assert_eq!(docs.len(), 1, "expected exactly one document entry"); - let (returned_id, maybe_doc) = docs.into_iter().next().unwrap(); - assert_eq!(returned_id, doc_id); - assert!(maybe_doc.is_some(), "document should exist after creation"); - } - other => panic!("expected VerifiedDocuments, got {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // Batch: unknown contract returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_batch_document_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base }, - ))], - ..Default::default() - })); - - // Provider returns None for all contracts - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof, got: {:?}", - result - ); - let err = result.unwrap_err(); - match &err { - Error::Proof(ProofError::UnknownContract(msg)) => { - assert!( - msg.contains("unknown contract"), - "expected error about unknown contract, got: {}", - msg - ); - } - other => panic!("expected UnknownContract error, got: {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // DataContractCreate: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_data_contract_create_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - // Build a minimal contract create transition - let created_contract = - get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); - let contract = created_contract.data_contract(); - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; - let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( - DataContractCreateTransitionV0 { - data_contract: data_contract_serialized, - identity_nonce: 0, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - // Empty proof should cause a GroveDB/proof error - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // IdentityCreate: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_create_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_create_transition::IdentityCreateTransition; - use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; - let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // IdentityTopUp: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_top_up_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; - use dpp::state_transition::state_transitions::identity::identity_topup_transition::v0::IdentityTopUpTransitionV0; - let st = StateTransition::IdentityTopUp(IdentityTopUpTransition::V0( - IdentityTopUpTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // IdentityCreditWithdrawal: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_credit_withdrawal_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; - use dpp::state_transition::state_transitions::identity::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; - let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( - IdentityCreditWithdrawalTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - amount: 100, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // IdentityUpdate: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_update_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_update_transition::IdentityUpdateTransition; - use dpp::state_transition::state_transitions::identity::identity_update_transition::v0::IdentityUpdateTransitionV0; - let st = StateTransition::IdentityUpdate(IdentityUpdateTransition::V0( - IdentityUpdateTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - revision: 1, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // IdentityCreditTransfer: empty proof returns error - // ----------------------------------------------------------------------- - - #[test] - fn verify_identity_credit_transfer_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; - use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; - let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( - IdentityCreditTransferTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - recipient_id: dpp::prelude::Identifier::random(), - amount: 50, - nonce: 1, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // Additional coverage for rarely-executed error branches. - // - // The following tests exercise variants that were not previously - // covered by the inline suite, and in particular focus on error arms - // (unknown-contract, invalid-transition, malformed-input) that are - // otherwise hard to exercise through happy-path tests. - // ----------------------------------------------------------------------- - - // --- DataContractUpdate: empty proof returns Error::Proof or Error::GroveDB. - #[test] - fn verify_data_contract_update_empty_proof_returns_error() { - let (_drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; - let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( - DataContractUpdateTransitionV0 { - identity_contract_nonce: 1, - data_contract: data_contract_serialized, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - // Empty proof → must error (either Error::Proof or Error::GroveDB). - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected Error::Proof or Error::GroveDB for empty proof, got: {:?}", - result - ); - } - - // --- MasternodeVote: unknown contract returns a specific - // `ProofError::UnknownContract` because the provider returns None. - #[test] - fn verify_masternode_vote_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; - use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; - use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; - use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; - use dpp::voting::vote_polls::VotePoll; - use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; - use dpp::voting::votes::resource_vote::ResourceVote; - use dpp::voting::votes::Vote; - - let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( - MasternodeVoteTransitionV0 { - pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), - voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), - vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: VotePoll::ContestedDocumentResourceVotePoll( - ContestedDocumentResourceVotePoll { - contract_id: dpp::prelude::Identifier::from([7u8; 32]), - document_type_name: "some_type".to_string(), - index_name: "idx".to_string(), - index_values: vec![], - }, - ), - resource_vote_choice: ResourceVoteChoice::Abstain, - })), - nonce: 1, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - // Provider always returns None – triggers UnknownContract. - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract error, got Ok"); - match err { - crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { - assert!( - msg.contains("unknown contract") && msg.contains("resource vote"), - "unexpected UnknownContract message: {msg}" - ); - } - other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), - } - } - - // --- ShieldedWithdrawal: empty proof + no nullifiers → error. - // - // The `first_nullifier` guard in the verifier is only reachable once - // `verify_shielded_nullifiers` returns Ok. With an empty proof, grove-db - // proof verification errors out first, so this test cannot reach the - // `InvalidTransition` guard specifically — we only assert that some - // Error::Proof / Error::GroveDB is returned on the failing path. - #[test] - fn verify_shielded_withdrawal_empty_proof_error() { - let platform_version = PlatformVersion::latest(); - use dpp::identity::core_script::CoreScript; - use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; - use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; - use dpp::withdrawal::Pooling; - - let st = StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( - ShieldedWithdrawalTransitionV0 { - actions: vec![], - unshielding_amount: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - core_fee_per_byte: 1, - pooling: Pooling::Never, - output_script: CoreScript::from_bytes(vec![]), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for shielded withdrawal with empty proof, got: {:?}", - result - ); - } - - // --- ShieldedTransfer: empty proof leads to an error (Error::Proof - // or Error::GroveDB) through the verify_shielded_nullifiers path. - #[test] - fn verify_shielded_transfer_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; - use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; - - let st = StateTransition::ShieldedTransfer(ShieldedTransferTransition::V0( - ShieldedTransferTransitionV0 { - actions: vec![], - value_balance: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for shielded transfer with empty proof, got: {:?}", - result - ); - } - - // --- Unshield: empty proof leads to an error. - #[test] - fn verify_unshield_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::unshield_transition::v0::UnshieldTransitionV0; - use dpp::state_transition::unshield_transition::UnshieldTransition; - - let st = StateTransition::Unshield(UnshieldTransition::V0(UnshieldTransitionV0 { - output_address: Default::default(), - actions: vec![], - unshielding_amount: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for unshield with empty proof, got: {:?}", - result - ); - } - - // --- IdentityCreateFromShieldedPool: empty proof returns error. - // - // Exercises the STRICT merged-query verify arm: an empty proof cannot satisfy the strict - // `verify_query` over the merged {nullifier-tree, identity} query, so the verifier must reject - // (rather than silently accepting). The positive prove→verify roundtrip lives in drive-abci's - // identity_create_from_shielded_pool tests (synthetic-action execution). - #[test] - fn verify_identity_create_from_shielded_pool_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::shielded::SerializedAction; - use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions; - use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; - use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; - - let actions = vec![SerializedAction { - nullifier: [0x11; 32], - rk: [0x22; 32], - cmx: [0x33; 32], - encrypted_note: vec![0x44; 216], - cv_net: [0x55; 32], - spend_auth_sig: [0x66; 64], - }]; - let identity_id = derive_identity_id_from_actions(&actions); - - let st = StateTransition::IdentityCreateFromShieldedPool( - IdentityCreateFromShieldedPoolTransition::V0( - IdentityCreateFromShieldedPoolTransitionV0 { - public_keys: vec![], - denomination: 10_000_000_000, - actions, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - send_to_address_on_creation_failure: PlatformAddress::P2pkh([0u8; 20]), - identity_id, - }, - ), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity create from shielded pool with empty proof, got: {:?}", - result - ); - } - - // --- IdentityCreditTransferToAddresses: empty proof returns error. - #[test] - fn verify_identity_credit_transfer_to_addresses_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; - use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; - - let st = StateTransition::IdentityCreditTransferToAddresses( - IdentityCreditTransferToAddressesTransition::V0( - IdentityCreditTransferToAddressesTransitionV0 { - identity_id: dpp::prelude::Identifier::from([3u8; 32]), - ..Default::default() - }, - ), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity credit transfer to addresses with empty proof, got: {:?}", - result - ); - } - - // ----------------------------------------------------------------------- - // Additional error-path coverage for state transitions whose error arms - // were not previously tested. These tests target proof decoding / early - // validation errors on state transition variants that were not covered - // by the existing test suite. - // ----------------------------------------------------------------------- - - // --- AddressFundsTransfer: empty proof returns error. - #[test] - fn verify_address_funds_transfer_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::address_funds_transfer_transition::v0::AddressFundsTransferTransitionV0; - use dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (1u32, 1000u64)); - let mut outputs = BTreeMap::new(); - outputs.insert(PlatformAddress::P2pkh([2u8; 20]), 500u64); - - let st = StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0( - AddressFundsTransferTransitionV0 { - inputs, - outputs, - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - - // --- AddressCreditWithdrawal: empty proof returns error. - #[test] - fn verify_address_credit_withdrawal_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::identity::core_script::CoreScript; - use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; - use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; - use dpp::withdrawal::Pooling; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); - - let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( - AddressCreditWithdrawalTransitionV0 { - inputs, - output: None, - fee_strategy: vec![], - core_fee_per_byte: 1, - pooling: Pooling::Never, - output_script: CoreScript::from_bytes(vec![]), - user_fee_increase: 0, - input_witnesses: vec![], - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - - // --- AddressCreditWithdrawal with change output: empty proof returns error. - #[test] - fn verify_address_credit_withdrawal_with_change_output_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::identity::core_script::CoreScript; - use dpp::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0; - use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; - use dpp::withdrawal::Pooling; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (1u32, 2000u64)); - - let st = StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( - AddressCreditWithdrawalTransitionV0 { - inputs, - output: Some((PlatformAddress::P2pkh([4u8; 20]), 1000u64)), - fee_strategy: vec![], - core_fee_per_byte: 1, - pooling: Pooling::Never, - output_script: CoreScript::from_bytes(vec![]), - user_fee_increase: 0, - input_witnesses: vec![], - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - - // --- AddressFundingFromAssetLock: empty proof returns error. - #[test] - fn verify_address_funding_from_asset_lock_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; - use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; - use dpp::platform_value::BinaryData; - use dpp::state_transition::address_funding_from_asset_lock_transition::v0::AddressFundingFromAssetLockTransitionV0; - use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; - use std::collections::BTreeMap; - - let mut outputs = BTreeMap::new(); - outputs.insert(PlatformAddress::P2pkh([7u8; 20]), None); - - let st = StateTransition::AddressFundingFromAssetLock( - AddressFundingFromAssetLockTransition::V0(AddressFundingFromAssetLockTransitionV0 { - asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), - inputs: BTreeMap::new(), - outputs, - fee_strategy: vec![], - user_fee_increase: 0, - signature: BinaryData::new(vec![]), - input_witnesses: vec![], - }), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - - // --- Shield: empty proof returns error. - #[test] - fn verify_shield_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::shield_transition::v0::ShieldTransitionV0; - use dpp::state_transition::shield_transition::ShieldTransition; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([8u8; 20]), (1u32, 1000u64)); - - let st = StateTransition::Shield(ShieldTransition::V0(ShieldTransitionV0 { - inputs, - actions: vec![], - amount: 500, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - - // --- ShieldFromAssetLock with invalid (no-output) Instant asset lock proof - // should return InvalidTransition("shield from asset lock has no outpoint"). - // Default InstantAssetLockProof has no AssetLockPayload, so out_point() returns None. - #[test] - fn verify_shield_from_asset_lock_missing_outpoint_returns_invalid_transition() { - let platform_version = PlatformVersion::latest(); - use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; - use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; - use dpp::platform_value::BinaryData; - use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; - - let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( - ShieldFromAssetLockTransitionV0 { - asset_lock_proof: AssetLockProof::Instant(InstantAssetLockProof::default()), - actions: vec![], - value_balance: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - surplus_output: None, - signature: BinaryData::new(vec![]), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected InvalidTransition error, got Ok"); - match err { - crate::error::Error::Proof(ProofError::InvalidTransition(msg)) => { - assert!( - msg.contains("shield from asset lock has no outpoint"), - "unexpected InvalidTransition message: {msg}" - ); - } - other => panic!("expected Error::Proof(InvalidTransition), got {:?}", other), - } - } - - // --- ShieldFromAssetLock with Chain asset lock proof (has outpoint) and - // empty GroveDB proof: should return an error from grove-db verification. - #[test] - fn verify_shield_from_asset_lock_chain_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::dashcore::OutPoint; - use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; - use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; - use dpp::platform_value::BinaryData; - use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; - - let st = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( - ShieldFromAssetLockTransitionV0 { - asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { - core_chain_locked_height: 100, - out_point: OutPoint::from([11u8; 36]), - }), - actions: vec![], - value_balance: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - surplus_output: None, - signature: BinaryData::new(vec![]), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for shield from asset lock (chain) with empty proof, got: {:?}", - result - ); - } - - // --- IdentityCreateFromAddresses: empty proof returns error. - #[test] - fn verify_identity_create_from_addresses_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; - use dpp::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([9u8; 20]), (1u32, 2000u64)); - - let st = StateTransition::IdentityCreateFromAddresses( - IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { - public_keys: vec![], - inputs, - output: None, - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity create from addresses with empty proof, got: {:?}", - result - ); - } - - #[test] - fn verify_identity_create_from_addresses_binds_public_keys() { - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::identity_create_from_addresses_transition::v0::IdentityCreateFromAddressesTransitionV0; - use dpp::state_transition::identity_create_from_addresses_transition::IdentityCreateFromAddressesTransition; - use dpp::state_transition::StateTransitionIdentityIdFromInputs; - use std::collections::BTreeMap; - - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::latest(); - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([9u8; 20]), (1u32, 2000u64)); - let id_transition = - IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { - public_keys: vec![], - inputs: inputs.clone(), - output: None, - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }); - let identity_id = id_transition - .identity_id_from_inputs() - .expect("expected identity id from inputs"); - - let mut identity = Identity::random_identity(3, Some(14), platform_version) - .expect("expected a platform identity"); - identity.set_id(identity_id); - drive - .add_new_identity( - identity.clone(), - false, - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("expected to add identity"); - - let matching = StateTransition::IdentityCreateFromAddresses( - IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { - public_keys: identity.public_keys().values().map(Into::into).collect(), - inputs: inputs.clone(), - output: None, - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }), - ); - let proof = drive - .prove_state_transition(&matching, None, platform_version) - .expect("expected proof creation") - .into_data() - .expect("expected proof data"); - - let matching_result = Drive::verify_state_transition_was_executed_with_proof( - &matching, - &BlockInfo::default(), - &proof, - &|_id| Ok(None), - platform_version, - ); - assert!(matching_result.is_ok()); - - let mismatched = StateTransition::IdentityCreateFromAddresses( - IdentityCreateFromAddressesTransition::V0(IdentityCreateFromAddressesTransitionV0 { - public_keys: vec![], - inputs, - output: None, - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }), - ); - let mismatched_result = Drive::verify_state_transition_was_executed_with_proof( - &mismatched, - &BlockInfo::default(), - &proof, - &|_id| Ok(None), - platform_version, - ); - assert!(matches!( - mismatched_result, - Err(Error::Proof(ProofError::IncorrectProof(_))) - )); - } - - // --- IdentityTopUpFromAddresses: empty proof returns error. - #[test] - fn verify_identity_top_up_from_addresses_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::identity_topup_from_addresses_transition::v0::IdentityTopUpFromAddressesTransitionV0; - use dpp::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; - use std::collections::BTreeMap; - - let mut inputs = BTreeMap::new(); - inputs.insert(PlatformAddress::P2pkh([10u8; 20]), (1u32, 500u64)); - - let st = StateTransition::IdentityTopUpFromAddresses( - IdentityTopUpFromAddressesTransition::V0(IdentityTopUpFromAddressesTransitionV0 { - inputs, - output: None, - identity_id: dpp::prelude::Identifier::from([11u8; 32]), - fee_strategy: vec![], - user_fee_increase: 0, - input_witnesses: vec![], - }), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity top up from addresses with empty proof, got: {:?}", - result - ); - } - - // --- IdentityCreditWithdrawal V1 empty proof returns error. - #[test] - fn verify_identity_credit_withdrawal_v1_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1; - use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; - use dpp::withdrawal::Pooling; - - let st = StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V1( - IdentityCreditWithdrawalTransitionV1 { - identity_id: dpp::prelude::Identifier::random(), - amount: 100, - core_fee_per_byte: 1, - pooling: Pooling::Never, - output_script: None, - nonce: 1, - user_fee_increase: 0, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity credit withdrawal v1 with empty proof, got: {:?}", - result - ); - } - - // --- MasternodeVote: empty proof (but with a known contract) returns - // an Error::Proof / Error::GroveDB because the proof can't be decoded. - #[test] - fn verify_masternode_vote_empty_proof_with_known_contract_returns_error() { - let (_drive, contract) = setup_drive_and_contract(); - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; - use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; - use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; - use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; - use dpp::voting::vote_polls::VotePoll; - use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; - use dpp::voting::votes::resource_vote::ResourceVote; - use dpp::voting::votes::Vote; - - let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( - MasternodeVoteTransitionV0 { - pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), - voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), - vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: VotePoll::ContestedDocumentResourceVotePoll( - ContestedDocumentResourceVotePoll { - contract_id: contract.id(), - document_type_name: "preorder".to_string(), - index_name: "idx".to_string(), - index_values: vec![], - }, - ), - resource_vote_choice: ResourceVoteChoice::Abstain, - })), - nonce: 1, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let contract_arc = Arc::new(contract.clone()); - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(Some(contract_arc.clone())); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for masternode vote with empty proof, got: {:?}", - result - ); - } - - // --- ShieldedWithdrawal with no nullifiers and a known contract that - // triggers InvalidTransition("shielded withdrawal has no nullifiers") via - // the first_nullifier guard. - // - // We route past the grove-db verification by providing `actions: vec![]` - // (so nullifiers() is empty) and a valid empty proof for nullifiers. - // Because proof is empty, grove-db errors out first — we only check for - // some Error variant here (this is a defensive test that ensures the - // known-contract branch doesn't crash). - #[test] - fn verify_shielded_withdrawal_with_known_contract_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::identity::core_script::CoreScript; - use dpp::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0; - use dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition; - use dpp::withdrawal::Pooling; - - let st = StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( - ShieldedWithdrawalTransitionV0 { - actions: vec![], - unshielding_amount: 0, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - core_fee_per_byte: 1, - pooling: Pooling::Never, - output_script: CoreScript::from_bytes(vec![]), - }, - )); - - // Load the real withdrawals contract so the known_contracts_provider - // returns Some. Empty proof will still fail, but we exercise the - // contract-lookup path. - use dpp::data_contracts::withdrawals_contract; - use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; - let withdrawals = - load_system_data_contract(SystemDataContract::Withdrawals, platform_version) - .expect("expected withdrawals system contract"); - let withdrawals_arc = Arc::new(withdrawals); - let expected_id = withdrawals_contract::ID; - let known_contracts_provider_fn: &ContractLookupFn = &|id| { - if id == &expected_id { - Ok(Some(withdrawals_arc.clone())) - } else { - Ok(None) - } - }; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for shielded withdrawal (known contract, empty proof), got: {:?}", - result - ); - } - - // --- Batch V1 with Token Mint transition + unknown contract returns - // UnknownContract error (token transition branch). - /// The classifier is the single authority on which transition families' - /// proofs bind execution; this table pins the snapshot-only families so - /// a reclassification (accidental or deliberate) fails a test instead of - /// silently upgrading a snapshot into execution evidence. - #[test] - fn classifier_tags_snapshot_only_families_as_affected_state() { - use dpp::state_transition::address_credit_withdrawal_transition::AddressCreditWithdrawalTransition; - use dpp::state_transition::address_funding_from_asset_lock_transition::AddressFundingFromAssetLockTransition; - use dpp::state_transition::address_funds_transfer_transition::AddressFundsTransferTransition; - use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; - use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; - use dpp::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; - use dpp::state_transition::identity_topup_from_addresses_transition::IdentityTopUpFromAddressesTransition; - use dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; - - let no_contracts: &ContractLookupFn = &|_id| Ok(None); - - let snapshot_only: Vec<(&str, StateTransition)> = vec![ - ( - "identity top up", - StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(Default::default())), - ), - ( - "identity credit withdrawal", - StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0( - Default::default(), - )), - ), - ( - "identity credit transfer", - StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( - Default::default(), - )), - ), - ( - "identity credit transfer to addresses", - StateTransition::IdentityCreditTransferToAddresses( - IdentityCreditTransferToAddressesTransition::V0(Default::default()), - ), - ), - ( - "identity top up from addresses", - StateTransition::IdentityTopUpFromAddresses( - IdentityTopUpFromAddressesTransition::V0(Default::default()), - ), - ), - ( - "address funds transfer", - StateTransition::AddressFundsTransfer(AddressFundsTransferTransition::V0( - Default::default(), - )), - ), - ( - "address funding from asset lock", - StateTransition::AddressFundingFromAssetLock( - AddressFundingFromAssetLockTransition::V0(Default::default()), - ), - ), - ( - "address credit withdrawal", - StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0( - Default::default(), - )), - ), - ]; - - for (name, st) in snapshot_only { - let binds = Drive::state_transition_proof_binds_execution_v1(&st, no_contracts) - .expect("classifier should not error for contract-independent families"); - assert!( - !binds, - "{name}: proof must be classified as an affected-state snapshot" - ); - } - } - - #[test] - fn verify_no_history_token_empty_proof_returns_error() { - use dpp::data_contract::accessors::v1::DataContractV1Setters; - use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; - use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; - use dpp::data_contract::associated_token::token_keeps_history_rules::v0::TokenKeepsHistoryRulesV0; - use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransition; - use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransition; - use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV1}; - - let platform_version = PlatformVersion::latest(); - let mut contract = - get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - let mut token_configuration = TokenConfigurationV0::default_most_restrictive(); - token_configuration.keeps_history = - TokenKeepsHistoryRulesV0::default_for_keeping_all_history(false).into(); - contract.set_tokens(BTreeMap::from([( - 0, - TokenConfiguration::V0(token_configuration), - )])); - let contract_id = contract.id(); - let token_id = contract.token_id(0).expect("expected token id"); - let owner_id = Identifier::from([9u8; 32]); - let recipient_id = Identifier::from([10u8; 32]); - - let base = || { - TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: contract_id, - token_id, - using_group_info: None, - }) - }; - let transitions = vec![ - TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { - base: base(), - burn_amount: 42, - public_note: None, - })), - TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { - base: base(), - amount: 42, - issued_to_identity_id: Some(recipient_id), - public_note: None, - })), - TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { - base: base(), - amount: 42, - recipient_id, - public_note: None, - shared_encrypted_note: None, - private_encrypted_note: None, - })), - ]; - - let contract = Arc::new(contract); - let known_contracts_provider_fn: &ContractLookupFn = &|id| { - if id == &contract_id { - Ok(Some(contract.clone())) - } else { - Ok(None) - } - }; - - for transition in transitions { - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id, - transitions: vec![BatchedTransition::Token(transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - assert!( - matches!(result, Err(Error::Proof(_)) | Err(Error::GroveDB(_))), - "expected a proof error for an empty proof, got: {:?}", - result - ); - } - } - - // --- Batch V1 with Token Mint transition + unknown contract returns - // UnknownContract error (token transition branch). - #[test] - fn verify_batch_token_mint_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::v0::TokenMintTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_mint_transition::TokenMintTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([44u8; 32]), - token_id: dpp::prelude::Identifier::from([45u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { - base: token_base, - amount: 99, - issued_to_identity_id: Some(dpp::prelude::Identifier::from([77u8; 32])), - public_note: None, - })); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([2u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - match err { - crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { - assert!( - msg.contains("unknown contract") && msg.contains("token verification"), - "unexpected UnknownContract message: {msg}" - ); - } - other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), - } - } - - // --- Batch V1 with Token Transfer transition + unknown contract. - #[test] - fn verify_batch_token_transfer_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([58u8; 32]), - token_id: dpp::prelude::Identifier::from([59u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Transfer(TokenTransferTransition::V0(TokenTransferTransitionV0 { - base: token_base, - amount: 100, - recipient_id: dpp::prelude::Identifier::from([88u8; 32]), - public_note: None, - shared_encrypted_note: None, - private_encrypted_note: None, - })); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([3u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch V1 with Token Freeze transition + unknown contract. - #[test] - fn verify_batch_token_freeze_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::v0::TokenFreezeTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_freeze_transition::TokenFreezeTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([71u8; 32]), - token_id: dpp::prelude::Identifier::from([72u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Freeze(TokenFreezeTransition::V0(TokenFreezeTransitionV0 { - base: token_base, - identity_to_freeze_id: dpp::prelude::Identifier::from([91u8; 32]), - public_note: None, - })); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([4u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch V1 with Token Unfreeze transition + unknown contract. - #[test] - fn verify_batch_token_unfreeze_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::v0::TokenUnfreezeTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([81u8; 32]), - token_id: dpp::prelude::Identifier::from([82u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Unfreeze(TokenUnfreezeTransition::V0(TokenUnfreezeTransitionV0 { - base: token_base, - frozen_identity_id: dpp::prelude::Identifier::from([92u8; 32]), - public_note: None, - })); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([5u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch V1 with Token DestroyFrozenFunds transition + unknown contract. - #[test] - fn verify_batch_token_destroy_frozen_funds_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::v0::TokenDestroyFrozenFundsTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([101u8; 32]), - token_id: dpp::prelude::Identifier::from([102u8; 32]), - using_group_info: None, - }); - let token_transition = TokenTransition::DestroyFrozenFunds( - TokenDestroyFrozenFundsTransition::V0(TokenDestroyFrozenFundsTransitionV0 { - base: token_base, - frozen_identity_id: dpp::prelude::Identifier::from([103u8; 32]), - public_note: None, - }), - ); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([6u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch V1 with Token Claim transition + unknown contract. - #[test] - fn verify_batch_token_claim_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([111u8; 32]), - token_id: dpp::prelude::Identifier::from([112u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { - base: token_base, - distribution_type: TokenDistributionType::PreProgrammed, - public_note: None, - })); - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([7u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch with a single document Replace transition + unknown contract - // returns UnknownContract. - #[test] - fn verify_batch_document_replace_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransition; - use dpp::state_transition::batch_transition::document_replace_transition::DocumentReplaceTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Replace(DocumentReplaceTransition::V0( - DocumentReplaceTransitionV0 { - base, - revision: 2, - data: Default::default(), - }, - ))], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch with a single document Transfer transition + unknown contract. - #[test] - fn verify_batch_document_transfer_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransition; - use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Transfer( - DocumentTransferTransition::V0(DocumentTransferTransitionV0 { - base, - revision: 1, - recipient_owner_id: dpp::prelude::Identifier::from([77u8; 32]), - }), - )], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch with a single document UpdatePrice transition + unknown contract. - #[test] - fn verify_batch_document_update_price_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransition; - use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::UpdatePrice( - DocumentUpdatePriceTransition::V0(DocumentUpdatePriceTransitionV0 { - base, - revision: 1, - price: 500, - }), - )], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch with a single document Purchase transition + unknown contract. - #[test] - fn verify_batch_document_purchase_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransition; - use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "test".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Purchase( - DocumentPurchaseTransition::V0(DocumentPurchaseTransitionV0 { - base, - revision: 1, - price: 500, - }), - )], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch V1 with an empty transitions vec returns InvalidTransition - // (covers the V1 batch "no transition" arm). - #[test] - fn verify_batch_v1_empty_transitions_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: Default::default(), - transitions: vec![], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected InvalidTransition, got Ok"); - match err { - Error::Proof(ProofError::InvalidTransition(msg)) => { - assert!( - msg.contains("no transition"), - "expected 'no transition' message, got: {msg}" - ); - } - other => panic!("expected InvalidTransition, got: {:?}", other), - } - } - - // --- Batch V1 with two transitions returns InvalidTransition (too-many arm). - #[test] - fn verify_batch_v1_too_many_transitions_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let make_delete = |nonce: u64| -> BatchedTransition { - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: nonce, - document_type_name: "x".to_string(), - data_contract_id: Default::default(), - }); - BatchedTransition::Document(DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base }, - ))) - }; - - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: Default::default(), - transitions: vec![make_delete(1), make_delete(2)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected InvalidTransition, got Ok"); - match err { - Error::Proof(ProofError::InvalidTransition(msg)) => { - assert!( - msg.contains("does not support more than one document"), - "expected too-many-document message, got: {msg}" - ); - } - other => panic!("expected InvalidTransition, got: {:?}", other), - } - } - - // --- IdentityCreditTransferToAddresses with address recipients returns - // error for empty proof (exercises the recipient_addresses()-iteration arm). - #[test] - fn verify_identity_credit_transfer_to_addresses_with_recipients_empty_proof_errors() { - let platform_version = PlatformVersion::latest(); - use dpp::address_funds::PlatformAddress; - use dpp::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0; - use dpp::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition; - use std::collections::BTreeMap; - - let mut recipient_addresses = BTreeMap::new(); - recipient_addresses.insert(PlatformAddress::P2pkh([21u8; 20]), 100u64); - - let st = StateTransition::IdentityCreditTransferToAddresses( - IdentityCreditTransferToAddressesTransition::V0( - IdentityCreditTransferToAddressesTransitionV0 { - identity_id: dpp::prelude::Identifier::from([22u8; 32]), - recipient_addresses, - ..Default::default() - }, - ), - ); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for ICTTA with recipients empty proof, got: {:?}", - result - ); - } - - // --- Non-empty, malformed proof bytes → decode / verification error. - // - // Exercises the decoder path inside grove-db for a non-empty but - // garbage proof, which should error out (either Error::GroveDB or - // Error::Proof). - #[test] - fn verify_identity_create_garbage_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::identity_create_transition::IdentityCreateTransition; - use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; - let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - // A short, non-empty garbage proof triggers the decoder error path. - let bad_proof: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11]; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &bad_proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for garbage proof bytes, got: {:?}", - result - ); - } - - // --- DataContractCreate with a larger garbage proof returns an error. - #[test] - fn verify_data_contract_create_garbage_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - let created_contract = - get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version); - let contract = created_contract.data_contract(); - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; - let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( - DataContractCreateTransitionV0 { - data_contract: data_contract_serialized, - identity_nonce: 0, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - let garbage_proof: [u8; 32] = [0x5A; 32]; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &garbage_proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for data contract create with garbage proof, got: {:?}", - result - ); - } - - // --- IdentityCreditTransfer: garbage proof returns error. - #[test] - fn verify_identity_credit_transfer_garbage_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; - use dpp::state_transition::state_transitions::identity::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; - let st = StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0( - IdentityCreditTransferTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - recipient_id: dpp::prelude::Identifier::random(), - amount: 50, - nonce: 1, - ..Default::default() - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - let garbage_proof = vec![0xFFu8; 20]; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &garbage_proof, - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for identity credit transfer with garbage proof, got: {:?}", - result - ); - } - - // --- IdentityCreate: garbage proof with a contract provider that errors - // internally does not affect the Identity path (provider is not queried). - // This sanity-checks that an IdentityCreate with an erroring provider - // still fails on the proof (not on the provider call). - #[test] - fn verify_identity_create_erroring_provider_ignored_for_identity_flow() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::identity_create_transition::IdentityCreateTransition; - use dpp::state_transition::state_transitions::identity::identity_create_transition::v0::IdentityCreateTransitionV0; - let st = StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0 { - identity_id: dpp::prelude::Identifier::random(), - ..Default::default() - }, - )); - - // Provider returns an error, but the IdentityCreate branch never - // consults the provider. The error surface is the empty proof. - let known_contracts_provider_fn: &ContractLookupFn = &|_id| { - Err(Error::Drive( - crate::error::drive::DriveError::CorruptedDriveState( - "unused provider error".to_string(), - ), - )) - }; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected proof/grovedb error (not provider error), got: {:?}", - result - ); - } - - // --- MasternodeVote: provider callback errors → error is propagated. - #[test] - fn verify_masternode_vote_provider_errors_is_propagated() { - let platform_version = PlatformVersion::latest(); - use dpp::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; - use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition; - use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; - use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll; - use dpp::voting::vote_polls::VotePoll; - use dpp::voting::votes::resource_vote::v0::ResourceVoteV0; - use dpp::voting::votes::resource_vote::ResourceVote; - use dpp::voting::votes::Vote; - - let st = StateTransition::MasternodeVote(MasternodeVoteTransition::V0( - MasternodeVoteTransitionV0 { - pro_tx_hash: dpp::prelude::Identifier::from([1u8; 32]), - voter_identity_id: dpp::prelude::Identifier::from([2u8; 32]), - vote: Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 { - vote_poll: VotePoll::ContestedDocumentResourceVotePoll( - ContestedDocumentResourceVotePoll { - contract_id: dpp::prelude::Identifier::from([7u8; 32]), - document_type_name: "some_type".to_string(), - index_name: "idx".to_string(), - index_values: vec![], - }, - ), - resource_vote_choice: ResourceVoteChoice::Abstain, - })), - nonce: 1, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - // Provider returns Err — the error should be propagated as-is. - let known_contracts_provider_fn: &ContractLookupFn = &|_id| { - Err(Error::Drive( - crate::error::drive::DriveError::CorruptedDriveState( - "synthetic provider failure".to_string(), - ), - )) - }; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected provider error, got Ok"); - match err { - Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { - assert_eq!(msg, "synthetic provider failure"); - } - other => panic!("expected CorruptedDriveState, got: {:?}", other), - } - } - - // --- Batch with document transition: provider error is propagated. - #[test] - fn verify_batch_document_provider_error_is_propagated() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransition; - use dpp::state_transition::batch_transition::document_delete_transition::DocumentDeleteTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "x".to_string(), - data_contract_id: Default::default(), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Delete(DocumentDeleteTransition::V0( - DocumentDeleteTransitionV0 { base }, - ))], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| { - Err(Error::Drive( - crate::error::drive::DriveError::CorruptedDriveState( - "synthetic batch provider failure".to_string(), - ), - )) - }; - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected provider error, got Ok"); - match err { - Error::Drive(crate::error::drive::DriveError::CorruptedDriveState(msg)) => { - assert_eq!(msg, "synthetic batch provider failure"); - } - other => panic!("expected CorruptedDriveState, got: {:?}", other), - } - } - - // --- DataContractCreate empty proof + keeps_history=true: exercises the - // happy-path arm up to proof verification but with an empty proof so it - // errors out. - #[test] - fn verify_data_contract_create_keeps_history_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - // Build a contract with keeps_history=true - use dpp::data_contract::config::v0::DataContractConfigV0; - use dpp::data_contract::config::DataContractConfig; - use dpp::data_contract::v1::DataContractV1; - use dpp::prelude::DataContract; - let contract = DataContract::V1(DataContractV1 { - id: dpp::prelude::Identifier::from([99u8; 32]), - version: 1, - owner_id: Default::default(), - document_types: Default::default(), - config: DataContractConfig::V0(DataContractConfigV0 { - can_be_deleted: false, - readonly: false, - keeps_history: true, - documents_keep_history_contract_default: false, - documents_mutable_contract_default: false, - documents_can_be_deleted_contract_default: false, - requires_identity_encryption_bounded_key: None, - requires_identity_decryption_bounded_key: None, - }), - schema_defs: None, - created_at: None, - updated_at: None, - created_at_block_height: None, - updated_at_block_height: None, - created_at_epoch: None, - updated_at_epoch: None, - groups: Default::default(), - tokens: Default::default(), - keywords: Vec::new(), - description: None, - }); - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; - use dpp::state_transition::data_contract_create_transition::DataContractCreateTransitionV0; - let st = StateTransition::DataContractCreate(DataContractCreateTransition::V0( - DataContractCreateTransitionV0 { - data_contract: data_contract_serialized, - identity_nonce: 0, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for keeps_history contract create with empty proof, got: {:?}", - result - ); - } - - // --- DataContractUpdate keeps_history=true + empty proof: errors out. - #[test] - fn verify_data_contract_update_keeps_history_empty_proof_returns_error() { - let platform_version = PlatformVersion::latest(); - use dpp::data_contract::config::v0::DataContractConfigV0; - use dpp::data_contract::config::DataContractConfig; - use dpp::data_contract::v1::DataContractV1; - use dpp::prelude::DataContract; - let contract = DataContract::V1(DataContractV1 { - id: dpp::prelude::Identifier::from([98u8; 32]), - version: 2, - owner_id: Default::default(), - document_types: Default::default(), - config: DataContractConfig::V0(DataContractConfigV0 { - can_be_deleted: false, - readonly: false, - keeps_history: true, - documents_keep_history_contract_default: false, - documents_mutable_contract_default: false, - documents_can_be_deleted_contract_default: false, - requires_identity_encryption_bounded_key: None, - requires_identity_decryption_bounded_key: None, - }), - schema_defs: None, - created_at: None, - updated_at: None, - created_at_block_height: None, - updated_at_block_height: None, - created_at_epoch: None, - updated_at_epoch: None, - groups: Default::default(), - tokens: Default::default(), - keywords: Vec::new(), - description: None, - }); - let data_contract_serialized: DataContractInSerializationFormat = contract - .clone() - .try_into_platform_versioned(platform_version) - .expect("expected to serialize contract"); - use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransitionV0; - let st = StateTransition::DataContractUpdate(DataContractUpdateTransition::V0( - DataContractUpdateTransitionV0 { - identity_contract_nonce: 1, - data_contract: data_contract_serialized, - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - }, - )); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - assert!( - matches!( - result, - Err(crate::error::Error::Proof(_)) | Err(crate::error::Error::GroveDB(_)) - ), - "expected error for keeps_history contract update with empty proof, got: {:?}", - result - ); - } - - // --- Batch V0 with a Document Create transition in contested status - // + unknown contract returns UnknownContract. The contested_status arm - // is a distinct branch that other tests don't cover. - #[test] - fn verify_batch_document_contested_create_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; - use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; - use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; - use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransition; - use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV0; - - let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { - id: Default::default(), - identity_contract_nonce: 1, - document_type_name: "t".to_string(), - data_contract_id: Default::default(), - }); - - let create_transition = DocumentCreateTransition::V0(DocumentCreateTransitionV0 { - base, - entropy: [0u8; 32], - data: Default::default(), - // Contested (prefunded) create → separate branch in verify() - prefunded_voting_balance: Some(("dash".to_string(), 1000)), - }); - - let st = StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { - owner_id: Default::default(), - transitions: vec![DocumentTransition::Create(create_transition)], - ..Default::default() - })); - - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract, got Ok"); - assert!( - matches!( - err, - crate::error::Error::Proof(ProofError::UnknownContract(_)) - ), - "expected Error::Proof(UnknownContract), got: {:?}", - err - ); - } - - // --- Batch with a single Token transition + unknown contract returns - // UnknownContract error (covers the token transition branch). - #[test] - fn verify_batch_token_transition_unknown_contract_returns_error() { - let platform_version = PlatformVersion::latest(); - - use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; - use dpp::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransition; - use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; - use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; - use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; - use dpp::state_transition::batch_transition::BatchTransition; - use dpp::state_transition::batch_transition::BatchTransitionV1; - - let token_base = TokenBaseTransition::V0(TokenBaseTransitionV0 { - identity_contract_nonce: 1, - token_contract_position: 0, - data_contract_id: dpp::prelude::Identifier::from([55u8; 32]), - token_id: dpp::prelude::Identifier::from([66u8; 32]), - using_group_info: None, - }); - let token_transition = - TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { - base: token_base, - burn_amount: 42, - public_note: None, - })); - - // V1 supports BatchedTransition::Token. V0 only supports document - // transitions — use V1 to include a token transition. - let st = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { - owner_id: dpp::prelude::Identifier::from([9u8; 32]), - transitions: vec![BatchedTransition::Token(token_transition)], - user_fee_increase: 0, - signature_public_key_id: 0, - signature: Default::default(), - })); - - // Provider returns None — triggers UnknownContract for the token. - let known_contracts_provider_fn: &ContractLookupFn = &|_id| Ok(None); - - let result = Drive::verify_state_transition_was_executed_with_proof( - &st, - &BlockInfo::default(), - &[], - known_contracts_provider_fn, - platform_version, - ); - - let err = result.expect_err("expected UnknownContract error, got Ok"); - match err { - crate::error::Error::Proof(ProofError::UnknownContract(msg)) => { - assert!( - msg.contains("unknown contract") && msg.contains("token verification"), - "unexpected UnknownContract message: {msg}" - ); - } - other => panic!("expected Error::Proof(UnknownContract), got {:?}", other), - } - } -} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs index f8ea147b407..51392436e0d 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs @@ -1,10 +1,10 @@ -//! V4 (PV15): V3 plus batch transition format 2 and the erase kind. +//! V4 (PV15): V3 plus the erase kind. //! -//! Format 2 carries a document transition shell that knows the erase kind -//! (`DocumentEraseTransition`), which purges the retained revisions of a -//! deleted keep-history document. Formats 0 and 1 keep their shells, so the -//! kind cannot appear in them; `document_erase_state_transition` bounds the -//! kind's own generation and stays `None` in every earlier table. +//! `DocumentEraseTransition` purges the retained revisions of a deleted +//! keep-history document. The kind is appended to the batch's document +//! transition enum; `document_erase_state_transition` bounds its generation +//! and stays `None` in every earlier table, where the batch basic-structure +//! gate refuses the kind. use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ @@ -14,11 +14,6 @@ use versioned_feature_core::FeatureVersionBounds; pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V4: DPPStateTransitionSerializationVersions = DPPStateTransitionSerializationVersions { - batch_state_transition: FeatureVersionBounds { - min_version: 0, - max_version: 2, - default_current_version: 2, - }, document_erase_state_transition: Some(DocumentFeatureVersionBounds { bounds: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs index c628f24ee2e..1bde330b4db 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs @@ -3,7 +3,6 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; pub mod v3; -pub mod v4; #[derive(Clone, Debug, Default)] pub struct DPPStateTransitionVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs deleted file mode 100644 index 5d40182e37b..00000000000 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v4.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_state_transition_versions::{ - DPPStateTransitionVersions, DocumentTransitionVersions, - DocumentsBatchTransitionValidationVersions, DocumentsBatchTransitionVersions, -}; - -/// Protocol 15 selects the batch validator that sees every batch wire format, -/// including format 2, whose document shell carries the erase kind. The -/// generation selected by earlier protocol versions only sees the shell of -/// formats 0 and 1 and remains unchanged. -pub const STATE_TRANSITION_VERSIONS_V4: DPPStateTransitionVersions = DPPStateTransitionVersions { - documents: DocumentTransitionVersions { - documents_batch_transition: DocumentsBatchTransitionVersions { - validation: DocumentsBatchTransitionValidationVersions { - validate_base_structure: 1, - ..STATE_TRANSITION_VERSIONS_V3 - .documents - .documents_batch_transition - .validation - }, - }, - }, - ..STATE_TRANSITION_VERSIONS_V3 -}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 80b5a065c28..a83d4a53486 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -215,14 +215,6 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// (PROTOCOL_VERSION_12+) passes `Some(epoch)` and bills via /// `execution_context.add_operation`. pub fetch_documents_for_transitions_knowing_contract_and_document_type: FeatureVersion, - /// Versions - /// `fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type`, - /// the sibling helper that reads transitions through the shell that knows - /// every batch wire format. Active from protocol version 15; earlier - /// versions only see the shell of wire formats 0 and 1 and use the helper - /// above. - pub fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - OptionalFeatureVersion, /// Versions the `fetch_document_with_id` helper. Same v0 vs v1 /// semantics as /// `fetch_documents_for_transitions_knowing_contract_and_document_type`. diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index ae11c96b747..a5faa5a3acb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -109,8 +109,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index e0415355848..d568945f5ff 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -157,8 +157,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). fetch_documents_for_transitions_knowing_contract_and_document_type: 1, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { // PROTOCOL_VERSION_14: v2 adds DashPay `profile` diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs index 0dc6391c704..a31ff1fbbe2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -5,38 +5,23 @@ use crate::version::drive_abci_versions::drive_abci_validation_versions::{ }; /// Protocol v15 validation versions: the delete and erase lifecycle of -/// keep-history documents, carried by batch transition wire format 2. +/// keep-history documents. /// -/// Every batch generation bumped here reads a batch through the shell that -/// knows the erase kind and produces or consumes batch action format 1, whose -/// items may include an erase. The generations selected by earlier protocol -/// versions keep the shell and action format of wire formats 0 and 1. +/// The batch generations selected by earlier protocol versions stay selected; +/// the erase kind is carried by the shipped batch and reaches them through the +/// arms appended for it, which the basic-structure gate keeps unreachable +/// below this version. /// -/// * `transform_into_action: 2` runs the transformer that sees every wire -/// format and emits action format 1; -/// * `advanced_structure: 1` and `state: 1` validate action format 1; -/// * `revision: 1` and `is_allowed: 1` read nonces and contested creates from -/// a batch of any wire format; -/// * `fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: -/// Some(0)` bills like generation 1 of the sibling helper for transitions -/// viewed through the shell of any wire format; /// * delete structure v2 admits a delete of a keep-history document and /// refuses one whose type carries a contested index; delete state v1 refuses /// a delete of a document that is already deleted or erasing as a paid -/// consensus error; create state v3 refuses the id of such a document; the -/// erase generations validate the erase itself and read the lifecycle it -/// acts on. +/// consensus error; create state v3 refuses the id of such a document; +/// * the erase generations validate the erase itself and read the lifecycle +/// it acts on. pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = DriveAbciValidationVersions { state_transitions: DriveAbciStateTransitionValidationVersions { batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { - advanced_structure: 1, - state: 1, - revision: 1, - transform_into_action: 2, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - Some(0), - is_allowed: 1, document_delete_transition_structure_validation: 2, document_create_transition_state_validation: 3, document_erase_transition_structure_validation: Some(0), diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index dd3e3d44f32..46edb3178c1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -109,8 +109,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index e975e2f85db..95ef1fcccda 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -109,8 +109,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index e226bf423fb..8f962d14a87 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -112,8 +112,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 7eb5c759cb8..dcbf2db5503 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -113,8 +113,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 8ec6cdf98d5..33ead66ce64 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -116,8 +116,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 9844beb851e..c9befa9af48 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -110,8 +110,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = transform_into_action: 0, failed_per_transition_action: 0, fetch_documents_for_transitions_knowing_contract_and_document_type: 0, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index c71e1e6c33f..83a0b29efc2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -158,8 +158,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). fetch_documents_for_transitions_knowing_contract_and_document_type: 1, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 028798e9db4..1de62df75b3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -151,8 +151,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = // replay (the v0 helpers pass epoch=None and never call // add_operation — byte-identical to pre-PR behavior). fetch_documents_for_transitions_knowing_contract_and_document_type: 1, - fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type: - None, fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { // PROTOCOL_VERSION_13: v1 drops the reject bindings for diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs index 40ec620cd05..106066efa67 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v5.rs @@ -9,16 +9,13 @@ use crate::version::drive_versions::drive_state_transition_method_versions::{ /// /// Delete conversion generation 1 records a lifecycle entry for a keep-history /// document instead of removing its retained revisions; erase conversion -/// removes them a chunk at a time; batch conversion generation 1 consumes -/// batch action format 1, whose items may include an erase, and emits the -/// operations of every item. +/// removes them a chunk at a time. pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5: DriveStateTransitionMethodVersions = DriveStateTransitionMethodVersions { convert_to_high_level_operations: DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions { document_delete_transition: 1, document_erase_transition: Some(0), - documents_batch_transition: 1, ..DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4.convert_to_high_level_operations }, ..DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4 diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs index d622addb544..013b30fda61 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_verify_method_versions/v3.rs @@ -1,20 +1,13 @@ use super::v2::DRIVE_VERIFY_METHOD_VERSIONS_V2; -use super::{ - DriveVerifyDocumentMethodVersions, DriveVerifyMethodVersions, - DriveVerifyStateTransitionMethodVersions, -}; +use super::{DriveVerifyDocumentMethodVersions, DriveVerifyMethodVersions}; /// Verification of composite document history and its independent metadata proof, -/// the cursor lookup of a proved page in the protocol 15 keep-history layout, and -/// state transition execution proofs for batch wire format 2. +/// and the cursor lookup of a proved page in the protocol 15 keep-history layout. pub const DRIVE_VERIFY_METHOD_VERSIONS_V3: DriveVerifyMethodVersions = DriveVerifyMethodVersions { document: DriveVerifyDocumentMethodVersions { verify_document_history: 1, verify_start_at_document_in_proof: 1, ..DRIVE_VERIFY_METHOD_VERSIONS_V2.document }, - state_transition: DriveVerifyStateTransitionMethodVersions { - verify_state_transition_was_executed_with_proof: 1, - }, ..DRIVE_VERIFY_METHOD_VERSIONS_V2 }; 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 21f8c074d5b..2a11156336d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -3,7 +3,7 @@ use super::drive_document_method_versions::v5::DRIVE_DOCUMENT_METHOD_VERSIONS_V5 use super::drive_state_transition_method_versions::v5::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5; use super::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; use super::v9::DRIVE_VERSION_V9; -use super::{DriveMethodVersions, DriveProveMethodVersions, DriveVersion}; +use super::{DriveMethodVersions, DriveVersion}; /// Drive version 10, introduced in protocol v15. /// @@ -11,18 +11,13 @@ use super::{DriveMethodVersions, DriveProveMethodVersions, DriveVersion}; /// matching contract writers, document operations, queries, and proof verifier. /// It also selects the delete and erase lifecycle of keep-history documents: /// the delete wrappers and action conversion that record and consume a -/// lifecycle entry, the batch conversion that knows action format 1, and the -/// state transition prover and verifier that know batch wire format 2. +/// lifecycle entry, and the erase conversion that removes retained revisions. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { methods: DriveMethodVersions { document: DRIVE_DOCUMENT_METHOD_VERSIONS_V5, contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, verify: DRIVE_VERIFY_METHOD_VERSIONS_V3, state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V5, - prove: DriveProveMethodVersions { - prove_state_transition: 1, - ..DRIVE_VERSION_V9.methods.prove - }, ..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 874bbfb0da4..3f4f9953f7f 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -1,6 +1,5 @@ use crate::version::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v4::STATE_TRANSITION_SERIALIZATION_VERSIONS_V4; -use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; use crate::version::dpp_versions::dpp_validation_versions::v6::DPP_VALIDATION_VERSIONS_V6; use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; @@ -39,25 +38,23 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// the last chunk removes the lifecycle entry, after which the id reads as /// absent. The history query reports the erasing state, its start time and /// the remaining revision count. -/// 4. **Batch transition format 2.** The erase kind is carried by a new batch -/// wire format whose document transition shell knows it; formats 0 and 1 -/// keep their shells and cannot carry an erase. Format 2 becomes the -/// default the SDKs emit. Inside Drive ABCI a batch becomes action format 1, -/// and the generations that transform, validate, convert, prove and verify -/// a batch were bumped to read every wire format. +/// 4. **Wire.** The erase kind is appended to the batch's document transition +/// enum, so a batch of either shipped wire format may carry one. The batch +/// basic-structure gate admits the kind only where this protocol version +/// publishes bounds for it, which keeps new software agreeing with software +/// that cannot decode the kind while an earlier version is still active. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: document v5 and contract v5 write the per-type history tree and the delete/erase lifecycle; verify v3 authenticates history pages, lifecycle counts and format 2 execution proofs; state transition methods v5 convert the lifecycle and batch action format 1; prover v1 proves format 2 + drive: DRIVE_VERSION_V10, // changed: document v5 and contract v5 write the per-type history tree and the delete/erase lifecycle; verify v3 authenticates history pages and lifecycle counts; state transition methods v5 convert the lifecycle delete and the erase drive_abci: DriveAbciVersion { methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the protocol-change hook v2 migrates retained histories on the first v15 block - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: batch generations read wire format 2 and action format 1; delete consults the lifecycle; erase structure and state validation + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: delete consults the lifecycle; erase structure and state validation query: DRIVE_ABCI_QUERY_VERSIONS_V4, // changed: the history handler reports the deleted and erasing lifecycle states with their times ..PLATFORM_V14.drive_abci }, dpp: DPPVersion { validation: DPP_VALIDATION_VERSIONS_V6, // changed: document type update validation v2 keeps `canBeErased` immutable - state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V4, // changed: batch transition format 2 and the erase kind join the wire - state_transitions: STATE_TRANSITION_VERSIONS_V4, // changed: batch basic structure v1 reads every wire format + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V4, // changed: the erase kind joins the wire contract_versions: CONTRACT_VERSIONS_V7, // changed: v4 document meta-schema and parser generation 4 admit deletable keep-history types and `canBeErased` ..PLATFORM_V14.dpp }, diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index e7e5195e568..a076b0e60e4 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -85,26 +85,6 @@ fn should_preserve_released_keep_history_validation_versions() { released_insert_generation, "document insert at protocol {protocol}" ); - assert_eq!( - version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version, - 1, - "batch wire versions at protocol {protocol}" - ); - assert_eq!( - version - .dpp - .state_transitions - .documents - .documents_batch_transition - .validation - .validate_base_structure, - 0, - "batch structure validation at protocol {protocol}" - ); assert_eq!( version .drive @@ -115,26 +95,6 @@ fn should_preserve_released_keep_history_validation_versions() { 0, "delete action conversion at protocol {protocol}" ); - assert_eq!( - version - .drive - .methods - .verify - .state_transition - .verify_state_transition_was_executed_with_proof, - 0, - "state transition proof verification at protocol {protocol}" - ); - assert_eq!( - version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action, - 1, - "batch action transform at protocol {protocol}" - ); } } @@ -201,35 +161,6 @@ fn should_activate_keep_history_validation_at_protocol_15() { 2, "protocol 15 selects the insert that never writes over retained revisions" ); - assert_eq!( - version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .max_version, - 2, - "protocol 15 admits BatchTransitionV2" - ); - assert_eq!( - version - .dpp - .state_transition_serialization_versions - .batch_state_transition - .default_current_version, - 2, - "protocol 15 constructs BatchTransitionV2" - ); - assert_eq!( - version - .dpp - .state_transitions - .documents - .documents_batch_transition - .validation - .validate_base_structure, - 1, - "protocol 15 selects structure validation that admits erase only in V2" - ); assert_eq!( version .drive @@ -240,57 +171,6 @@ fn should_activate_keep_history_validation_at_protocol_15() { 1, "protocol 15 selects the lifecycle-specific delete operation" ); - assert_eq!( - version - .drive - .methods - .verify - .state_transition - .verify_state_transition_was_executed_with_proof, - 1, - "protocol 15 selects erase-aware proof verification" - ); - assert_eq!( - version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action, - 2, - "protocol 15 selects the erase-aware transformer" - ); - assert_eq!( - version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .advanced_structure, - 1, - "protocol 15 selects erase-aware advanced validation" - ); - assert_eq!( - version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .state, - 1, - "protocol 15 selects erase-aware state validation" - ); - assert_eq!( - version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .fetch_documents_for_transitions_knowing_contract_and_document_type, - 1, - "the fetch through the shell of wire formats 0 and 1 is unchanged; the \ - shell of any format has its own slot" - ); let bounds = version .dpp @@ -313,6 +193,98 @@ fn should_activate_keep_history_validation_at_protocol_15() { ); } +/// The erase kind is appended to the batch's document transition enum and +/// carried by the shipped batch wire formats, so no batch-level generation +/// changes with it: the batch wire bounds and every generation that reads a +/// batch as a whole are the same at protocol 15 as at protocol 14. Only the +/// per-kind erase slots turn on. +#[test] +fn should_carry_the_erase_kind_without_a_new_batch_generation() { + let released = PlatformVersion::get(14).unwrap(); + let current = PlatformVersion::get(15).unwrap(); + + let batch_bounds = |version: &PlatformVersion| { + let bounds = &version + .dpp + .state_transition_serialization_versions + .batch_state_transition; + ( + bounds.min_version, + bounds.max_version, + bounds.default_current_version, + ) + }; + assert_eq!( + batch_bounds(current), + batch_bounds(released), + "the batch wire formats are unchanged; the erase kind rides inside them" + ); + assert_eq!( + batch_bounds(current), + (0, 1, 1), + "the batch default wire format stays 1" + ); + + let batch_validation = |version: &PlatformVersion| { + version + .dpp + .state_transitions + .documents + .documents_batch_transition + .validation + .validate_base_structure + }; + assert_eq!( + batch_validation(current), + batch_validation(released), + "the batch basic-structure generation gates the erase kind by its bounds slot" + ); + + let batch_generations = |version: &PlatformVersion| { + let batch = &version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition; + ( + batch.transform_into_action, + batch.advanced_structure, + batch.state, + batch.revision, + batch.is_allowed, + batch.fetch_documents_for_transitions_knowing_contract_and_document_type, + ) + }; + assert_eq!( + batch_generations(current), + batch_generations(released), + "the batch transformer, structure, state, nonce and admission generations are unchanged" + ); + + let batch_drive = |version: &PlatformVersion| { + ( + version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .documents_batch_transition, + version.drive.methods.prove.prove_state_transition, + version + .drive + .methods + .verify + .state_transition + .verify_state_transition_was_executed_with_proof, + ) + }; + assert_eq!( + batch_drive(current), + batch_drive(released), + "the batch conversion, prover and execution-proof verifier are unchanged" + ); +} + /// The lifecycle read and the erase storage operation exist at exactly one /// version each, so no table can select an implementation that is not there. #[test] @@ -394,13 +366,13 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { ); assert_eq!( version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .fetch_documents_for_transitions_of_any_format_knowing_contract_and_document_type, + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .as_ref() + .map(|bounds| bounds.bounds.default_current_version), expected_erase_version, - "document fetch through the shell of any wire format at protocol {protocol}" + "erase wire bounds at protocol {protocol}" ); } } diff --git a/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs b/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs index 2e128df29db..2460dfaf975 100644 --- a/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs +++ b/packages/rs-platform-wallet-ffi/src/parse_state_transition.rs @@ -21,9 +21,9 @@ use std::borrow::Cow; use std::slice; use dpp::serialization::PlatformDeserializableUntrusted; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; -use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRefV1; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::state_transition::batch_transition::token_direct_purchase_transition::v0::v0_methods::TokenDirectPurchaseTransitionV0Methods; @@ -172,7 +172,7 @@ fn project_parsed_token_direct_purchase( batch: &BatchTransition, batch_description: &str, ) -> Result { - let transitions_len = batch.transitions_len_v1(); + let transitions_len = batch.transitions_len(); if transitions_len != 1 { return Err(PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, @@ -184,8 +184,8 @@ fn project_parsed_token_direct_purchase( )); } - let Some(BatchedTransitionRefV1::Token(TokenTransition::DirectPurchase(purchase))) = - batch.first_transition_v1() + let Some(BatchedTransitionRef::Token(TokenTransition::DirectPurchase(purchase))) = + batch.first_transition() else { return Err(PlatformWalletFFIResult::err( PlatformWalletFFIResultCode::ErrorInvalidParameter, diff --git a/packages/rs-sdk/src/platform/documents/transitions/erase.rs b/packages/rs-sdk/src/platform/documents/transitions/erase.rs index 102fff3f9e7..bb35a4a96c3 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/erase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/erase.rs @@ -8,7 +8,7 @@ use dpp::document::{Document, INITIAL_REVISION}; use dpp::identity::signer::Signer; use dpp::identity::IdentityPublicKey; use dpp::prelude::UserFeeIncrease; -use dpp::state_transition::batch_transition::methods::v2::DocumentsBatchTransitionMethodsV2; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::proof_result::StateTransitionProofResult; @@ -141,8 +141,8 @@ impl DocumentEraseTransitionBuilder { identity_contract_nonce, user_fee_increase, signer, - creation_options, platform_version, + creation_options, ) .await?; @@ -185,10 +185,10 @@ impl DocumentEraseTransitionBuilder { let batch_feature_version = creation_options .and_then(|options| options.batch_feature_version) .unwrap_or(serialization.batch_state_transition.default_current_version); - if batch_feature_version != 2 { + if !matches!(batch_feature_version, 0 | 1) { return Err(Error::Protocol(ProtocolError::UnknownVersionMismatch { method: "DocumentEraseTransitionBuilder::sign".to_string(), - known_versions: vec![2], + known_versions: vec![0, 1], received: batch_feature_version, })); } @@ -420,7 +420,7 @@ mod tests { #[test] fn should_sign_with_the_settings_fee_increase_unless_set_explicitly() { let options = StateTransitionCreationOptions { - batch_feature_version: Some(2), + batch_feature_version: Some(1), ..Default::default() }; let settings = PutSettings { @@ -455,13 +455,13 @@ mod tests { let current = PlatformVersion::latest(); DocumentEraseTransitionBuilder::check_erase_is_constructible(None, current) .expect("protocol 15 constructs erases"); - for unsupported in [0, 1] { + for shipped in [0, 1] { let options = StateTransitionCreationOptions { - batch_feature_version: Some(unsupported), + batch_feature_version: Some(shipped), ..Default::default() }; DocumentEraseTransitionBuilder::check_erase_is_constructible(Some(&options), current) - .expect_err("a pre-erase batch generation is refused before nonce reservation"); + .expect("both shipped batch wire formats carry an erase"); } let unknown_batch = StateTransitionCreationOptions { batch_feature_version: Some(9), diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs index 89e0e5df571..1a5acf13908 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/document_transition/mod.rs @@ -68,6 +68,7 @@ impl DocumentTransitionWasm { DocumentTransition::Transfer(_) => JsValue::null(), DocumentTransition::UpdatePrice(_) => JsValue::null(), DocumentTransition::Purchase(_) => JsValue::null(), + DocumentTransition::Erase(_) => JsValue::null(), DocumentTransition::IndexOnlyDelete(index_only_delete) => { let json_value = index_only_delete.data().to_json_value().unwrap(); json_value @@ -118,6 +119,7 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(update_price) => Some(update_price.price()), DocumentTransition::Purchase(purchase) => Some(purchase.price()), DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } @@ -131,6 +133,7 @@ impl DocumentTransitionWasm { DocumentTransition::UpdatePrice(_) => None, DocumentTransition::Purchase(_) => None, DocumentTransition::IndexOnlyDelete(_) => None, + DocumentTransition::Erase(_) => None, } } diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index 2a0b45775ae..cce5e70e488 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -21,11 +21,9 @@ use dpp::state_transition::StateTransition::{ IdentityCreditWithdrawal, IdentityUpdate, MasternodeVote, ShieldFromIdentity, }; use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionV1, -}; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; @@ -401,14 +399,6 @@ impl StateTransitionWasm { Some(token_batch.identity_contract_nonce()) } }, - BatchTransition::V2(v2) => match v2.transitions.first()? { - BatchedTransitionV1::Document(doc_batch) => { - Some(doc_batch.identity_contract_nonce()) - } - BatchedTransitionV1::Token(token_batch) => { - Some(token_batch.identity_contract_nonce()) - } - }, }, StateTransition::IdentityCreate(_) => None, IdentityTopUp(_) => None, @@ -541,11 +531,6 @@ impl StateTransitionWasm { BatchTransition::V1(v1) } - BatchTransition::V2(mut v2) => { - v2.owner_id = owner_id; - - BatchTransition::V2(v2) - } }; self.0 = Batch(batch); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs index 18b4d6b736a..a1ee2efe8f2 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batch_transition.rs @@ -11,14 +11,10 @@ use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; use dpp::platform_value::string_encoding::{decode, encode}; use dpp::prelude::UserFeeIncrease; use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; -use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; -use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionV1, -}; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; -use dpp::state_transition::batch_transition::{ - BatchTransition, BatchTransitionV1, BatchTransitionV2, -}; +use dpp::state_transition::batch_transition::{BatchTransition, BatchTransitionV1}; use dpp::state_transition::{ StateTransition, StateTransitionIdentitySigned, StateTransitionLike, StateTransitionOwned, StateTransitionSingleSigned, @@ -80,7 +76,7 @@ impl From for BatchTransition { fn convert_array_to_vec_batched( batched_transitions: &js_sys::Array, -) -> WasmDppResult> { +) -> WasmDppResult> { let mut transitions = Vec::with_capacity(batched_transitions.length() as usize); for batched_transition in batched_transitions.iter() { @@ -88,25 +84,12 @@ fn convert_array_to_vec_batched( .to_wasm::("BatchedTransition")? .clone(); - transitions.push(BatchedTransitionV1::from(transition)); + transitions.push(BatchedTransition::from(transition)); } Ok(transitions) } -/// The same transitions in the shell of batch formats 0 and 1, when every one -/// of them fits there; an erase does not. -fn downcast_batched_transitions( - transitions: Vec, -) -> Result, Vec> { - transitions - .iter() - .cloned() - .map(BatchedTransition::try_from) - .collect::, _>>() - .map_err(|_| transitions) -} - #[wasm_bindgen(js_class = BatchTransition)] impl BatchTransitionWasm { #[wasm_bindgen(js_name = "fromBatchedTransitions")] @@ -116,34 +99,22 @@ impl BatchTransitionWasm { #[wasm_bindgen(js_name = "userFeeIncrease")] user_fee_increase: UserFeeIncrease, ) -> WasmDppResult { let transitions = convert_array_to_vec_batched(batched_transitions)?; - let owner_id = owner_id.try_into()?; - // Batch format 1 carries every transition but an erase; an erase needs - // format 2, which only protocol version 15 and later accept. - let batch = match downcast_batched_transitions(transitions) { - Ok(transitions) => BatchTransition::V1(BatchTransitionV1 { - owner_id, - transitions, - user_fee_increase, - signature_public_key_id: 0u32, - signature: BinaryData::default(), - }), - Err(transitions) => BatchTransition::V2(BatchTransitionV2 { - owner_id, + Ok(BatchTransitionWasm(BatchTransition::V1( + BatchTransitionV1 { + owner_id: owner_id.try_into()?, transitions, user_fee_increase, signature_public_key_id: 0u32, signature: BinaryData::default(), - }), - }; - - Ok(BatchTransitionWasm(batch)) + }, + ))) } #[wasm_bindgen(getter = "transitions")] pub fn batched_transitions(&self) -> Vec { self.0 - .transitions_iter_v1() + .transitions_iter() .map(|transition| BatchedTransitionWasm::from(transition.to_owned_transition())) .collect() } @@ -155,20 +126,7 @@ impl BatchTransitionWasm { ) -> WasmDppResult<()> { let transitions = convert_array_to_vec_batched(batched_transitions)?; - match (&mut self.0, downcast_batched_transitions(transitions)) { - (BatchTransition::V2(batch), Ok(transitions)) => { - batch.transitions = transitions.into_iter().map(Into::into).collect(); - } - (BatchTransition::V2(batch), Err(transitions)) => { - batch.transitions = transitions; - } - (batch, Ok(transitions)) => batch.set_transitions(transitions), - (_, Err(_)) => { - return Err(WasmDppError::invalid_argument( - "an erase transition needs batch transition format 2", - )); - } - } + self.0.set_transitions(transitions); Ok(()) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs index d7c70037e76..3fbbf9b8065 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/batched_transition.rs @@ -6,13 +6,13 @@ use crate::state_transitions::batch::document_transition::DocumentTransitionWasm use crate::state_transitions::batch::token_transition::TokenTransitionWasm; use crate::utils::get_class_type; use dpp::prelude::Identifier; -use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; +use dpp::state_transition::batch_transition::batched_transition::document_transition::{ + DocumentTransition, DocumentTransitionV0Methods, +}; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ TokenTransition, TokenTransitionV0Methods, }; -use dpp::state_transition::batch_transition::batched_transition::{ - BatchedTransition, BatchedTransitionV1, DocumentTransitionV1, -}; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; @@ -34,26 +34,17 @@ impl_from_for_extern_type!( TokenTransitionWasm ); -/// Wraps the batched transition shell of batch format 2, which knows every -/// kind the older batch formats carry plus the erase kind, so one class -/// mirrors a batched transition of any batch format. #[derive(Debug, Clone, PartialEq)] #[wasm_bindgen(js_name = "BatchedTransition")] -pub struct BatchedTransitionWasm(BatchedTransitionV1); - -impl From for BatchedTransitionWasm { - fn from(v: BatchedTransitionV1) -> Self { - BatchedTransitionWasm(v) - } -} +pub struct BatchedTransitionWasm(BatchedTransition); impl From for BatchedTransitionWasm { fn from(v: BatchedTransition) -> Self { - BatchedTransitionWasm(v.into()) + BatchedTransitionWasm(v) } } -impl From for BatchedTransitionV1 { +impl From for BatchedTransition { fn from(v: BatchedTransitionWasm) -> Self { v.0 } @@ -71,11 +62,11 @@ impl BatchedTransitionWasm { } match get_class_type(&js_transition)?.as_str() { - "TokenTransition" => Ok(BatchedTransitionWasm(BatchedTransitionV1::Token( + "TokenTransition" => Ok(BatchedTransitionWasm::from(BatchedTransition::from( TokenTransition::from(TokenTransitionWasm::try_from(&js_transition)?), ))), - "DocumentTransition" => Ok(BatchedTransitionWasm(BatchedTransitionV1::Document( - DocumentTransitionV1::from(DocumentTransitionWasm::try_from(&js_transition)?), + "DocumentTransition" => Ok(BatchedTransitionWasm(BatchedTransition::Document( + DocumentTransition::from(DocumentTransitionWasm::try_from(&js_transition)?), ))), _ => Err(WasmDppError::invalid_argument("Invalid transition type")), } @@ -84,10 +75,10 @@ impl BatchedTransitionWasm { #[wasm_bindgen(js_name = "toTransition")] pub fn to_transition(&self) -> BatchedTransitionLikeJs { match &self.0 { - BatchedTransitionV1::Document(document_transition) => { + BatchedTransition::Document(document_transition) => { DocumentTransitionWasm::from(document_transition.clone()).into() } - BatchedTransitionV1::Token(token_transition) => { + BatchedTransition::Token(token_transition) => { TokenTransitionWasm::from(token_transition.clone()).into() } } @@ -96,10 +87,10 @@ impl BatchedTransitionWasm { #[wasm_bindgen(getter = "dataContractId")] pub fn data_contract_id(&self) -> IdentifierWasm { match self.0.clone() { - BatchedTransitionV1::Document(document_transition) => { + BatchedTransition::Document(document_transition) => { document_transition.data_contract_id().into() } - BatchedTransitionV1::Token(token_transition) => { + BatchedTransition::Token(token_transition) => { token_transition.data_contract_id().into() } } @@ -113,15 +104,15 @@ impl BatchedTransitionWasm { let contract_id: Identifier = contract_id.try_into()?; self.0 = match self.0.clone() { - BatchedTransitionV1::Document(mut document_transition) => { + BatchedTransition::Document(mut document_transition) => { document_transition.set_data_contract_id(contract_id); - BatchedTransitionV1::Document(document_transition) + BatchedTransition::Document(document_transition) } - BatchedTransitionV1::Token(mut token_transition) => { + BatchedTransition::Token(mut token_transition) => { token_transition.set_data_contract_id(contract_id); - BatchedTransitionV1::Token(token_transition) + BatchedTransition::Token(token_transition) } }; diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs index 03864de991e..1e5cce18f0b 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transition.rs @@ -13,33 +13,23 @@ use dpp::prelude::{IdentityNonce, Revision}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; use dpp::state_transition::batch_transition::batched_transition::document_transition_action_type::{ DocumentTransitionActionType, DocumentTransitionActionTypeGetter, }; use crate::utils::try_to_u64; use wasm_bindgen::prelude::wasm_bindgen; -/// Wraps the document transition shell of batch format 2, which knows every -/// kind the older batch formats carry plus the erase kind, so one class -/// mirrors a document transition of any batch format. #[derive(Clone)] #[wasm_bindgen(js_name = "DocumentTransition")] -pub struct DocumentTransitionWasm(DocumentTransitionV1); - -impl From for DocumentTransitionWasm { - fn from(transition: DocumentTransitionV1) -> Self { - DocumentTransitionWasm(transition) - } -} +pub struct DocumentTransitionWasm(DocumentTransition); impl From for DocumentTransitionWasm { fn from(transition: DocumentTransition) -> Self { - DocumentTransitionWasm(transition.into()) + DocumentTransitionWasm(transition) } } -impl From for DocumentTransitionV1 { +impl From for DocumentTransition { fn from(transition: DocumentTransitionWasm) -> Self { transition.0 } @@ -100,7 +90,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "createTransition")] pub fn create_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::Create(create) => Ok(DocumentCreateTransitionWasm::from(create)), + DocumentTransition::Create(create) => Ok(DocumentCreateTransitionWasm::from(create)), _ => Err(WasmDppError::invalid_argument( "Document transition is not a create transition", )), @@ -110,7 +100,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "replaceTransition")] pub fn replace_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::Replace(replace) => { + DocumentTransition::Replace(replace) => { Ok(DocumentReplaceTransitionWasm::from(replace)) } _ => Err(WasmDppError::invalid_argument( @@ -122,7 +112,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "deleteTransition")] pub fn delete_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::Delete(delete) => Ok(DocumentDeleteTransitionWasm::from(delete)), + DocumentTransition::Delete(delete) => Ok(DocumentDeleteTransitionWasm::from(delete)), _ => Err(WasmDppError::invalid_argument( "Document transition is not a delete transition", )), @@ -132,7 +122,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "purchaseTransition")] pub fn purchase_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::Purchase(purchase) => { + DocumentTransition::Purchase(purchase) => { Ok(DocumentPurchaseTransitionWasm::from(purchase)) } _ => Err(WasmDppError::invalid_argument( @@ -144,7 +134,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "transferTransition")] pub fn transfer_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::Transfer(transfer) => { + DocumentTransition::Transfer(transfer) => { Ok(DocumentTransferTransitionWasm::from(transfer)) } _ => Err(WasmDppError::invalid_argument( @@ -156,7 +146,7 @@ impl DocumentTransitionWasm { #[wasm_bindgen(getter = "updatePriceTransition")] pub fn update_price_transition(&self) -> WasmDppResult { match self.0.clone() { - DocumentTransitionV1::UpdatePrice(update_price) => { + DocumentTransition::UpdatePrice(update_price) => { Ok(DocumentUpdatePriceTransitionWasm::from(update_price)) } _ => Err(WasmDppError::invalid_argument( diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs index d03c05ff096..7a79a1e7e94 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs @@ -12,7 +12,7 @@ use crate::utils::{ try_vec_to_fixed_bytes, ToSerdeJSONExt, }; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods; use dpp::state_transition::batch_transition::DocumentCreateTransition; @@ -149,7 +149,7 @@ impl DocumentCreateTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs index c80c72a7e3c..22935a38988 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs @@ -7,7 +7,7 @@ use crate::state_transitions::batch::generators::generate_delete_transition; use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::DocumentDeleteTransition; use wasm_bindgen::prelude::wasm_bindgen; @@ -74,7 +74,7 @@ impl DocumentDeleteTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs index 7c520145e59..6fa0745c34b 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs @@ -9,7 +9,7 @@ use crate::utils::{try_from_options, try_from_options_optional, try_from_options use dpp::fee::Credits; use dpp::prelude::{IdentityNonce, Revision}; use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::batched_transition::DocumentPurchaseTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use wasm_bindgen::prelude::wasm_bindgen; @@ -114,7 +114,7 @@ impl DocumentPurchaseTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs index c55dd7e04db..ec9d4ace0a0 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs @@ -8,7 +8,7 @@ use crate::state_transitions::batch::generators::generate_replace_transition; use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64, ToSerdeJSONExt}; use dpp::prelude::{IdentityNonce, Revision}; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use dpp::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods; use dpp::state_transition::batch_transition::DocumentReplaceTransition; @@ -115,7 +115,7 @@ impl DocumentReplaceTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs index 79cb6a0dc20..a80a4ea85dd 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs @@ -9,7 +9,7 @@ use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::prelude::IdentityNonce; use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::batched_transition::DocumentTransferTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; use wasm_bindgen::prelude::wasm_bindgen; @@ -101,7 +101,7 @@ impl DocumentTransferTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs index 6051961bdd4..e60dd1350f8 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs @@ -8,7 +8,7 @@ use crate::state_transitions::batch::token_payment_info::TokenPaymentInfoWasm; use crate::utils::{try_from_options, try_from_options_optional, try_from_options_with, try_to_u64}; use dpp::fee::Credits; use dpp::prelude::IdentityNonce; -use dpp::state_transition::batch_transition::batched_transition::DocumentTransitionV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::DocumentUpdatePriceTransition; use dpp::state_transition::batch_transition::document_base_transition::document_base_transition_trait::DocumentBaseTransitionAccessors; @@ -94,7 +94,7 @@ impl DocumentUpdatePriceTransitionWasm { #[wasm_bindgen(js_name = "toDocumentTransition")] pub fn to_document_transition(&self) -> DocumentTransitionWasm { - let rs_transition = DocumentTransitionV1::from(self.0.clone()); + let rs_transition = DocumentTransition::from(self.0.clone()); DocumentTransitionWasm::from(rs_transition) } diff --git a/packages/wasm-sdk/src/state_transitions/broadcast.rs b/packages/wasm-sdk/src/state_transitions/broadcast.rs index 1ab01abc636..b2b2ca30d9e 100644 --- a/packages/wasm-sdk/src/state_transitions/broadcast.rs +++ b/packages/wasm-sdk/src/state_transitions/broadcast.rs @@ -7,7 +7,7 @@ use crate::error::WasmSdkError; use crate::sdk::WasmSdk; use crate::settings::{parse_put_settings, PutSettingsJs}; use dash_sdk::dpp::platform_value::Identifier; -use dash_sdk::dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV1; +use dash_sdk::dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; use dash_sdk::dpp::state_transition::StateTransition; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; @@ -22,7 +22,7 @@ use wasm_dpp2::StateTransitionWasm; fn referenced_contract_ids(state_transition: &StateTransition) -> BTreeSet { match state_transition { StateTransition::Batch(batch_transition) => batch_transition - .transitions_iter_v1() + .transitions_iter() .map(|transition| transition.data_contract_id()) .collect(), _ => BTreeSet::new(), From e04c0147771c70e3deafa4d86c1d5e7f26f077c8 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 20 Sep 2026 01:34:36 +0700 Subject: [PATCH 38/48] test(platform): pin the erase kind's wire byte and its refusal at protocol 14 Close the coverage gaps the rework's review found. The kind's admission is now pinned end to end: a signed format 1 batch with an erase submitted through `process_raw_state_transitions` at protocol 14 is refused as `UnpaidConsensusError(UnsupportedVersionError)`, and at the dpp level the same batch decodes under protocol 14 through the in-version deserializer, names itself `DocumentsBatch([Erase])`, and is refused only by basic structure, which also restores the format 1 active-range assertion. The appended bincode discriminant is pinned: an erase and a delete over the same base encode identically except for one byte, 7 against 2, in batch formats 0 and 1, and decode back to an erase. The batched shell's `$transition` JSON case for an erase is added alongside. The platform-version pins are made literal again: the released loop asserts eleven batch-level slots by value for protocols 12 to 14, and the protocol 15 parity tuple compares twelve, so a change to both tables at once still fails. The batch action credit sums are checked with an erase alongside purchases. Co-Authored-By: Claude Fable 5.1 --- packages/rs-dpp/src/state_transition/mod.rs | 80 ++++++++++++++++ .../batched_transition/mod.rs | 9 +- .../document/batch_transition/tests.rs | 75 ++++++++++++++- .../batch/tests/document/erase.rs | 92 +++++++++++++++++++ .../state_transition_action/batch/tests.rs | 20 ++++ .../tests/keep_history_delete_versions.rs | 73 ++++++++++++++- 6 files changed, 346 insertions(+), 3 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index cff847f8ecf..85900ccd24a 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -2562,6 +2562,86 @@ mod tests { })) } + fn sample_format_1_batch_st_with_erase() -> StateTransition { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::{ + BatchedTransition, DocumentEraseTransition, + }; + use crate::state_transition::batch_transition::BatchTransitionV1; + + let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 { + id: Identifier::from([1u8; 32]), + identity_contract_nonce: 3, + document_type_name: "note".to_string(), + data_contract_id: Identifier::from([2u8; 32]), + }); + let erase = + DocumentTransition::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base, + })); + StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::from([8u8; 32]), + transitions: vec![BatchedTransition::Document(erase)], + user_fee_increase: 2, + signature_public_key_id: 7, + signature: BinaryData::new(vec![0xEE; 65]), + })) + } + + /// The erase kind rides in the shipped batch formats, so a format 1 batch + /// carrying one keeps format 1's range and decodes under protocol 14 on + /// software that knows the kind. Agreement with software that cannot + /// decode it comes one step later: the basic-structure check refuses the + /// kind wherever the active version publishes no bounds for it. + #[cfg(all(feature = "state-transitions", feature = "validation"))] + #[test] + fn test_format_1_batch_with_erase_decodes_at_protocol_14_and_is_refused_by_structure() { + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + use crate::serialization::PlatformSerializable; + + let state_transition = sample_format_1_batch_st_with_erase(); + assert_eq!( + state_transition.active_version_range(), + 9..=LATEST_VERSION, + "format 1 arrived with tokens at protocol version 9; the erase kind does not move it" + ); + let bytes = PlatformSerializable::serialize_to_bytes(&state_transition) + .expect("serialize succeeds"); + + let released = PlatformVersion::get(14).expect("protocol version 14 exists"); + let decoded = + StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, released) + .expect("a format 1 batch with an erase decodes under protocol version 14"); + assert_eq!(decoded.name(), "DocumentsBatch([Erase])"); + let StateTransition::Batch(batch) = decoded else { + panic!("expected a batch, got {decoded:?}"); + }; + + let is_unsupported_version = |error: &ConsensusError| { + matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + ) + }; + let result = batch + .validate_base_structure(released) + .expect("no protocol err"); + assert!( + result.errors.iter().any(is_unsupported_version), + "protocol version 14 must refuse the erase kind, got {:?}", + result.errors + ); + let result = batch + .validate_base_structure(PlatformVersion::latest()) + .expect("no protocol err"); + assert!( + !result.errors.iter().any(is_unsupported_version), + "the latest protocol version must admit the erase kind, got {:?}", + result.errors + ); + } + fn sample_batch_st_empty() -> StateTransition { StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 { owner_id: Identifier::from([1u8; 32]), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index d2a0fa480d1..dda869a3e06 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -83,7 +83,7 @@ impl crate::serialization::ValueConvertible for BatchedTransition {} pub(crate) mod json_convertible_tests { use super::*; use crate::state_transition::batch_transition::batched_transition::{ - document_create_transition, token_burn_transition, + document_create_transition, document_erase_transition, token_burn_transition, }; use document_transition::DocumentTransition; use token_transition::TokenTransition; @@ -131,6 +131,13 @@ pub(crate) mod json_convertible_tests { assert_umbrella_round_trip(BatchedTransition::Document(inner), "document"); } + #[test] + fn umbrella_document_erase() { + let inner = + DocumentTransition::Erase(document_erase_transition::json_convertible_tests::fixture()); + assert_umbrella_round_trip(BatchedTransition::Document(inner), "document"); + } + #[test] fn umbrella_token() { let inner = TokenTransition::Burn(token_burn_transition::json_convertible_tests::fixture()); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs index c8011b7ec59..20dfaaf2e42 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs @@ -36,7 +36,7 @@ mod batch_transition_tests { use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{ - BatchTransitionV0, BatchTransitionV1, + BatchTransition, BatchTransitionV0, BatchTransitionV1, }; use crate::state_transition::StateTransitionLike; use crate::state_transition::StateTransition; @@ -1186,4 +1186,77 @@ mod batch_transition_tests { assert!(matches!(second, BatchedTransitionRef::Token(_))); assert!(iter.next().is_none()); } + + // ----------------------------------------------------------------------- + // The erase kind on the wire + // ----------------------------------------------------------------------- + + fn make_erase_transition(nonce: u64) -> DocumentTransition { + use crate::state_transition::batch_transition::batched_transition::document_erase_transition::DocumentEraseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::DocumentEraseTransition; + + DocumentTransition::Erase(DocumentEraseTransition::V0(DocumentEraseTransitionV0 { + base: make_base_transition(nonce), + })) + } + + /// The erase kind is appended after every shipped kind, so its bincode + /// discriminant is the next one, 7, and every earlier kind keeps its own. + /// Pinning the byte keeps a later variant reordering from silently + /// changing what old software reads. An erase and a delete over the same + /// base encode identically except for that one byte, in both shipped + /// batch formats, and the bytes decode back to an erase. + #[test] + fn should_encode_an_erase_with_the_appended_discriminant_in_both_shipped_formats() { + use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; + + let delete = make_delete_transition(1); + let erase = make_erase_transition(1); + + let format_0 = + |transition: DocumentTransition| BatchTransition::V0(make_batch_v0(vec![transition])); + let format_1 = |transition: DocumentTransition| { + BatchTransition::V1(make_batch_v1(vec![BatchedTransition::Document(transition)])) + }; + + for (make, format) in [ + ( + &format_0 as &dyn Fn(DocumentTransition) -> BatchTransition, + 0u8, + ), + (&format_1, 1u8), + ] { + let delete_bytes = make(delete.clone()) + .serialize_to_bytes() + .expect("serialize"); + let erase_batch = make(erase.clone()); + let erase_bytes = erase_batch.serialize_to_bytes().expect("serialize"); + + assert_eq!(erase_bytes[0], format, "the first byte is the batch format"); + assert_eq!(erase_bytes.len(), delete_bytes.len()); + let differing: Vec = erase_bytes + .iter() + .zip(&delete_bytes) + .enumerate() + .filter(|(_, (a, b))| a != b) + .map(|(index, _)| index) + .collect(); + assert_eq!( + differing.len(), + 1, + "an erase and a delete differ only in the kind discriminant" + ); + assert_eq!(erase_bytes[differing[0]], 7, "erase is the eighth kind"); + assert_eq!(delete_bytes[differing[0]], 2, "delete is the third kind"); + + let recovered = + BatchTransition::deserialize_from_bytes_untrusted(&erase_bytes).expect("decode"); + assert_eq!(recovered, erase_batch); + assert!(recovered + .first_transition() + .expect("one transition") + .as_transition_erase() + .is_some()); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs index 297548bef21..7e85f3a4d61 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/erase.rs @@ -1053,3 +1053,95 @@ async fn should_reject_a_replace_of_a_deleted_document() { DocumentLifecycleState::Deleted(_) ); } + +/// The erase kind is appended to the shipped batch, so a node that knows it +/// decodes a format 1 batch carrying one at protocol 14 as well as at 15. +/// What keeps such a node agreeing with a node that cannot decode the kind is +/// the basic-structure gate: at protocol 14, where no bounds are published for +/// the kind, the batch is refused as an unsupported version before any state +/// is read or any fee is charged. +#[tokio::test] +async fn should_refuse_an_erase_at_protocol_14_as_an_unsupported_version() { + let released = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert!( + released + .dpp + .state_transition_serialization_versions + .document_erase_state_transition + .is_none(), + "the test needs a released version that publishes no erase bounds" + ); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(released.protocol_version) + .build_with_mock_rpc() + .set_initial_state_structure(); + + // A deployed keep-history contract, loaded the way protocol 14 loads it. + let contract = json_document_to_contract( + "tests/supporting_files/contract/note/note-contract-keep-history-and-can-be-deleted.json", + false, + released, + ) + .expect("expected the keep-history note contract"); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + released, + ) + .expect("expected to apply the contract"); + let (owner, signer, key) = setup_identity(&mut platform, 1003, dash_to_credits!(1.0)); + + // The transition itself can only be built by software that knows the + // kind; a protocol 14 node receives the bytes from such software. + let latest = PlatformVersion::latest(); + let document_type = contract + .document_type_for_name("note") + .expect("expected the note document type"); + let mut rng = StdRng::seed_from_u64(1303); + let entropy = Bytes32::random_with_rng(&mut rng); + let document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + owner.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + latest, + ) + .expect("expected a random note"); + let erase = BatchTransition::new_document_erase_transition_from_document( + document, + document_type, + &key, + 1, + 0, + &signer, + latest, + None, + ) + .await + .expect("expected an erase transition"); + assert_matches!( + erase, + StateTransition::Batch(dpp::state_transition::batch_transition::BatchTransition::V1(_)), + "the erase rides in the shipped batch format" + ); + + let result = process( + &mut platform, + erase.serialize_to_bytes().expect("serialized"), + ); + + assert_matches!( + result, + StateTransitionExecutionResult::UnpaidConsensusError( + dpp::consensus::ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + ), + "protocol 14 must refuse the erase kind by its version bounds" + ); +} diff --git a/packages/rs-drive/src/state_transition_action/batch/tests.rs b/packages/rs-drive/src/state_transition_action/batch/tests.rs index 263b626d827..3038b804458 100644 --- a/packages/rs-drive/src/state_transition_action/batch/tests.rs +++ b/packages/rs-drive/src/state_transition_action/batch/tests.rs @@ -3124,5 +3124,25 @@ mod erase_action { None ); assert_eq!(erase_only.all_used_balances().unwrap(), None); + + let purchases = || { + vec![ + BatchedTransitionAction::DocumentAction(make_purchase().into()), + BatchedTransitionAction::TokenAction(make_direct_purchase().into()), + ] + }; + let without_erase = batch(purchases()); + let mut with_erase_transitions = vec![erase()]; + with_erase_transitions.extend(purchases()); + let with_erase = batch(with_erase_transitions); + assert_eq!( + with_erase.all_purchases_amount().unwrap(), + without_erase.all_purchases_amount().unwrap() + ); + assert_eq!(with_erase.all_purchases_amount().unwrap(), Some(15_000)); + assert_eq!( + with_erase.all_used_balances().unwrap(), + without_erase.all_used_balances().unwrap() + ); } } diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index a076b0e60e4..dd2eab92a5c 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -95,6 +95,70 @@ fn should_preserve_released_keep_history_validation_versions() { 0, "delete action conversion at protocol {protocol}" ); + // The batch-level slots the released versions replay with. Pinned + // literally so that a change to both protocol 14 and 15 at once still + // fails the protocol 15 comparison test below. + let batch_bounds = &version + .dpp + .state_transition_serialization_versions + .batch_state_transition; + assert_eq!( + ( + batch_bounds.min_version, + batch_bounds.max_version, + batch_bounds.default_current_version + ), + (0, 1, 1), + "batch wire bounds at protocol {protocol}" + ); + assert_eq!( + version + .dpp + .state_transitions + .documents + .documents_batch_transition + .validation + .validate_base_structure, + 0, + "batch structure validation at protocol {protocol}" + ); + let batch = &version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition; + assert_eq!( + ( + batch.basic_structure, + batch.advanced_structure, + batch.state, + batch.revision, + batch.transform_into_action, + batch.failed_per_transition_action, + batch.fetch_documents_for_transitions_knowing_contract_and_document_type, + batch.fetch_document_with_id, + batch.is_allowed, + batch.document_reference_validation, + batch.document_base_transition_state_validation, + ), + (0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0), + "batch structure, state, nonce, transform, failure, fetch and admission \ + generations at protocol {protocol}" + ); + assert_eq!( + version.drive.methods.prove.prove_state_transition, 0, + "state transition prover at protocol {protocol}" + ); + assert_eq!( + version + .drive + .methods + .verify + .state_transition + .verify_state_transition_was_executed_with_proof, + 0, + "state transition proof verification at protocol {protocol}" + ); } } @@ -247,18 +311,25 @@ fn should_carry_the_erase_kind_without_a_new_batch_generation() { .state_transitions .batch_state_transition; ( + batch.basic_structure, batch.transform_into_action, batch.advanced_structure, batch.state, batch.revision, + batch.failed_per_transition_action, batch.is_allowed, batch.fetch_documents_for_transitions_knowing_contract_and_document_type, + batch.fetch_document_with_id, + batch.data_triggers.bindings, + batch.document_reference_validation, + batch.document_base_transition_state_validation, ) }; assert_eq!( batch_generations(current), batch_generations(released), - "the batch transformer, structure, state, nonce and admission generations are unchanged" + "the batch transformer, structure, state, nonce, failure, fetch, trigger and \ + admission generations are unchanged" ); let batch_drive = |version: &PlatformVersion| { From 1e8c9d3fa2c5dd9bef0c16b5240e46b4573eee53 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 20 Sep 2026 22:59:26 +0700 Subject: [PATCH 39/48] fix(drive): bill the contract fetch of a lifecycle delete and version its estimator Three review findings on the keep-history lifecycle, none touching a released generation. The `DeleteDocumentWithLifecycle` operation resolved its contract into a local accumulator and returned only the delete's operations, so a caller naming the contract by id performed a charged fetch that never reached the operation pipeline. The delete's operations are now appended to the resolution's, as the erase operation already did; a test converts the same delete with a borrowed contract and with the contract id and pins that the by-id path adds exactly the fetch cost, first. The lifecycle-record estimator, shared by delete generation 1 and erase estimation generation 0, ran unversioned. It now sits behind `add_estimation_costs_for_lifecycle_record`, dispatched on an optional slot that is `None` in every released document method table and `Some(0)` at protocol 15, so a later change to its layer counts or record size cannot move the estimates of the generations that already rely on it. The slot joins the one-implementation-per-slot pin. wasm-sdk declared a structural `DocumentHistoryResult` interface with the same name as the class wasm-bindgen generates, and the two could not merge (TS2687 and TS2717 without `skipLibCheck`). The interface is gone; its documentation moved onto the generated getters, which the declarations carry. Co-Authored-By: Claude Fable 5.1 --- .../mod.rs | 60 +++++++++++++++++++ .../v0/mod.rs} | 4 +- .../drive/document/estimation_costs/mod.rs | 2 +- .../src/drive/document/lifecycle/tests.rs | 47 +++++++++++++++ .../src/util/batch/drive_op_batch/document.rs | 30 ++++++---- .../drive_document_method_versions/mod.rs | 4 ++ .../drive_document_method_versions/v1.rs | 1 + .../drive_document_method_versions/v2.rs | 1 + .../drive_document_method_versions/v3.rs | 1 + .../drive_document_method_versions/v4.rs | 1 + .../drive_document_method_versions/v5.rs | 1 + .../tests/keep_history_delete_versions.rs | 10 ++++ packages/wasm-sdk/src/queries/document.rs | 35 ++++------- 13 files changed, 159 insertions(+), 38 deletions(-) create mode 100644 packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/mod.rs rename packages/rs-drive/src/drive/document/estimation_costs/{add_estimation_costs_for_document_lifecycle.rs => add_estimation_costs_for_lifecycle_record/v0/mod.rs} (94%) diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/mod.rs new file mode 100644 index 00000000000..6d1fc26be2c --- /dev/null +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/mod.rs @@ -0,0 +1,60 @@ +mod v0; + +use std::collections::HashMap; + +use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::DataContract; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerInformation; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; + +impl Drive { + /// Registers the layers a lifecycle record write or removal touches. + /// + /// The record is written by a delete of a keep-history document and + /// removed by the terminal erase chunk, so both estimate through this one + /// entry point; its own version slot keeps a later change to the layer + /// counts or record size from moving the estimates of the generations + /// that already rely on it. + /// + /// # Parameters + /// * `contract`: The contract that contains the document type. + /// * `document_type`: The type whose lifecycle tree holds the record. + /// * `layers`: The estimated layer information the dry run builds. + /// * `platform_version`: The platform 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(crate) fn add_estimation_costs_for_lifecycle_record( + contract: &DataContract, + document_type: DocumentTypeRef, + layers: &mut HashMap, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .document + .delete + .add_estimation_costs_for_lifecycle_record + { + Some(0) => { + Self::add_estimation_costs_for_lifecycle_record_v0(contract, document_type, layers) + } + Some(version) => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "add_estimation_costs_for_lifecycle_record".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Drive(DriveError::VersionNotActive { + method: "add_estimation_costs_for_lifecycle_record".to_string(), + known_versions: vec![0], + })), + } + } +} diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs similarity index 94% rename from packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs rename to packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs index b39ca03665a..7012b4e4e12 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_document_lifecycle.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs @@ -4,7 +4,6 @@ use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; -use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::EstimatedLayerCount::{ApproximateElements, PotentiallyAtMaxElements}; use grovedb::EstimatedLayerInformation; @@ -25,11 +24,10 @@ impl Drive { /// /// A dry run that leaves either layer unregistered fails outright, so both /// are always registered even when the tree already exists. - pub(crate) fn add_estimation_costs_for_lifecycle_record( + pub(super) fn add_estimation_costs_for_lifecycle_record_v0( contract: &DataContract, document_type: DocumentTypeRef, layers: &mut HashMap, - _platform_version: &PlatformVersion, ) -> Result<(), Error> { let flags_size = Some(StorageFlags::approximate_size(true, None)); layers.insert( diff --git a/packages/rs-drive/src/drive/document/estimation_costs/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/mod.rs index f717c8c5da8..d8e89847f8a 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/mod.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/mod.rs @@ -4,7 +4,7 @@ mod add_estimation_costs_for_add_document_to_primary_storage; mod add_estimation_costs_for_add_contested_document_to_primary_storage; -mod add_estimation_costs_for_document_lifecycle; +mod add_estimation_costs_for_lifecycle_record; mod add_estimation_costs_for_erase_document; diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index 68e027fd7de..ab5b92a82bc 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -1895,3 +1895,50 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { assert!(page.entries.is_empty()); } } + +/// A lifecycle delete given only the contract's id has to fetch the contract, +/// and that fetch is billed: its operation must come back with the delete's +/// operations rather than be dropped on the way out, as it is for an erase. +#[test] +fn should_keep_the_contract_fetch_cost_of_a_lifecycle_delete_by_contract_id() { + use crate::fees::op::LowLevelDriveOperation; + use crate::util::batch::drive_op_batch::DriveLowLevelOperationConverter; + use crate::util::batch::DocumentOperationType; + use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; + + let owner = [48u8; 32]; + let version = latest(); + let (drive, contract, id) = setup_history(1, owner); + let block_info = BlockInfo::default_with_time(5_000); + + let delete_with = |contract_info: DataContractInfo| { + DocumentOperationType::DeleteDocumentWithLifecycle { + document_id: id, + deleter_id: Some(Identifier::new(owner)), + contract_info, + document_type_info: DocumentTypeInfo::DocumentTypeName("person".to_string()), + } + .into_low_level_drive_operations(&drive, &mut None, &block_info, None, version) + .expect("expected the delete to convert") + }; + + // The delete bills its own reads too, so the comparison is against the + // borrowed-contract path, which performs no fetch. + let borrowed = delete_with(DataContractInfo::BorrowedDataContract(&contract)); + assert!(!borrowed.is_empty(), "the delete itself emits operations"); + + let by_id = delete_with(DataContractInfo::DataContractId(contract.id())); + assert!( + matches!( + by_id.first(), + Some(LowLevelDriveOperation::PreCalculatedFeeResult(_)) + ), + "the contract fetch is billed first, got {:?}", + by_id.first() + ); + assert_eq!( + by_id[1..], + borrowed[..], + "after the fetch cost, the by-id path emits exactly the delete's operations" + ); +} 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 5fde1093077..d82ec5b50a8 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 @@ -496,18 +496,24 @@ impl DocumentOperationType<'_> { let contract = contract_resolved_info.as_ref(); let document_type = document_type_info.resolve(contract)?; - drive.delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( - document_id, - contract, - document_type, - block_info, - deleter_id, - None, - estimated_costs_only_with_layer_info, - block_info.time_ms, - transaction, - platform_version, - ) + // The contract resolution above may have billed a fetch; keep + // its operations ahead of the delete's so the caller pays for + // both. + let mut operations = drive + .delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + document_id, + contract, + document_type, + block_info, + deleter_id, + None, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + platform_version, + )?; + drive_operations.append(&mut operations); + Ok(drive_operations) } DocumentOperationType::EraseDocument { document_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 23a05aec606..68f5d935f6a 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 @@ -166,6 +166,10 @@ pub struct DriveDocumentDeleteMethodVersions { /// Estimation layers for the erase chunk. Same dormancy as /// `erase_document_for_contract_operations`. pub add_estimation_costs_for_erase_document: OptionalFeatureVersion, + /// Estimation layers for the lifecycle record a delete writes and an + /// erase removes, shared by both. Same dormancy as + /// `erase_document_for_contract_operations`. + pub add_estimation_costs_for_lifecycle_record: OptionalFeatureVersion, } #[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 63d9e9fa116..c096be8aa91 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 @@ -44,6 +44,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = delete_index_only_document_for_contract: 0, erase_document_for_contract_operations: None, add_estimation_costs_for_erase_document: None, + add_estimation_costs_for_lifecycle_record: None, }, 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 14e5a08796c..8dadedddfae 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 @@ -46,6 +46,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = delete_index_only_document_for_contract: 0, erase_document_for_contract_operations: None, add_estimation_costs_for_erase_document: None, + add_estimation_costs_for_lifecycle_record: None, }, 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 462b1adb210..00dd6076149 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 @@ -61,6 +61,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = delete_index_only_document_for_contract: 0, erase_document_for_contract_operations: None, add_estimation_costs_for_erase_document: None, + add_estimation_costs_for_lifecycle_record: None, }, 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 0a1878137bc..9d448c789ab 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 @@ -111,6 +111,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = delete_index_only_document_for_contract: 0, erase_document_for_contract_operations: None, add_estimation_costs_for_erase_document: None, + add_estimation_costs_for_lifecycle_record: None, }, 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 index ddff0075319..4f3ff5d9972 100644 --- 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 @@ -35,6 +35,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = delete_document_for_contract_operations: 1, erase_document_for_contract_operations: Some(0), add_estimation_costs_for_erase_document: Some(0), + add_estimation_costs_for_lifecycle_record: Some(0), ..DRIVE_DOCUMENT_METHOD_VERSIONS_V4.delete }, insert: DriveDocumentInsertMethodVersions { diff --git a/packages/rs-platform-version/tests/keep_history_delete_versions.rs b/packages/rs-platform-version/tests/keep_history_delete_versions.rs index dd2eab92a5c..867581b70e1 100644 --- a/packages/rs-platform-version/tests/keep_history_delete_versions.rs +++ b/packages/rs-platform-version/tests/keep_history_delete_versions.rs @@ -395,6 +395,16 @@ fn should_expose_one_implementation_of_each_new_lifecycle_slot() { expected_erase_version, "erase estimation at protocol {protocol}" ); + assert_eq!( + version + .drive + .methods + .document + .delete + .add_estimation_costs_for_lifecycle_record, + expected_erase_version, + "lifecycle record estimation at protocol {protocol}" + ); assert_eq!( version .drive diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index fbd104993ea..137f39904a5 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -191,27 +191,6 @@ export interface DocumentHistoryQuery { /** Maximum ten entries. A single revision requires limit one. */ limit?: number; } -export interface DocumentHistoryResult { - entries: { timeMs: bigint; revision: bigint; document: Document }[]; - /** Missing when the legacy storage generation cannot authenticate lifecycle metadata. */ - lifecycle?: { - /** - * ACTIVE while the document is visible to ordinary reads, DELETED once it - * has been deleted and its revisions are retained, ERASING once an - * authorized erasure has begun, ABSENT when nothing is left. - */ - state: "ACTIVE" | "DELETED" | "ERASING" | "ABSENT"; - remainingRevisions: bigint; - /** Zero unless the document has been deleted. */ - deletedAtMs: bigint; - /** Zero unless an authorized erasure has begun. */ - erasingStartedAtMs: bigint; - /** Timestamp of the newest revision retained when the erasure began. */ - erasingFromTimeMs: bigint; - /** History sequence of that revision. */ - erasingFromRevision: bigint; - }; -} "#; #[wasm_bindgen] @@ -438,7 +417,13 @@ struct DocumentHistoryLifecycleSerde { #[wasm_bindgen(js_class = DocumentHistoryLifecycle)] impl DocumentHistoryLifecycleWasm { - #[wasm_bindgen(getter)] + /// `ACTIVE` while the document is visible to ordinary reads, `DELETED` + /// once it has been deleted and its revisions are retained, `ERASING` once + /// an authorized erasure has begun, `ABSENT` when nothing is left. + #[wasm_bindgen( + getter, + unchecked_return_type = "\"ACTIVE\" | \"DELETED\" | \"ERASING\" | \"ABSENT\"" + )] pub fn state(&self) -> String { self.state.clone() } @@ -448,21 +433,25 @@ impl DocumentHistoryLifecycleWasm { BigInt::from(self.remaining_revisions) } + /// Zero unless the document has been deleted. #[wasm_bindgen(getter = "deletedAtMs")] pub fn deleted_at_ms(&self) -> BigInt { BigInt::from(self.deleted_at_ms) } + /// Zero unless an authorized erasure has begun. #[wasm_bindgen(getter = "erasingStartedAtMs")] pub fn erasing_started_at_ms(&self) -> BigInt { BigInt::from(self.erasing_started_at_ms) } + /// Timestamp of the newest revision retained when the erasure began. #[wasm_bindgen(getter = "erasingFromTimeMs")] pub fn erasing_from_time_ms(&self) -> BigInt { BigInt::from(self.erasing_from_time_ms) } + /// History sequence of the newest revision retained when the erasure began. #[wasm_bindgen(getter = "erasingFromRevision")] pub fn erasing_from_revision(&self) -> BigInt { BigInt::from(self.erasing_from_revision) @@ -509,6 +498,8 @@ impl DocumentHistoryResultWasm { self.entries.clone() } + /// Missing when the legacy storage generation cannot authenticate lifecycle + /// metadata. #[wasm_bindgen(getter)] pub fn lifecycle(&self) -> Option { self.lifecycle.clone() From 9c8532dee7f7e3f6dff048eb88e28d03f771563c Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 21 Sep 2026 18:28:30 +0700 Subject: [PATCH 40/48] fix(drive): refuse to erase a document that is not deleted The erase operation read the lifecycle record only on a non-terminal chunk, where it needed the record's fields. A terminal chunk emitted its revision deletes without ever establishing that the document was deleted, so a caller of the public Drive method could strip an active document's retained history: a document that was never deleted, or one whose id was reused after an earlier erasure finished, both kept the per-type lifecycle tree the terminal path relied on. Transition validation refuses both before they reach Drive, but the method and the `EraseDocument` operation do not pass through it. The record is now read before anything is emitted, on every chunk. Its presence is what proves the document may lose revisions: it exists exactly while a document is deleted or erasing, and the insert generation of this protocol version refuses to create over a deleted id, so a record and a current pointer cannot coexist. A missing record, including a type that has no lifecycle tree at all, refuses the erase as invalid input. The admission estimate already priced this read on every chunk. A test erases a document that was never deleted and one recreated under a fully erased id; both are refused with every revision intact. The history query test pins that a proved response carries no lifecycle fields on the wire, so nothing a client reads there can stand in for what the proof derives. Co-Authored-By: Claude Fable 5.1 --- .../src/query/document_history/v1/tests.rs | 7 ++ .../v0/mod.rs | 32 +++-- .../src/drive/document/lifecycle/tests.rs | 109 +++++++++++++++++- 3 files changed, 132 insertions(+), 16 deletions(-) diff --git a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs index 4cca756061d..be888965b68 100644 --- a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs @@ -651,6 +651,13 @@ fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { .unwrap() .into_data() .unwrap(); + // A proved response carries the proof and nothing else: there are no + // lifecycle fields on the wire for a client to trust, so every value + // the verifier returns below is derived from the proof itself. + assert!( + matches!(response.result, Some(ResponseResult::Proof(_))), + "{stage}: a proved response must not carry lifecycle claims" + ); let verify = |response: GetDocumentHistoryResponseV0| { DocumentHistory::maybe_from_proof( request.clone(), diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs index 1457c520835..054996b7956 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -112,6 +112,19 @@ impl Drive { ); } + // The record exists exactly while a document is deleted or erasing, so + // its presence is what proves the document may lose revisions at all. + // It is read before anything is emitted: a caller that reaches this + // without a delete, or after an erasure has already finished and the + // id was reused, would otherwise strip an active document's history. + let (record, flags) = self.fetch_lifecycle_record_with_flags( + document_id, + &lifecycle_path, + &mut batch_operations, + transaction, + platform_version, + )?; + let removable = self.enumerate_newest_revision_keys( &history_path, chunk, @@ -179,13 +192,6 @@ impl Drive { &platform_version.drive, )?; } else { - let (record, flags) = self.fetch_lifecycle_record_with_flags( - document_id, - &lifecycle_path, - &mut batch_operations, - transaction, - platform_version, - )?; if record.is_erasing() { // A continuation: the record already carries the erasure this // chunk is finishing, and nothing but an authorized first chunk @@ -264,8 +270,10 @@ impl Drive { transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(DocumentLifecycleRecord, Option>), Error> { + // A type nothing has been deleted under has no lifecycle tree at all; + // that reads as no record, the same as an id without one. let element = self - .grove_get_raw( + .grove_get_raw_optional( lifecycle_path.into(), document_id.as_slice(), DirectQueryType::StatefulDirectQuery, @@ -273,8 +281,8 @@ impl Drive { batch_operations, &platform_version.drive, )? - .ok_or(Error::Drive(DriveError::CorruptedDriveState( - "a document being erased has no lifecycle record".to_string(), + .ok_or(Error::Drive(DriveError::InvalidInput( + "only a deleted document can be erased: it has no lifecycle record".to_string(), )))?; let Element::Item(bytes, flags) = element else { return Err(Error::Drive(DriveError::CorruptedElementType( @@ -313,7 +321,7 @@ impl Drive { /// The enumeration is priced at one revision more than a chunk removes, /// because that is what it reads, and at the full body size, because the /// query surface returns bodies whether or not the caller wants them. The - /// record read a non-terminal chunk performs is priced too. + /// record read every chunk performs first is priced too. #[allow(clippy::too_many_arguments)] fn estimated_erase_document_operations( &self, @@ -345,7 +353,7 @@ impl Drive { )?; } - // A non-terminal chunk reads the record before overwriting it. + // Every chunk reads the record first; a non-terminal one overwrites it. self.grove_get_raw( lifecycle_path.into(), document_id.as_slice(), diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index ab5b92a82bc..f6e75461b5f 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -672,10 +672,9 @@ fn should_reject_an_erase_of_a_document_that_retains_nothing() { latest(), ) .expect_err("there is nothing left to erase"); - assert!(matches!( - error, - Error::Drive(DriveError::CorruptedDriveState(_)) - )); + // The terminal chunk removed the record along with the revisions, so the + // id reads as never deleted and the refusal is the not-deleted one. + assert!(matches!(error, Error::Drive(DriveError::InvalidInput(_)))); } /// The two record shapes must occupy the same number of bytes: an erase start @@ -1942,3 +1941,105 @@ fn should_keep_the_contract_fetch_cost_of_a_lifecycle_delete_by_contract_id() { "after the fetch cost, the by-id path emits exactly the delete's operations" ); } + +/// The erase operation is a public Drive method, reachable without the +/// transition validation that checks the lifecycle. It has to refuse a +/// document that is not deleted on its own: a document that was never deleted, +/// and one whose id was reused after an earlier erasure finished, both keep +/// every revision. +#[test] +fn should_refuse_to_erase_a_document_that_is_not_deleted() { + let owner = [49u8; 32]; + let version = latest(); + let (drive, contract, id) = setup_history(2, owner); + let document_type = document_type_of(&contract); + let block_info = BlockInfo::default_with_time(5_000); + let attempt = |drive: &Drive| { + drive.erase_document_for_contract_operations( + id, + &contract, + document_type, + &block_info, + &mut None, + None, + version, + ) + }; + let revisions_retained = |drive: &Drive| { + drive + .fetch_document_history( + &DocumentHistoryDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".into(), + document_id: id.to_buffer(), + filter: DocumentHistoryFilter::StartAtTime(0), + limit: Some(10), + }, + document_type, + None, + version, + ) + .expect("expected to read the history") + .entries + .len() + }; + + // Never deleted. + let error = attempt(&drive).expect_err("an active document must not be erasable"); + assert!( + matches!(error, Error::Drive(DriveError::InvalidInput(_))), + "got {error:?}" + ); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Active(_) + )); + assert_eq!(revisions_retained(&drive), 2); + + // Deleted, fully erased, then the id reused by a fresh document: the + // earlier erasure left the per-type lifecycle container behind, and that + // must not stand in for a record of this document. + delete(&drive, &contract, id, Identifier::new(owner), 6_000); + erase(&drive, &contract, id, 7_000); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Absent + )); + let mut recreated = + json_document_to_document(PERSON, Some(owner.into()), document_type, version) + .expect("expected a person document"); + recreated.set_revision(Some(1)); + assert_eq!(recreated.id(), id, "the fixture reproduces the same id"); + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo(( + &recreated, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(owner)))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default_with_time(8_000), + true, + None, + version, + None, + ) + .expect("expected the id to be free again"); + + let error = attempt(&drive).expect_err("a recreated document must not be erasable"); + assert!( + matches!(error, Error::Drive(DriveError::InvalidInput(_))), + "got {error:?}" + ); + assert!(matches!( + lifecycle_of(&drive, &contract, id), + DocumentLifecycleState::Active(_) + )); + assert_eq!(revisions_retained(&drive), 1); +} From e659eb173241651b404803a8dea97ecc1eeb742b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 21 Sep 2026 21:44:39 +0700 Subject: [PATCH 41/48] refactor(dpp): have document type update validation v2 delegate to v1 Generation 2 repeated generation 1's pipeline with one option computed differently. It now performs its two lifecycle checks and calls generation 1 for everything else: `canBeErased` stays immutable, and an erasable type is refused before it can withdraw deletion, which is the one input on which generation 1's own option would have decided otherwise. Every other input reaches generation 1 with the options it always computed. Co-Authored-By: Claude Fable 5.1 --- .../methods/validate_update/v2/mod.rs | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs index 1c881301131..3e196e689d5 100644 --- a/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v2/mod.rs @@ -12,9 +12,10 @@ //! itself immutable, so an erasable type that stopped allowing deletion could //! never again reach a state erase acts on. //! -//! Every other check is v1's: index definitions compared by name, byte array -//! encodings frozen, top-level requiredness frozen except for `requiredSince` -//! additions, and schema compatibility. +//! Every other check is v1's, which v2 delegates to once the two lifecycle +//! rules above have passed. The one case where v1 alone would decide +//! differently, an erasable type withdrawing deletion, is refused here before +//! v1 sees it, so v1's option computation stays what it was. use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError; use crate::data_contract::document_type::accessors::{ @@ -25,8 +26,6 @@ use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; use platform_version::version::PlatformVersion; -use super::common::UpdateValidationOptions; - impl DocumentTypeRef<'_> { #[inline(always)] pub(super) fn validate_update_v2( @@ -50,37 +49,21 @@ impl DocumentTypeRef<'_> { )); } - let options = UpdateValidationOptions { - allow_history_delete_repair: self.documents_keep_history() - && new_document_type.documents_keep_history() - && self.documents_can_be_deleted() - && !new_document_type.documents_can_be_deleted() - && !self.documents_can_be_erased(), - }; - let result = self.validate_config_with_options(new_document_type, &options); - - if !result.is_valid() { - return Ok(result); - } - - let result = self.validate_index_definitions_unchanged(new_document_type); - - if !result.is_valid() { - return Ok(result); - } - - let result = self.validate_byte_array_encoding_stability(new_document_type); - - if !result.is_valid() { - return Ok(result); - } - - let result = self.validate_required_fields_update(new_document_type, new_contract_version); - - if !result.is_valid() { - return Ok(result); + if self.documents_can_be_erased() + && self.documents_can_be_deleted() + && !new_document_type.documents_can_be_deleted() + { + return Ok(SimpleConsensusValidationResult::new_with_error( + DocumentTypeUpdateError::new( + self.data_contract_id(), + self.name(), + "document type whose documents can be erased can not stop allowing deletion" + .to_string(), + ) + .into(), + )); } - self.validate_schema_with_options(new_document_type, platform_version, &options) + self.validate_update_v1(new_document_type, new_contract_version, platform_version) } } From 8faa6c19e1a7715c88bf3a6d9717971b7be679ff Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 01:13:25 +0700 Subject: [PATCH 42/48] refactor(drive-abci): report the lifecycle states from the one history query handler The document history query handler was introduced with the protocol 15 storage layout, so the generation that reports the deleted and erasing states with their times is that same handler, not a second one beside it. The copy is removed, its one distinct test moves to the handler's tests, and the `document_history_processing` slot and the query versions table that existed only to select the copy go with it. Before protocol 15 the lifecycle read yields only the active and absent states, so the handler answers identically there. Co-Authored-By: Claude Fable 5.1 --- .../src/query/document_history/mod.rs | 23 +- .../src/query/document_history/v0/mod.rs | 10 +- .../src/query/document_history/v0/tests.rs | 196 +++++ .../src/query/document_history/v1/mod.rs | 163 ----- .../src/query/document_history/v1/tests.rs | 690 ------------------ .../drive_abci_query_versions/mod.rs | 3 - .../drive_abci_query_versions/v0.rs | 1 - .../drive_abci_query_versions/v1.rs | 1 - .../drive_abci_query_versions/v3.rs | 1 - .../drive_abci_query_versions/v4.rs | 13 - .../src/version/mocks/v2_test.rs | 1 - .../rs-platform-version/src/version/v15.rs | 2 - 12 files changed, 204 insertions(+), 900 deletions(-) delete mode 100644 packages/rs-drive-abci/src/query/document_history/v1/mod.rs delete mode 100644 packages/rs-drive-abci/src/query/document_history/v1/tests.rs delete mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs diff --git a/packages/rs-drive-abci/src/query/document_history/mod.rs b/packages/rs-drive-abci/src/query/document_history/mod.rs index 6ff4612d4c8..0e6fd2d30ee 100644 --- a/packages/rs-drive-abci/src/query/document_history/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/mod.rs @@ -1,4 +1,3 @@ -use crate::error::execution::ExecutionError; use crate::error::query::QueryError; use crate::error::Error; use crate::platform_types::platform::Platform; @@ -10,7 +9,6 @@ use dapi_grpc::platform::v0::{GetDocumentHistoryRequest, GetDocumentHistoryRespo use dpp::version::PlatformVersion; mod v0; -mod v1; impl Platform { /// Querying of a document history. @@ -45,25 +43,8 @@ impl Platform { match version { RequestVersion::V0(request) => { - let result = match platform_version - .drive_abci - .query - .document_history_processing - { - 0 => { - self.query_document_history_v0(request, platform_state, platform_version)? - } - 1 => { - self.query_document_history_v1(request, platform_state, platform_version)? - } - processing_version => { - return Err(Error::Execution(ExecutionError::UnknownVersionMismatch { - method: "query_document_history".to_string(), - known_versions: vec![0, 1], - received: processing_version, - })); - } - }; + let result = + self.query_document_history_v0(request, platform_state, platform_version)?; Ok(result.map(|response| GetDocumentHistoryResponse { version: Some(ResponseVersion::V0(response)), })) diff --git a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs index b27b010d982..fb5ec9dd51d 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/mod.rs @@ -138,13 +138,15 @@ impl Platform { lifecycle: history.lifecycle.map(|lifecycle| Lifecycle { state: match lifecycle.state { DocumentHistoryState::Active => State::Active, + DocumentHistoryState::Deleted => State::Deleted, + DocumentHistoryState::Erasing => State::Erasing, DocumentHistoryState::Absent => State::Absent, - DocumentHistoryState::Deleted | DocumentHistoryState::Erasing => { - State::Absent - } } as i32, remaining_revisions: lifecycle.remaining_revisions, - ..Default::default() + deleted_at_ms: lifecycle.times.deleted_at_ms, + erasing_started_at_ms: lifecycle.times.erasing_started_at_ms, + erasing_from_time_ms: lifecycle.times.erasing_from_time_ms, + erasing_from_revision: lifecycle.times.erasing_from_revision, }), }) }; diff --git a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs index 5069d151b0f..1eea86eb159 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs @@ -579,3 +579,199 @@ fn should_reject_missing_selectors_before_reading_state() { .unwrap() .is_valid()); } + +#[test] +fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { + use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; + + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + let mut state = state.as_ref().clone(); + let contract = json_document_to_contract( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json" + ), + false, + version, + ) + .unwrap(); + platform + .drive + .apply_contract(&contract, BlockInfo::default(), true, None, None, version) + .unwrap(); + let document_type = contract.document_type_for_name("profile").unwrap(); + let owner = Identifier::from([8; 32]); + let mut document = json_document_to_document( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" + ), + Some(owner), + document_type, + version, + ) + .unwrap(); + let chunk = version + .system_limits + .max_document_revisions_erased_per_transition + .expect("protocol 15 bounds the erase chunk") as u64; + for revision in 1..=chunk + 2 { + document.set_revision(Some(revision)); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentInfo::DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &contract, + document_type, + }, + revision > 1, + BlockInfo::default_with_time(1_000 + revision), + true, + None, + version, + None, + ) + .unwrap(); + } + + let key = SecretKey::::from_hash(b"document-history-quorum"); + let provider = Provider { + contract: Arc::new(contract.clone()), + key: key.public_key().to_bytes().try_into().unwrap(), + }; + state.last_committed_block_info = Some( + dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { + basic_info: BlockInfo { + height: 42, + core_height: 12, + time_ms: 3_000, + epoch: Default::default(), + }, + app_hash: [0; 32], + quorum_hash: [9; 32], + block_id_hash: [7; 32], + proposer_pro_tx_hash: [0; 32], + signature: [0; 96], + round: 0, + } + .into(), + ); + let wire_request = GetDocumentHistoryRequestV0 { + data_contract_id: contract.id().to_vec(), + document_type_name: "profile".into(), + document_id: document.id().to_vec(), + limit: None, + prove: true, + filter: Some(Filter::StartAtMs(0)), + }; + let request: GetDocumentHistoryRequest = wire_request.clone().into(); + + for (stage, expected_state) in [ + ("deleted", DocumentHistoryState::Deleted), + ("erasing", DocumentHistoryState::Erasing), + ] { + let operations = if stage == "deleted" { + platform + .drive + .delete_document_for_contract_operations_with_lifecycle( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(5_000), + Some(owner), + None, + &mut None, + 5_000, + None, + version, + ) + .unwrap() + } else { + platform + .drive + .erase_document_for_contract_operations( + document.id(), + &contract, + document_type, + &BlockInfo::default_with_time(6_000), + &mut None, + None, + version, + ) + .unwrap() + }; + platform + .drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &version.drive, + ) + .unwrap(); + + let root = platform + .drive + .grove + .root_hash(None, &version.drive.grove_version) + .value + .unwrap(); + let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); + let signed = signed_proof( + vec![], + root, + &metadata, + &key, + platform.config.validator_set.quorum_type as u32, + ); + let committed = state.last_committed_block_info.as_mut().unwrap(); + committed.set_app_hash(root); + committed.set_signature(signed.signature.try_into().unwrap()); + + let response = platform + .query_document_history_v0(wire_request.clone(), &state, version) + .unwrap() + .into_data() + .unwrap(); + // A proved response carries the proof and nothing else: there are no + // lifecycle fields on the wire for a client to trust, so every value + // the verifier returns below is derived from the proof itself. + assert!( + matches!(response.result, Some(ResponseResult::Proof(_))), + "{stage}: a proved response must not carry lifecycle claims" + ); + let verify = |response: GetDocumentHistoryResponseV0| { + DocumentHistory::maybe_from_proof( + request.clone(), + GetDocumentHistoryResponse::from(response), + Network::Testnet, + version, + &provider, + ) + }; + let history = verify(response.clone()) + .unwrap_or_else(|error| panic!("{stage}: honest response must verify: {error}")) + .expect("the proof authenticates a history"); + let lifecycle = history + .lifecycle + .expect("the proof authenticates the lifecycle"); + assert_eq!(lifecycle.state, expected_state, "{stage}"); + assert_eq!(lifecycle.times.deleted_at_ms, 5_000, "{stage}"); + + let mut tampered = response; + let proof = proof_mut(&mut tampered); + let mut envelope = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); + let middle = envelope.metadata_proof.len() / 2; + envelope.metadata_proof[middle] ^= 1; + proof.grovedb_proof = envelope.to_bytes().unwrap(); + assert!( + verify(tampered).is_err(), + "{stage}: changing the proof that authenticates lifecycle must be rejected" + ); + } +} diff --git a/packages/rs-drive-abci/src/query/document_history/v1/mod.rs b/packages/rs-drive-abci/src/query/document_history/v1/mod.rs deleted file mode 100644 index aeb2ed0a571..00000000000 --- a/packages/rs-drive-abci/src/query/document_history/v1/mod.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::error::{query::QueryError, Error}; -use crate::platform_types::{platform::Platform, platform_state::PlatformState}; -use crate::query::{response_metadata::CheckpointUsed, QueryValidationResult}; -use dapi_grpc::platform::v0::get_document_history_request::{ - get_document_history_request_v0::Filter, GetDocumentHistoryRequestV0, -}; -use dapi_grpc::platform::v0::get_document_history_response::{ - get_document_history_response_v0::{ - lifecycle::State, Entry, History, Lifecycle, Result as ResponseResult, - }, - GetDocumentHistoryResponseV0, -}; -use dpp::check_validation_result_with_data; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; -use dpp::validation::ValidationResult; -use dpp::version::PlatformVersion; -use drive::query::document_history_drive_query::{ - DocumentHistoryDriveQuery, DocumentHistoryFilter, DocumentHistoryState, -}; -use drive::util::grove_operations::GroveDBToUse; - -impl Platform { - pub(super) fn query_document_history_v1( - &self, - request: GetDocumentHistoryRequestV0, - platform_state: &PlatformState, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let contract_id = - check_validation_result_with_data!(request.data_contract_id.try_into().map_err(|_| { - QueryError::InvalidArgument("data_contract_id must be 32 bytes".to_owned()) - })); - let document_id = check_validation_result_with_data!(request - .document_id - .try_into() - .map_err(|_| QueryError::InvalidArgument("document_id must be 32 bytes".to_owned()))); - let filter = check_validation_result_with_data!(request.filter.ok_or_else(|| { - QueryError::InvalidArgument("exactly one history filter is required".to_owned()) - })); - let filter = match filter { - Filter::StartAtMs(time) => DocumentHistoryFilter::StartAtTime(time), - Filter::StartAfter(cursor) => DocumentHistoryFilter::StartAfter { - time_ms: cursor.time_ms, - revision: cursor.revision, - }, - Filter::StartAtRevision(revision) => DocumentHistoryFilter::StartAtRevision(revision), - Filter::Revision(revision) => DocumentHistoryFilter::Revision(revision), - }; - let limit = check_validation_result_with_data!(request - .limit - .map(u16::try_from) - .transpose() - .map_err(|_| QueryError::InvalidArgument("history limit out of bounds".to_owned()))); - let query = DocumentHistoryDriveQuery { - contract_id, - document_type_name: request.document_type_name, - document_id, - filter, - limit, - }; - check_validation_result_with_data!(query - .validate() - .map_err(|error| QueryError::InvalidArgument(error.to_string()))); - let fetched = self - .drive - .fetch_contract(contract_id, None, None, None, platform_version) - .value?; - let fetched = check_validation_result_with_data!( - fetched.ok_or_else(|| QueryError::NotFound("data contract not found".to_owned())) - ); - let contract = &fetched.contract; - let document_type = check_validation_result_with_data!(contract - .document_type_for_name(&query.document_type_name) - .map_err(|_| QueryError::NotFound("document type not found".to_owned()))); - use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; - if !document_type.documents_keep_history() { - return Ok(QueryValidationResult::new_with_error( - QueryError::InvalidArgument("document type does not keep history".to_owned()), - )); - } - // A query Drive refuses (a revision filter over a gapped history, for - // one) is the caller's mistake and answers as an invalid argument; - // storage and corruption errors still propagate. - let result = if request.prove { - let proof = match self.drive.prove_document_history( - &query, - document_type, - None, - platform_version, - ) { - Ok(proved) => proved, - Err(drive::error::Error::Query(query_error)) => { - return Ok(QueryValidationResult::new_with_error(QueryError::Query( - query_error, - ))); - } - Err(error) => return Err(error.into()), - }; - // The proof travels in the layout the protocol version stores, - // signed once; from protocol version 15 that is one proof object - // carrying both GroveDB proofs. - let proof = self - .response_proof_v0(platform_state, proof, GroveDBToUse::Current)? - .1; - ResponseResult::Proof(proof) - } else { - let history = match self.drive.fetch_document_history( - &query, - document_type, - None, - platform_version, - ) { - Ok(history) => history, - Err(drive::error::Error::Query(query_error)) => { - return Ok(QueryValidationResult::new_with_error(QueryError::Query( - query_error, - ))); - } - Err(error) => return Err(error.into()), - }; - let entries = history - .entries - .into_iter() - .map(|entry| { - Ok(Entry { - time_ms: entry.time_ms, - revision: entry.revision, - document: entry - .document - .serialize(document_type, contract, platform_version) - .map_err(Error::Protocol)?, - }) - }) - .collect::, Error>>()?; - ResponseResult::History(History { - entries, - lifecycle: history.lifecycle.map(|lifecycle| Lifecycle { - state: match lifecycle.state { - DocumentHistoryState::Active => State::Active, - DocumentHistoryState::Deleted => State::Deleted, - DocumentHistoryState::Erasing => State::Erasing, - DocumentHistoryState::Absent => State::Absent, - } as i32, - remaining_revisions: lifecycle.remaining_revisions, - deleted_at_ms: lifecycle.times.deleted_at_ms, - erasing_started_at_ms: lifecycle.times.erasing_started_at_ms, - erasing_from_time_ms: lifecycle.times.erasing_from_time_ms, - erasing_from_revision: lifecycle.times.erasing_from_revision, - }), - }) - }; - Ok(QueryValidationResult::new_with_data( - GetDocumentHistoryResponseV0 { - result: Some(result), - metadata: Some(self.response_metadata_v0(platform_state, CheckpointUsed::Current)), - }, - )) - } -} - -#[cfg(test)] -mod tests; diff --git a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs b/packages/rs-drive-abci/src/query/document_history/v1/tests.rs deleted file mode 100644 index be888965b68..00000000000 --- a/packages/rs-drive-abci/src/query/document_history/v1/tests.rs +++ /dev/null @@ -1,690 +0,0 @@ -use super::*; -use crate::query::tests::setup_platform; -use dapi_grpc::platform::v0::get_document_history_request::get_document_history_request_v0::Cursor; -use dapi_grpc::platform::v0::{ - GetDocumentHistoryRequest, GetDocumentHistoryResponse, Proof, ResponseMetadata, -}; -use dpp::block::block_info::BlockInfo; -use dpp::bls_signatures::{Bls12381G2Impl, SecretKey, SignatureSchemes}; -use dpp::dashcore::Network; -use dpp::data_contract::TokenConfiguration; -use dpp::document::{DocumentV0Getters, DocumentV0Setters}; -use dpp::prelude::{CoreBlockHeight, DataContract, Identifier}; -use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; -use drive::util::object_size_info::{DocumentAndContractInfo, DocumentInfo, OwnedDocumentInfo}; -use drive::verify::document::DocumentHistoryProof; -use drive_proof_verifier::types::DocumentHistory; -use drive_proof_verifier::{ContextProvider, ContextProviderError, FromProof}; -use std::sync::Arc; -use tenderdash_abci::{ - proto::types::{CanonicalVote, SignedMsgType, StateId}, - signatures::{Hashable, Signable}, -}; - -struct Provider { - contract: Arc, - key: [u8; 48], -} -impl ContextProvider for Provider { - fn get_data_contract( - &self, - id: &Identifier, - _: &PlatformVersion, - ) -> Result>, ContextProviderError> { - Ok((id == &self.contract.id()).then(|| self.contract.clone())) - } - fn get_token_configuration( - &self, - _: &Identifier, - ) -> Result, ContextProviderError> { - Ok(None) - } - fn get_quorum_public_key( - &self, - _: u32, - _: [u8; 32], - _: u32, - ) -> Result<[u8; 48], ContextProviderError> { - Ok(self.key) - } - fn get_platform_activation_height(&self) -> Result { - Ok(1) - } -} - -fn signed_proof( - bytes: Vec, - root: [u8; 32], - metadata: &ResponseMetadata, - key: &SecretKey, - quorum_type: u32, -) -> Proof { - let state = StateId { - app_version: metadata.protocol_version as u64, - core_chain_locked_height: metadata.core_chain_locked_height, - time: metadata.time_ms, - app_hash: root.to_vec(), - height: metadata.height, - }; - let commit = CanonicalVote { - r#type: SignedMsgType::Precommit.into(), - block_id: vec![7; 32], - chain_id: metadata.chain_id.clone(), - height: metadata.height as i64, - round: 0, - state_id: state - .calculate_msg_hash(&metadata.chain_id, metadata.height as i64, 0) - .unwrap(), - }; - let digest = commit - .calculate_sign_hash( - &metadata.chain_id, - quorum_type.try_into().unwrap(), - &[9; 32], - metadata.height as i64, - 0, - ) - .unwrap(); - Proof { - grovedb_proof: bytes, - quorum_hash: vec![9; 32], - signature: key - .sign(SignatureSchemes::Basic, &digest) - .unwrap() - .as_raw_value() - .to_compressed() - .to_vec(), - round: 0, - block_id_hash: vec![7; 32], - quorum_type, - } -} - -/// The proof a proved history response answers with; a proved response never -/// carries the history itself. -fn proof_mut(response: &mut GetDocumentHistoryResponseV0) -> &mut Proof { - match response.result.as_mut() { - Some(ResponseResult::Proof(proof)) => proof, - _ => panic!("a proved history query answers with a proof"), - } -} - -#[test] -fn should_round_trip_history_api_through_quorum_and_grove_proof_verification() { - history_api_proof_round_trip(false); -} - -#[test] -fn should_reject_legacy_proof_envelopes_in_history_v1_through_sdk() { - history_api_proof_round_trip(true); -} - -fn history_api_proof_round_trip(gapped: bool) { - let (platform, state, version) = setup_platform(None, Network::Testnet, None); - let mut state = state.as_ref().clone(); - let contract = json_document_to_contract(concat!(env!("CARGO_MANIFEST_DIR"), "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json"), false, version).unwrap(); - platform - .drive - .apply_contract(&contract, BlockInfo::default(), true, None, None, version) - .unwrap(); - let document_type = contract.document_type_for_name("profile").unwrap(); - let mut document = json_document_to_document( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" - ), - Some([8; 32].into()), - document_type, - version, - ) - .unwrap(); - for revision in 1..=3 { - if gapped && revision == 2 { - continue; - } - document.set_revision(Some(revision)); - platform - .drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentInfo::DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - true, - BlockInfo::default_with_time(2000), - true, - None, - version, - None, - ) - .unwrap(); - } - let key = SecretKey::::from_hash(b"document-history-quorum"); - let provider = Provider { - contract: Arc::new(contract.clone()), - key: key.public_key().to_bytes().try_into().unwrap(), - }; - state.last_committed_block_info = Some( - dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { - basic_info: BlockInfo { - height: 42, - core_height: 12, - time_ms: 3000, - epoch: Default::default(), - }, - app_hash: [0; 32], - quorum_hash: [9; 32], - block_id_hash: [7; 32], - proposer_pro_tx_hash: [0; 32], - signature: [0; 96], - round: 0, - } - .into(), - ); - let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); - let root = platform - .drive - .grove - .root_hash(None, &version.drive.grove_version) - .value - .unwrap(); - use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; - let signed = signed_proof( - vec![], - root, - &metadata, - &key, - platform.config.validator_set.quorum_type as u32, - ); - let committed = state.last_committed_block_info.as_mut().unwrap(); - committed.set_app_hash(root); - committed.set_signature(signed.signature.try_into().unwrap()); - if gapped { - for (wire_selector, filter) in [ - (Filter::Revision(3), DocumentHistoryFilter::Revision(3)), - ( - Filter::StartAtRevision(3), - DocumentHistoryFilter::StartAtRevision(3), - ), - ] { - let request = GetDocumentHistoryRequestV0 { - data_contract_id: contract.id().to_vec(), - document_type_name: "profile".into(), - document_id: document.id().to_vec(), - limit: None, - prove: true, - filter: Some(wire_selector), - }; - // Drive refuses revision reads over a gapped history; the handler - // must answer that as an invalid argument, not as a node error - // the client would retry. - for prove in [true, false] { - let mut refused = request.clone(); - refused.prove = prove; - let result = platform - .query_document_history_v1(refused, &state, version) - .expect("a refused revision read is a query error, not a node error"); - assert!(!result.is_valid()); - assert!( - matches!(result.errors.first(), Some(QueryError::Query(_))), - "prove={prove}: {:?}", - result.errors - ); - } - let mut time_request = request.clone(); - time_request.filter = Some(Filter::StartAtMs(0)); - let mut response = platform - .query_document_history_v1(time_request, &state, version) - .unwrap() - .into_data() - .unwrap(); - let query = DocumentHistoryDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "profile".into(), - document_id: document.id().to_buffer(), - filter, - limit: None, - }; - let proof = proof_mut(&mut response); - let mut proofs = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); - proofs.metadata_proof = - drive::util::test_helpers::history_proof::downgrade_history_count( - &proofs.metadata_proof, - Some(3), - version, - ); - proofs.entries_proof = Some( - platform - .drive - .grove_get_proved_path_query( - &query.construct_path_query(version).unwrap(), - None, - &mut vec![], - &version.drive, - ) - .unwrap(), - ); - proof.grovedb_proof = proofs.to_bytes().unwrap(); - let error = DocumentHistory::maybe_from_proof( - request.clone(), - response.clone(), - Network::Testnet, - version, - &provider, - ) - .expect_err("document history requires GroveDB v1 even with a valid quorum signature"); - assert!(matches!( - error, - drive_proof_verifier::Error::UnsupportedGroveDBProofVersion { .. } - )); - assert!(drive_proof_verifier::types::DocumentHistoryProofInfo::maybe_from_proof_with_metadata( - request, response, Network::Testnet, version, &provider, - ).is_err()); - } - return; - } - let selections = [ - ( - document.id().to_buffer(), - Filter::StartAtMs(2000), - DocumentHistoryFilter::StartAtTime(2000), - ), - ( - document.id().to_buffer(), - Filter::StartAfter(Cursor { - time_ms: 2000, - revision: 1, - }), - DocumentHistoryFilter::StartAfter { - time_ms: 2000, - revision: 1, - }, - ), - ( - document.id().to_buffer(), - Filter::Revision(2), - DocumentHistoryFilter::Revision(2), - ), - ( - document.id().to_buffer(), - Filter::StartAtRevision(65535), - DocumentHistoryFilter::StartAtRevision(65535), - ), - ( - [255; 32], - Filter::Revision(2), - DocumentHistoryFilter::Revision(2), - ), - ]; - for (id, wire_selector, filter) in selections { - let request = GetDocumentHistoryRequestV0 { - data_contract_id: contract.id().to_vec(), - document_type_name: "profile".into(), - document_id: id.to_vec(), - limit: None, - prove: true, - filter: Some(wire_selector), - }; - let request_v0 = request.clone(); - let response = platform - .query_document_history_v1(request.clone(), &state, version) - .unwrap() - .into_data() - .unwrap(); - let query = DocumentHistoryDriveQuery { - contract_id: contract.id().to_buffer(), - document_type_name: "profile".into(), - document_id: id, - limit: None, - filter, - }; - let expected = platform - .drive - .fetch_document_history(&query, document_type, None, version) - .unwrap(); - let request: GetDocumentHistoryRequest = request.into(); - let verify = |response: GetDocumentHistoryResponseV0| { - DocumentHistory::maybe_from_proof( - request.clone(), - GetDocumentHistoryResponse::from(response), - Network::Testnet, - version, - &provider, - ) - }; - let (exported, _, _) = - drive_proof_verifier::types::DocumentHistoryProofInfo::maybe_from_proof_with_metadata( - request.clone(), - GetDocumentHistoryResponse::from(response.clone()), - Network::Testnet, - version, - &provider, - ) - .unwrap(); - let exported = exported.unwrap(); - assert_eq!(exported.response, response); - assert_eq!( - exported - .verify(request.clone(), Network::Testnet, version, &provider) - .unwrap(), - Some(exported.history) - ); - let result = verify(response.clone()).unwrap().unwrap(); - assert_eq!(result.entries, expected.entries); - assert_eq!(result.lifecycle, expected.lifecycle.clone()); - // Without a proof the same query answers with the history itself. - let mut plain_request = request_v0.clone(); - plain_request.prove = false; - let plain = platform - .query_document_history_v1(plain_request, &state, version) - .unwrap() - .into_data() - .unwrap(); - let Some(ResponseResult::History(history)) = plain.result else { - panic!("an unproved history query answers with the history"); - }; - assert_eq!( - history - .entries - .iter() - .map(|entry| (entry.time_ms, entry.revision)) - .collect::>(), - expected - .entries - .iter() - .map(|entry| (entry.time_ms, entry.revision)) - .collect::>() - ); - let lifecycle = history - .lifecycle - .expect("the history carries its lifecycle"); - let expected_lifecycle = expected - .lifecycle - .as_ref() - .expect("pages of the history tree carry a lifecycle"); - assert_eq!( - lifecycle.remaining_revisions, - expected_lifecycle.remaining_revisions - ); - assert_eq!( - lifecycle.state, - match expected_lifecycle.state { - DocumentHistoryState::Active => State::Active, - DocumentHistoryState::Deleted => State::Deleted, - DocumentHistoryState::Erasing => State::Erasing, - DocumentHistoryState::Absent => State::Absent, - } as i32 - ); - let with_proofs = |mut response: GetDocumentHistoryResponseV0, - edit: &dyn Fn(&mut DocumentHistoryProof)| { - let proof = proof_mut(&mut response); - let mut proofs = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); - edit(&mut proofs); - proof.grovedb_proof = proofs.to_bytes().unwrap(); - response - }; - let legacy_metadata = with_proofs(response.clone(), &|proofs| { - proofs.metadata_proof = - drive::util::test_helpers::history_proof::downgrade_history_count( - &proofs.metadata_proof, - None, - version, - ); - }); - let error = - verify(legacy_metadata).expect_err("metadata always requires a GroveDB v1 envelope"); - assert!(matches!( - error, - drive_proof_verifier::Error::UnsupportedGroveDBProofVersion { .. } - )); - let mut torn = response.clone(); - proof_mut(&mut torn).grovedb_proof.push(0); - assert!( - verify(torn).is_err(), - "a proof envelope with trailing bytes is not the proof the node produced" - ); - let mut bad_signature = response.clone(); - proof_mut(&mut bad_signature).signature[0] ^= 1; - assert!(verify(bad_signature).is_err()); - let has_entries_proof = { - let mut probe = response.clone(); - DocumentHistoryProof::from_bytes(&proof_mut(&mut probe).grovedb_proof) - .unwrap() - .entries_proof - .is_some() - }; - if has_entries_proof { - let downgraded = with_proofs(response.clone(), &|proofs| { - let bytes = proofs.entries_proof.as_ref().unwrap(); - proofs.entries_proof = Some( - drive::util::test_helpers::history_proof::downgrade_history_count( - bytes, None, version, - ), - ); - }); - assert!( - verify(downgraded).is_err(), - "entries also require an authenticated V1 envelope" - ); - let missing = with_proofs(response.clone(), &|proofs| proofs.entries_proof = None); - assert!(verify(missing).is_err()); - } - } -} - -#[test] -fn should_reject_missing_selectors_before_reading_state() { - let (platform, state, version) = setup_platform(None, Network::Testnet, None); - let request = GetDocumentHistoryRequestV0 { - data_contract_id: vec![1; 32], - document_type_name: "note".into(), - document_id: vec![2; 32], - filter: None, - limit: None, - prove: false, - }; - assert!(!platform - .query_document_history_v1(request, &state, version) - .unwrap() - .is_valid()); -} - -#[test] -fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { - use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Setters; - - let (platform, state, version) = setup_platform(None, Network::Testnet, None); - let mut state = state.as_ref().clone(); - let contract = json_document_to_contract( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract-with-profile-history.json" - ), - false, - version, - ) - .unwrap(); - platform - .drive - .apply_contract(&contract, BlockInfo::default(), true, None, None, version) - .unwrap(); - let document_type = contract.document_type_for_name("profile").unwrap(); - let owner = Identifier::from([8; 32]); - let mut document = json_document_to_document( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/../rs-drive/tests/supporting_files/contract/dashpay/profile0.json" - ), - Some(owner), - document_type, - version, - ) - .unwrap(); - let chunk = version - .system_limits - .max_document_revisions_erased_per_transition - .expect("protocol 15 bounds the erase chunk") as u64; - for revision in 1..=chunk + 2 { - document.set_revision(Some(revision)); - platform - .drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentInfo::DocumentRefInfo((&document, None)), - owner_id: None, - }, - contract: &contract, - document_type, - }, - revision > 1, - BlockInfo::default_with_time(1_000 + revision), - true, - None, - version, - None, - ) - .unwrap(); - } - - let key = SecretKey::::from_hash(b"document-history-quorum"); - let provider = Provider { - contract: Arc::new(contract.clone()), - key: key.public_key().to_bytes().try_into().unwrap(), - }; - state.last_committed_block_info = Some( - dpp::block::extended_block_info::v0::ExtendedBlockInfoV0 { - basic_info: BlockInfo { - height: 42, - core_height: 12, - time_ms: 3_000, - epoch: Default::default(), - }, - app_hash: [0; 32], - quorum_hash: [9; 32], - block_id_hash: [7; 32], - proposer_pro_tx_hash: [0; 32], - signature: [0; 96], - round: 0, - } - .into(), - ); - let wire_request = GetDocumentHistoryRequestV0 { - data_contract_id: contract.id().to_vec(), - document_type_name: "profile".into(), - document_id: document.id().to_vec(), - limit: None, - prove: true, - filter: Some(Filter::StartAtMs(0)), - }; - let request: GetDocumentHistoryRequest = wire_request.clone().into(); - - for (stage, expected_state) in [ - ("deleted", DocumentHistoryState::Deleted), - ("erasing", DocumentHistoryState::Erasing), - ] { - let operations = if stage == "deleted" { - platform - .drive - .delete_document_for_contract_operations_with_lifecycle( - document.id(), - &contract, - document_type, - &BlockInfo::default_with_time(5_000), - Some(owner), - None, - &mut None, - 5_000, - None, - version, - ) - .unwrap() - } else { - platform - .drive - .erase_document_for_contract_operations( - document.id(), - &contract, - document_type, - &BlockInfo::default_with_time(6_000), - &mut None, - None, - version, - ) - .unwrap() - }; - platform - .drive - .apply_batch_low_level_drive_operations( - None, - None, - operations, - &mut vec![], - &version.drive, - ) - .unwrap(); - - let root = platform - .drive - .grove - .root_hash(None, &version.drive.grove_version) - .value - .unwrap(); - let metadata = platform.response_metadata_v0(&state, CheckpointUsed::Current); - let signed = signed_proof( - vec![], - root, - &metadata, - &key, - platform.config.validator_set.quorum_type as u32, - ); - let committed = state.last_committed_block_info.as_mut().unwrap(); - committed.set_app_hash(root); - committed.set_signature(signed.signature.try_into().unwrap()); - - let response = platform - .query_document_history_v1(wire_request.clone(), &state, version) - .unwrap() - .into_data() - .unwrap(); - // A proved response carries the proof and nothing else: there are no - // lifecycle fields on the wire for a client to trust, so every value - // the verifier returns below is derived from the proof itself. - assert!( - matches!(response.result, Some(ResponseResult::Proof(_))), - "{stage}: a proved response must not carry lifecycle claims" - ); - let verify = |response: GetDocumentHistoryResponseV0| { - DocumentHistory::maybe_from_proof( - request.clone(), - GetDocumentHistoryResponse::from(response), - Network::Testnet, - version, - &provider, - ) - }; - let history = verify(response.clone()) - .unwrap_or_else(|error| panic!("{stage}: honest response must verify: {error}")) - .expect("the proof authenticates a history"); - let lifecycle = history - .lifecycle - .expect("the proof authenticates the lifecycle"); - assert_eq!(lifecycle.state, expected_state, "{stage}"); - assert_eq!(lifecycle.times.deleted_at_ms, 5_000, "{stage}"); - - let mut tampered = response; - let proof = proof_mut(&mut tampered); - let mut envelope = DocumentHistoryProof::from_bytes(&proof.grovedb_proof).unwrap(); - let middle = envelope.metadata_proof.len() / 2; - envelope.metadata_proof[middle] ^= 1; - proof.grovedb_proof = envelope.to_bytes().unwrap(); - assert!( - verify(tampered).is_err(), - "{stage}: changing the proof that authenticates lifecycle must be rejected" - ); - } -} diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs index a3de509ffbc..4e00a02840d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/mod.rs @@ -2,7 +2,6 @@ pub mod v0; pub mod v1; pub mod v2; pub mod v3; -pub mod v4; use versioned_feature_core::{FeatureVersion, FeatureVersionBounds}; @@ -13,8 +12,6 @@ pub struct DriveAbciQueryVersions { pub proofs_query: FeatureVersion, pub document_query: FeatureVersionBounds, pub document_history: FeatureVersionBounds, - /// Internal processing generation for the document-history wire v0 query. - pub document_history_processing: FeatureVersion, /// Per-helper version slots for internal v1-document-query /// routing helpers. Separate from `document_query` (which /// versions the wire surface) because the helper output is diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs index f632a1396e4..4462a3cf293 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v0.rs @@ -28,7 +28,6 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V0: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, - document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs index 14f307e58c7..1ab0fc4fae2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v1.rs @@ -30,7 +30,6 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V1: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, - document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs index 351c4889e11..036a932a5f6 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v3.rs @@ -41,7 +41,6 @@ pub const DRIVE_ABCI_QUERY_VERSIONS_V3: DriveAbciQueryVersions = DriveAbciQueryV max_version: 0, default_current_version: 0, }, - document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 2, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs deleted file mode 100644 index a982b6417c4..00000000000 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_query_versions/v4.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::version::drive_abci_versions::drive_abci_query_versions::v3::DRIVE_ABCI_QUERY_VERSIONS_V3; -use crate::version::drive_abci_versions::drive_abci_query_versions::DriveAbciQueryVersions; - -/// Version 4 of the Drive ABCI query versions. -/// -/// Differs from v3 in one slot: `document_history_processing` is 1, the -/// history handler that reports the deleted and erasing lifecycle states of a -/// keep-history document with their times. The wire version of the request and -/// response is unchanged. -pub const DRIVE_ABCI_QUERY_VERSIONS_V4: DriveAbciQueryVersions = DriveAbciQueryVersions { - document_history_processing: 1, - ..DRIVE_ABCI_QUERY_VERSIONS_V3 -}; 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 2efa8fa033b..44a45107bc1 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -178,7 +178,6 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_version: 0, default_current_version: 0, }, - document_history_processing: 0, document_query_helpers: DriveAbciDocumentQueryHelperVersions { compute_aggregate_mode_and_check_limit: 0, }, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 3f4f9953f7f..5581c4ae167 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -3,7 +3,6 @@ use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v use crate::version::dpp_versions::dpp_validation_versions::v6::DPP_VALIDATION_VERSIONS_V6; use crate::version::dpp_versions::DPPVersion; use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; -use crate::version::drive_abci_versions::drive_abci_query_versions::v4::DRIVE_ABCI_QUERY_VERSIONS_V4; use crate::version::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; use crate::version::drive_abci_versions::DriveAbciVersion; use crate::version::drive_versions::v10::DRIVE_VERSION_V10; @@ -49,7 +48,6 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { drive_abci: DriveAbciVersion { methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the protocol-change hook v2 migrates retained histories on the first v15 block validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: delete consults the lifecycle; erase structure and state validation - query: DRIVE_ABCI_QUERY_VERSIONS_V4, // changed: the history handler reports the deleted and erasing lifecycle states with their times ..PLATFORM_V14.drive_abci }, dpp: DPPVersion { From 21b03695ce3860cfe901ac86430c29b4189ee23f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 01:45:15 +0700 Subject: [PATCH 43/48] refactor(drive): let every delete entry point record a keep-history deletion The delete entry points that carry only a block time refused a keep-history document at the lifecycle generation, from inside their version dispatch, on the reasoning that they could not author the lifecycle entry. The entry needs the deletion time, which every entry point carries, and a deleter only to credit the record's bytes, which the fee-applying wrappers already omit on purpose. Nothing else of the block reaches the record, so the refusal protected nothing. The refusal is gone. Each dispatcher matches on the version alone, and the generation-1 body of every entry point delegates to the lifecycle-aware body with the block time it was given and no deleter. The test that pinned the refusal now pins that all three entry points record the deletion at the time they were given. Co-Authored-By: Claude Fable 5.1 --- .../mod.rs | 20 +- .../v1/mod.rs | 39 ++- .../mod.rs | 27 +- .../v1/mod.rs | 71 ++++- .../mod.rs | 20 +- .../v1/mod.rs | 39 ++- .../rs-drive/src/drive/document/delete/mod.rs | 270 ++++++++++-------- 7 files changed, 292 insertions(+), 194 deletions(-) diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs index fdbfbda0c92..8cd7e82c707 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs @@ -6,8 +6,6 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -63,26 +61,10 @@ impl Drive { drive_operations, platform_version, ), - // This signature carries only the block time. Generation 1 records - // a lifecycle entry for a keep-history document with the block and - // deleter that authored the delete, which only the - // `_with_lifecycle` entry point receives, so it serves the types - // that keep no history and refuses the rest instead of recording a - // fabricated block. - 1 if contract - .document_type_for_name(document_type_name)? - .documents_keep_history() => - { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "deleting a keep-history document needs the block and deleter that authored it", - ))) - } 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( document_id, contract, document_type_name, - &BlockInfo::default_with_time(block_time_ms), - None, estimated_costs_only_with_layer_info, block_time_ms, transaction, @@ -133,7 +115,7 @@ impl Drive { drive_operations, platform_version, ), - 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( + 1 => self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( document_id, contract, document_type_name, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs index 2de3672955d..9715f5a18fd 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs @@ -11,10 +11,47 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; impl Drive { - /// Deletes a document. + /// Deletes a document and adds the operations through the entry point that + /// carries only the block time. + /// + /// Generation 1 records a lifecycle entry for a keep-history document. That + /// entry names the deletion time, which this signature carries, and credits + /// the record's bytes to the deleter, which it does not: an entry written + /// through here belongs to nobody, as with the fee-applying wrappers, and + /// refunds nobody when erased. #[inline(always)] #[allow(clippy::too_many_arguments)] pub(super) fn delete_document_for_contract_apply_and_add_to_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + estimated_costs_only_with_layer_info: Option< + HashMap, + >, + block_time_ms: u64, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( + document_id, + contract, + document_type_name, + &BlockInfo::default_with_time(block_time_ms), + None, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + drive_operations, + platform_version, + ) + } + + /// Deletes a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( &self, document_id: Identifier, contract: &DataContract, 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 fb3dc180bb3..08f71671ca9 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 @@ -7,7 +7,6 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use dpp::block::block_info::BlockInfo; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; @@ -100,25 +99,13 @@ impl Drive { transaction, platform_version, ), - // This signature carries only the block time. Generation 1 records - // a lifecycle entry for a keep-history document with the block and - // deleter that authored the delete, which only - // `delete_document_for_contract_operations_with_lifecycle` receives, - // so it serves the types that keep no history and refuses the rest - // instead of recording a fabricated block. - 1 if document_type.documents_keep_history() => { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "deleting a keep-history document needs the block and deleter that authored it", - ))) - } 1 => self.delete_document_for_contract_operations_v1( document_id, contract, document_type, - &BlockInfo::default_with_time(block_time_ms), - None, previous_batch_operations, estimated_costs_only_with_layer_info, + block_time_ms, transaction, platform_version, ), @@ -205,7 +192,7 @@ impl Drive { transaction, platform_version, ), - 1 => self.delete_document_for_contract_operations_v1( + 1 => self.delete_document_for_contract_operations_with_lifecycle_v1( document_id, contract, document_type, @@ -272,21 +259,13 @@ impl Drive { transaction, platform_version, ), - // As above: without the authoring block and deleter, generation 1 - // serves only the types that keep no history. - 1 if document_type.documents_keep_history() => { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "deleting a keep-history document needs the block and deleter that authored it", - ))) - } 1 => self.force_delete_document_for_contract_operations_v1( document_id, contract, document_type, - &BlockInfo::default_with_time(block_time_ms), - None, previous_batch_operations, estimated_costs_only_with_layer_info, + block_time_ms, transaction, platform_version, ), diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 0f1a4d29573..e2be7354b7e 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -42,10 +42,75 @@ use dpp::identifier::Identifier; use dpp::version::PlatformVersion; impl Drive { - /// Prepares the operations for deleting a document. + /// Prepares the operations for deleting a document through the entry point + /// that carries only the block time. + /// + /// Generation 1 records a lifecycle entry for a keep-history document. That + /// entry names the deletion time, which this signature carries, and credits + /// the record's bytes to the deleter, which it does not: an entry written + /// through here belongs to nobody, as with the fee-applying wrappers, and + /// refunds nobody when erased. #[inline(always)] #[allow(clippy::too_many_arguments)] pub(super) fn delete_document_for_contract_operations_v1( + &self, + document_id: Identifier, + 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> { + self.delete_document_for_contract_operations_with_lifecycle_v1( + document_id, + contract, + document_type, + &BlockInfo::default_with_time(block_time_ms), + None, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ) + } + + /// The forced counterpart of [`Self::delete_document_for_contract_operations_v1`]. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn force_delete_document_for_contract_operations_v1( + &self, + document_id: Identifier, + 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> { + self.force_delete_document_for_contract_operations_with_lifecycle_v1( + document_id, + contract, + document_type, + &BlockInfo::default_with_time(block_time_ms), + None, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ) + } + + /// Prepares the operations for deleting a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_operations_with_lifecycle_v1( &self, document_id: Identifier, contract: &DataContract, @@ -65,7 +130,7 @@ impl Drive { ))); } - self.force_delete_document_for_contract_operations_v1( + self.force_delete_document_for_contract_operations_with_lifecycle_v1( document_id, contract, document_type, @@ -89,7 +154,7 @@ impl Drive { /// Every other document type is deleted exactly as v0 deletes it. #[inline(always)] #[allow(clippy::too_many_arguments)] - pub(super) fn force_delete_document_for_contract_operations_v1( + pub(super) fn force_delete_document_for_contract_operations_with_lifecycle_v1( &self, document_id: Identifier, contract: &DataContract, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs index a64c6228917..9526ec981ac 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs @@ -6,8 +6,6 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -62,26 +60,10 @@ impl Drive { transaction, platform_version, ), - // This signature carries only the block time. Generation 1 records - // a lifecycle entry for a keep-history document with the block and - // deleter that authored the delete, which only the - // `_with_lifecycle` entry point receives, so it serves the types - // that keep no history and refuses the rest instead of recording a - // fabricated block. - 1 if contract - .document_type_for_name(document_type_name)? - .documents_keep_history() => - { - Err(Error::Drive(DriveError::CorruptedCodeExecution( - "deleting a keep-history document needs the block and deleter that authored it", - ))) - } 1 => self.delete_document_for_contract_with_named_type_operations_v1( document_id, contract, document_type_name, - &BlockInfo::default_with_time(block_time_ms), - None, previous_batch_operations, estimated_costs_only_with_layer_info, block_time_ms, @@ -131,7 +113,7 @@ impl Drive { transaction, platform_version, ), - 1 => self.delete_document_for_contract_with_named_type_operations_v1( + 1 => self.delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( document_id, contract, document_type_name, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs index f87fc8b5890..b30bb409044 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs @@ -18,10 +18,47 @@ use dpp::identifier::Identifier; use dpp::version::PlatformVersion; impl Drive { - /// Prepares the operations for deleting a document. + /// Prepares the operations for deleting a document named by its type + /// through the entry point that carries only the block time. + /// + /// Generation 1 records a lifecycle entry for a keep-history document. That + /// entry names the deletion time, which this signature carries, and credits + /// the record's bytes to the deleter, which it does not: an entry written + /// through here belongs to nobody, as with the fee-applying wrappers, and + /// refunds nobody when erased. #[inline(always)] #[allow(clippy::too_many_arguments)] pub(super) fn delete_document_for_contract_with_named_type_operations_v1( + &self, + document_id: Identifier, + contract: &DataContract, + document_type_name: &str, + 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> { + self.delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( + document_id, + contract, + document_type_name, + &BlockInfo::default_with_time(block_time_ms), + None, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_time_ms, + transaction, + platform_version, + ) + } + + /// Prepares the operations for deleting a document. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( &self, document_id: Identifier, contract: &DataContract, diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 53a476a8217..62272e5c067 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1395,157 +1395,173 @@ mod tests { ); } - /// A delete entry point that carries only the block time cannot author the - /// lifecycle entry of a keep-history document, so at the lifecycle - /// generation it must refuse such a document loudly instead of recording a - /// fabricated block, while the entry points that carry the block still - /// delete it. + /// A delete entry point that carries only the block time still records the + /// lifecycle entry of a keep-history document at the lifecycle generation: + /// the entry needs the deletion time, which every entry point carries, and + /// a deleter only to credit the record's bytes, which these carry no more + /// than the fee-applying wrappers do. #[test] - fn should_refuse_a_keep_history_delete_through_entry_points_without_a_block() { + fn should_record_a_keep_history_delete_through_entry_points_without_a_deleter() { use crate::drive::document::lifecycle::DocumentLifecycleState; use dpp::document::DocumentV0Getters; - let drive = setup_drive_with_initial_state_structure(None); let platform_version = PlatformVersion::latest(); - let contract = setup_contract( - &drive, - "tests/supporting_files/contract/family/family-contract-with-history.json", - None, - None, - None::, - None, - None, - ); - let document_type = contract - .document_type_for_name("person") - .expect("expected to get document type"); - let owner_id = rand::thread_rng().gen::<[u8; 32]>(); - let person_document = json_document_to_document( - "tests/supporting_files/contract/family/person0.json", - Some(owner_id.into()), - document_type, - platform_version, - ) - .expect("expected to get document"); - drive - .add_document_for_contract( - DocumentAndContractInfo { - owned_document_info: OwnedDocumentInfo { - document_info: DocumentRefInfo(( - &person_document, - Some(Cow::Owned(StorageFlags::SingleEpoch(0))), - )), - owner_id: None, - }, - contract: &contract, - document_type, - }, - false, - BlockInfo::default(), - true, + let setup = || { + let drive = setup_drive_with_initial_state_structure(None); + let contract = setup_contract( + &drive, + "tests/supporting_files/contract/family/family-contract-with-history.json", + None, + None, + None::, None, - platform_version, None, - ) - .expect("expected to insert a document successfully"); - let document_id = person_document.id(); - - let is_refusal = |error: Error| { - assert!( - matches!( - &error, - Error::Drive(DriveError::CorruptedCodeExecution(message)) - if message.contains("needs the block and deleter") - ), - "expected the keep-history refusal, got {error:?}" ); - }; - is_refusal( + let document_type = contract + .document_type_for_name("person") + .expect("expected to get document type"); + let owner_id = rand::thread_rng().gen::<[u8; 32]>(); + let person_document = json_document_to_document( + "tests/supporting_files/contract/family/person0.json", + Some(owner_id.into()), + document_type, + platform_version, + ) + .expect("expected to get document"); drive - .delete_document_for_contract_operations( - document_id, - &contract, - document_type, - None, - &mut None, - 1_000, + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &person_document, + Some(Cow::Owned(StorageFlags::SingleEpoch(0))), + )), + owner_id: None, + }, + contract: &contract, + document_type, + }, + false, + BlockInfo::default(), + true, None, platform_version, - ) - .expect_err("a delete without a block must refuse a keep-history document"), - ); - is_refusal( - drive - .delete_document_for_contract_with_named_type_operations( - document_id, - &contract, - "person", - None, - &mut None, - 1_000, None, - platform_version, ) - .expect_err("a delete without a block must refuse a keep-history document"), - ); - is_refusal( - drive - .delete_document_for_contract_apply_and_add_to_operations( - document_id, - &contract, - "person", - None, - 1_000, + .expect("expected to insert a document successfully"); + (drive, contract, person_document.id()) + }; + let assert_deleted_at = |drive: &Drive, contract: &DataContract, document_id| { + let document_type = contract + .document_type_for_name("person") + .expect("expected to get document type"); + assert!( + matches!( + drive + .fetch_document_lifecycle( + contract, + document_type, + document_id, + None, + None, + platform_version, + ) + .expect("expected to read the lifecycle") + .0, + DocumentLifecycleState::Deleted(_) + ), + "the delete must record the lifecycle entry" + ); + let (_, record) = drive + .fetch_document_history( + &crate::query::document_history_drive_query::DocumentHistoryDriveQuery { + contract_id: contract.id().to_buffer(), + document_type_name: "person".to_string(), + document_id: document_id.to_buffer(), + filter: crate::query::document_history_drive_query::DocumentHistoryFilter::StartAtTime(0), + limit: Some(1), + }, + document_type, None, - &mut vec![], platform_version, ) - .expect_err("a delete without a block must refuse a keep-history document"), - ); - assert!( - matches!( - drive - .fetch_document_lifecycle( - &contract, - document_type, - document_id, - None, - None, - platform_version, - ) - .expect("expected to read the lifecycle") - .0, - DocumentLifecycleState::Active(_) - ), - "a refused delete leaves the document untouched" - ); + .map(|history| (history.entries.len(), history.lifecycle)) + .expect("expected to read the history"); + assert_eq!( + record + .expect("the lifecycle is authenticated") + .times + .deleted_at_ms, + 1_000, + "the entry carries the deletion time the entry point was given" + ); + }; + let (drive, contract, document_id) = setup(); + let document_type = contract + .document_type_for_name("person") + .expect("expected to get document type"); + let operations = drive + .delete_document_for_contract_operations( + document_id, + &contract, + document_type, + None, + &mut None, + 1_000, + None, + platform_version, + ) + .expect("expected the delete to build"); drive - .delete_document_for_contract( + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the delete to apply"); + assert_deleted_at(&drive, &contract, document_id); + + let (drive, contract, document_id) = setup(); + let operations = drive + .delete_document_for_contract_with_named_type_operations( document_id, &contract, "person", - BlockInfo::default_with_time(2_000), - true, + None, + &mut None, + 1_000, None, platform_version, - Some(&EPOCH_CHANGE_FEE_VERSION_TEST), ) - .expect("the entry point that carries the block deletes the document"); - assert!(matches!( - drive - .fetch_document_lifecycle( - &contract, - document_type, - document_id, - None, - None, - platform_version, - ) - .expect("expected to read the lifecycle") - .0, - DocumentLifecycleState::Deleted(_) - )); + .expect("expected the delete to build"); + drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the delete to apply"); + assert_deleted_at(&drive, &contract, document_id); + + let (drive, contract, document_id) = setup(); + drive + .delete_document_for_contract_apply_and_add_to_operations( + document_id, + &contract, + "person", + None, + 1_000, + None, + &mut vec![], + platform_version, + ) + .expect("expected the delete to apply"); + assert_deleted_at(&drive, &contract, document_id); } // ---------- Error-path tests (added for coverage) ---------- From fc6786389a22d482875b883a65150f89a1524573 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 02:10:54 +0700 Subject: [PATCH 44/48] refactor(drive)!: give the delete entry points the block and deleter instead of parallel methods The lifecycle delete needs the block and the deleter, so the delete entry points grew `_with_lifecycle` twins beside them, dispatched on the sibling's version slot, and the delete operation grew a `DeleteDocumentWithLifecycle` variant beside `DeleteDocument`. One method per directory means the signature changes instead: `delete_document_for_contract_operations`, its forced and post-drain forms, the named-type and by-id-named-type forms and the apply-and-add form now take `block_info: &BlockInfo` and `deleter_id: Option` in place of `block_time_ms`, and `DocumentOperationType::DeleteDocument` carries `deleter_id`. Generation 0 reads the time off the block and ignores the deleter, so released protocol versions behave as before; generation 1 records both. The generation-1 modules of `delete_document_for_contract`, its by-id form, the named-type form and the apply-and-add form existed only to forward the block and deleter to the twins. With the signature carrying them, they are the generation-0 bodies, so those modules and their version-table bumps are gone; the lifecycle decision stays in `delete_document_for_contract_operations` generation 1, where the behaviour lives. A delete converted from an operation naming its contract by id returns the contract fetch cost ahead of the delete's operations for every deleter, as the lifecycle variant already did. Co-Authored-By: Claude Fable 5.1 --- .../src/query/document_history/v0/tests.rs | 3 +- .../v0/tests/batched_group_drain.rs | 1 + .../contract/update/update_keywords/v0/mod.rs | 3 +- .../delete_document_for_contract/mod.rs | 13 +- .../delete_document_for_contract/v0/mod.rs | 3 +- .../delete_document_for_contract/v1/mod.rs | 60 --------- .../mod.rs | 75 +---------- .../v0/mod.rs | 7 +- .../v1/mod.rs | 101 --------------- .../delete_document_for_contract_id/mod.rs | 13 +- .../delete_document_for_contract_id/v0/mod.rs | 3 +- .../delete_document_for_contract_id/v1/mod.rs | 80 ------------ .../mod.rs | 14 +- .../v0/mod.rs | 11 +- .../mod.rs | 122 +++--------------- .../v1/mod.rs | 71 +--------- .../mod.rs | 71 +--------- .../v0/mod.rs | 7 +- .../v1/mod.rs | 90 ------------- .../rs-drive/src/drive/document/delete/mod.rs | 13 +- .../time_range_index_e2e_tests.rs | 6 +- .../src/drive/document/lifecycle/tests.rs | 16 +-- .../document/document_delete_transition.rs | 3 +- .../src/util/batch/drive_op_batch/document.rs | 48 +------ .../tests/drive_storage_ops_coverage.rs | 1 + .../drive_document_method_versions/v5.rs | 4 - 26 files changed, 92 insertions(+), 747 deletions(-) delete mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs delete mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs delete mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs delete mode 100644 packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs diff --git a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs index 1eea86eb159..453c93b8f0c 100644 --- a/packages/rs-drive-abci/src/query/document_history/v0/tests.rs +++ b/packages/rs-drive-abci/src/query/document_history/v0/tests.rs @@ -677,7 +677,7 @@ fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { let operations = if stage == "deleted" { platform .drive - .delete_document_for_contract_operations_with_lifecycle( + .delete_document_for_contract_operations( document.id(), &contract, document_type, @@ -685,7 +685,6 @@ fn should_derive_deleted_and_erasing_lifecycle_from_the_proof() { Some(owner), None, &mut None, - 5_000, None, version, ) diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs index 531272d06aa..d374b60b257 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v0/tests/batched_group_drain.rs @@ -177,6 +177,7 @@ fn delete_op<'a>( ) -> DriveOperation<'a> { DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { document_id, + deleter_id: None, contract_info: DataContractInfo::BorrowedDataContract(contract), document_type_info: DocumentTypeInfo::DocumentTypeNameAsStr(doctype), }) diff --git a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs index 08f3168acd6..d6b840b4d37 100644 --- a/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_keywords/v0/mod.rs @@ -146,9 +146,10 @@ impl Drive { *doc_id, &contract, document_type, + block_info, + None, None, estimated_costs_only_with_layer_info, - block_info.time_ms, transaction, platform_version, )?); diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs index f28fd284ba7..616d27de6c6 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -60,19 +59,9 @@ impl Drive { platform_version, previous_fee_versions, ), - 1 => self.delete_document_for_contract_v1( - document_id, - contract, - document_type_name, - block_info, - apply, - transaction, - platform_version, - previous_fee_versions, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs index b82d4b25c51..18e27f9a5dd 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v0/mod.rs @@ -37,8 +37,9 @@ impl Drive { document_id, contract, document_type_name, + &block_info, + None, estimated_costs_only_with_layer_info, - block_info.time_ms, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs deleted file mode 100644 index 9da1db0d4d2..00000000000 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract/v1/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::drive::Drive; -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::DataContract; -use dpp::fee::fee_result::FeeResult; - -use dpp::fee::default_costs::CachedEpochIndexFeeVersions; -use dpp::identifier::Identifier; -use dpp::version::PlatformVersion; -use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, TransactionArg}; -use std::collections::HashMap; - -impl Drive { - /// Deletes a document and returns the associated fee. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_v1( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - block_info: BlockInfo, - apply: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, - ) -> Result { - let mut drive_operations: Vec = vec![]; - let estimated_costs_only_with_layer_info = if apply { - None::> - } else { - Some(HashMap::new()) - }; - self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( - document_id, - contract, - document_type_name, - &block_info, - // These fee-applying wrappers carry no signer, so a lifecycle record - // they write belongs to no identity and refunds nobody. - None, - estimated_costs_only_with_layer_info, - block_info.time_ms, - transaction, - &mut drive_operations, - platform_version, - )?; - let fees = Drive::calculate_fee( - None, - Some(drive_operations), - &block_info.epoch, - self.config.epochs_per_era, - platform_version, - previous_fee_versions, - )?; - Ok(fees) - } -} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs index 8cd7e82c707..22e37125306 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -15,75 +14,25 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; impl Drive { - /// Deletes a document. + /// Deletes a document and adds the operations to the given list. /// /// # Parameters /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type_name`: The name of the document type. - /// * `owner_id`: The owner ID of the document. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record a + /// keep-history delete writes; `None` credits nobody. /// * `estimated_costs_only_with_layer_info`: An optional hashmap with layer information for estimated costs. /// * `transaction`: The transaction argument. /// * `drive_operations`: A mutable vector of low level drive operations. - /// * `drive_version`: The drive version to select the correct function version to run. + /// * `platform_version`: The platform 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. #[allow(clippy::too_many_arguments)] pub fn delete_document_for_contract_apply_and_add_to_operations( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - estimated_costs_only_with_layer_info: Option< - HashMap, - >, - block_time_ms: u64, - transaction: TransactionArg, - drive_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - match platform_version - .drive - .methods - .document - .delete - .delete_document_for_contract_apply_and_add_to_operations - { - 0 => self.delete_document_for_contract_apply_and_add_to_operations_v0( - document_id, - contract, - document_type_name, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - drive_operations, - platform_version, - ), - 1 => self.delete_document_for_contract_apply_and_add_to_operations_v1( - document_id, - contract, - document_type_name, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - drive_operations, - platform_version, - ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { - method: "delete_document_for_contract_apply_and_add_to_operations".to_string(), - known_versions: vec![0, 1], - received: version, - })), - } - } - - /// Deletes a document and adds the operations to the given list, recording - /// the lifecycle entry of a keep-history document with the block and deleter - /// that authored the delete. - #[allow(clippy::too_many_arguments)] - pub fn delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( &self, document_id: Identifier, contract: &DataContract, @@ -93,7 +42,6 @@ impl Drive { estimated_costs_only_with_layer_info: Option< HashMap, >, - block_time_ms: u64, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, @@ -106,30 +54,19 @@ impl Drive { .delete_document_for_contract_apply_and_add_to_operations { 0 => self.delete_document_for_contract_apply_and_add_to_operations_v0( - document_id, - contract, - document_type_name, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - drive_operations, - platform_version, - ), - 1 => self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( document_id, contract, document_type_name, block_info, deleter_id, estimated_costs_only_with_layer_info, - block_time_ms, transaction, drive_operations, platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_apply_and_add_to_operations".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs index 2a65843e041..4d84f634ee9 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v0/mod.rs @@ -1,6 +1,7 @@ use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use dpp::identifier::Identifier; @@ -18,10 +19,11 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, mut estimated_costs_only_with_layer_info: Option< HashMap, >, - block_time_ms: u64, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, @@ -37,9 +39,10 @@ impl Drive { document_id, contract, document_type_name, + block_info, + deleter_id, None, &mut estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, )?; diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs deleted file mode 100644 index 9715f5a18fd..00000000000 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_apply_and_add_to_operations/v1/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::drive::Drive; -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::DataContract; - -use dpp::identifier::Identifier; -use dpp::version::PlatformVersion; -use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, TransactionArg}; -use std::collections::HashMap; - -impl Drive { - /// Deletes a document and adds the operations through the entry point that - /// carries only the block time. - /// - /// Generation 1 records a lifecycle entry for a keep-history document. That - /// entry names the deletion time, which this signature carries, and credits - /// the record's bytes to the deleter, which it does not: an entry written - /// through here belongs to nobody, as with the fee-applying wrappers, and - /// refunds nobody when erased. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_apply_and_add_to_operations_v1( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - estimated_costs_only_with_layer_info: Option< - HashMap, - >, - block_time_ms: u64, - transaction: TransactionArg, - drive_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( - document_id, - contract, - document_type_name, - &BlockInfo::default_with_time(block_time_ms), - None, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - drive_operations, - platform_version, - ) - } - - /// Deletes a document. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_apply_and_add_to_operations_with_lifecycle_v1( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, - mut estimated_costs_only_with_layer_info: Option< - HashMap, - >, - block_time_ms: u64, - transaction: TransactionArg, - drive_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - // With no caller transaction, TTL preparation (direct drainage - // writes) and the apply below would each commit on their own; span - // them with one owned transaction so the write is all-or-nothing. - let owned_transaction = (estimated_costs_only_with_layer_info.is_none() - && transaction.is_none()) - .then(|| self.grove.start_transaction()); - let transaction = owned_transaction.as_ref().or(transaction); - let batch_operations = self - .delete_document_for_contract_with_named_type_operations_with_lifecycle( - document_id, - contract, - document_type_name, - block_info, - deleter_id, - None, - &mut estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - )?; - self.apply_batch_low_level_drive_operations( - estimated_costs_only_with_layer_info, - transaction, - batch_operations, - drive_operations, - &platform_version.drive, - )?; - if let Some(owned_transaction) = owned_transaction { - self.commit_transaction(owned_transaction, &platform_version.drive)?; - } - Ok(()) - } -} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs index 1a70a431e92..3737d85c356 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use grovedb::TransactionArg; @@ -61,19 +60,9 @@ impl Drive { platform_version, previous_fee_versions, ), - 1 => self.delete_document_for_contract_id_v1( - document_id, - contract_id, - document_type_name, - block_info, - apply, - transaction, - platform_version, - previous_fee_versions, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_id".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs index 50e415bbb06..934fa62531f 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v0/mod.rs @@ -56,8 +56,9 @@ impl Drive { document_id, contract, document_type_name, + &block_info, + None, estimated_costs_only_with_layer_info, - block_info.time_ms, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs deleted file mode 100644 index e2b056d54ad..00000000000 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id/v1/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -use grovedb::batch::KeyInfoPath; - -use grovedb::{EstimatedLayerInformation, TransactionArg}; - -use std::collections::HashMap; - -use crate::drive::Drive; -use crate::error::document::DocumentError; -use dpp::block::block_info::BlockInfo; -use dpp::fee::default_costs::CachedEpochIndexFeeVersions; - -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; - -use dpp::fee::fee_result::FeeResult; -use dpp::identifier::Identifier; -use dpp::version::PlatformVersion; - -impl Drive { - /// Deletes a document and returns the associated fee. - /// The contract CBOR is given instead of the contract itself. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_id_v1( - &self, - document_id: Identifier, - contract_id: Identifier, - document_type_name: &str, - block_info: BlockInfo, - apply: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, - ) -> Result { - let mut drive_operations: Vec = vec![]; - let estimated_costs_only_with_layer_info = if apply { - None::> - } else { - Some(HashMap::new()) - }; - - let contract_fetch_info = self - .get_contract_with_fetch_info_and_add_to_operations( - contract_id.to_buffer(), - Some(&block_info.epoch), - true, - transaction, - &mut drive_operations, - platform_version, - )? - .ok_or(Error::Document(DocumentError::DataContractNotFound))?; - - let contract = &contract_fetch_info.contract; - - self.delete_document_for_contract_apply_and_add_to_operations_with_lifecycle( - document_id, - contract, - document_type_name, - &block_info, - // These fee-applying wrappers carry no signer, so a lifecycle record - // they write belongs to no identity and refunds nobody. - None, - estimated_costs_only_with_layer_info, - block_info.time_ms, - transaction, - &mut drive_operations, - platform_version, - )?; - let fees = Drive::calculate_fee( - None, - Some(drive_operations), - &block_info.epoch, - self.config.epochs_per_era, - platform_version, - previous_fee_versions, - )?; - - Ok(fees) - } -} diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs index 10d1297bdf0..6e54b7ff430 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/mod.rs @@ -4,7 +4,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::epoch::Epoch; +use dpp::block::block_info::BlockInfo; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; @@ -20,7 +20,9 @@ impl Drive { /// * `contract_id`: The ID of the contract that contains the document. /// * `document_type_name`: The name of the document type. /// * `owner_id`: The owner ID of the document. - /// * `epoch`: The epoch of the block. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record a + /// keep-history delete writes; `None` credits nobody. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. @@ -35,12 +37,12 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - epoch: &Epoch, + block_info: &BlockInfo, + deleter_id: Option, 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> { @@ -55,10 +57,10 @@ impl Drive { document_id, contract_id, document_type_name, - epoch, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ), diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs index 1349e7e07a6..fbb84eaa6f1 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_id_with_named_type_operations/v0/mod.rs @@ -10,7 +10,7 @@ use crate::error::document::DocumentError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; -use dpp::block::epoch::Epoch; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::identifier::Identifier; @@ -25,19 +25,19 @@ impl Drive { document_id: Identifier, contract_id: Identifier, document_type_name: &str, - epoch: &Epoch, + block_info: &BlockInfo, + deleter_id: Option, 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> { let mut operations = vec![]; let Some(contract_fetch_info) = self.get_contract_with_fetch_info_and_add_to_operations( contract_id.to_buffer(), - Some(epoch), + Some(&block_info.epoch), true, transaction, &mut operations, @@ -53,9 +53,10 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ) 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 08f71671ca9..a4004b2e1bf 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 @@ -23,104 +23,19 @@ impl Drive { /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record a + /// keep-history delete writes; `None` credits nobody. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. - /// * `drive_version`: The drive version to select the correct function version to run. + /// * `platform_version`: The platform version to select the correct function version to run. /// /// # Returns /// * `Ok(Vec)` if the operation was successful. /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. #[allow(clippy::too_many_arguments)] pub fn delete_document_for_contract_operations( - &self, - document_id: Identifier, - 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 estimated_costs_only_with_layer_info.is_none() { - self.prepare_document_time_range_ttl( - contract, - document_type, - block_time_ms, - transaction, - platform_version, - )?; - } - self.delete_document_for_contract_operations_without_ttl_drain( - document_id, - contract, - document_type, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ) - } - - /// Build against post-drain state. The caller must prepare the whole - /// batch before invoking this method; no cleanup occurs during conversion. - #[allow(clippy::too_many_arguments)] - pub(crate) fn delete_document_for_contract_operations_without_ttl_drain( - &self, - document_id: Identifier, - 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_document_for_contract_operations - { - 0 => self.delete_document_for_contract_operations_v0( - document_id, - contract, - document_type, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ), - 1 => self.delete_document_for_contract_operations_v1( - document_id, - 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: "delete_document_for_contract_operations".to_string(), - known_versions: vec![0, 1], - received: version, - })), - } - } - - /// Prepares a lifecycle-aware delete using the block and deleter that - /// authored the state transition. - #[allow(clippy::too_many_arguments)] - pub fn delete_document_for_contract_operations_with_lifecycle( &self, document_id: Identifier, contract: &DataContract, @@ -131,7 +46,6 @@ impl Drive { estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - block_time_ms: u64, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -139,12 +53,12 @@ impl Drive { self.prepare_document_time_range_ttl( contract, document_type, - block_time_ms, + block_info.time_ms, transaction, platform_version, )?; } - self.delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + self.delete_document_for_contract_operations_without_ttl_drain( document_id, contract, document_type, @@ -152,15 +66,15 @@ impl Drive { deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ) } - /// Builds a lifecycle-aware delete against post-drain state. + /// Build against post-drain state. The caller must prepare the whole + /// batch before invoking this method; no cleanup occurs during conversion. #[allow(clippy::too_many_arguments)] - pub(crate) fn delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + pub(crate) fn delete_document_for_contract_operations_without_ttl_drain( &self, document_id: Identifier, contract: &DataContract, @@ -171,7 +85,6 @@ impl Drive { estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - block_time_ms: u64, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -188,11 +101,11 @@ impl Drive { document_type, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, + block_info.time_ms, transaction, platform_version, ), - 1 => self.delete_document_for_contract_operations_with_lifecycle_v1( + 1 => self.delete_document_for_contract_operations_v1( document_id, contract, document_type, @@ -219,11 +132,12 @@ impl Drive { /// * `contract`: The contract that contains the document. /// * `document_type`: The type of the document. /// * `block_info`: The block this delete belongs to. - /// * `deleter_id`: The identity credited with the lifecycle record's bytes. + /// * `deleter_id`: The identity credited with the lifecycle record a + /// keep-history delete writes; `None` credits nobody. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. - /// * `drive_version`: The drive version to select the correct function version to run. + /// * `platform_version`: The platform version to select the correct function version to run. /// /// # Returns /// * `Ok(Vec)` if the operation was successful. @@ -234,11 +148,12 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type: DocumentTypeRef, + block_info: &BlockInfo, + deleter_id: Option, 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> { @@ -255,7 +170,7 @@ impl Drive { document_type, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, + block_info.time_ms, transaction, platform_version, ), @@ -263,9 +178,10 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ), diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index e2be7354b7e..0f1a4d29573 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -42,75 +42,10 @@ use dpp::identifier::Identifier; use dpp::version::PlatformVersion; impl Drive { - /// Prepares the operations for deleting a document through the entry point - /// that carries only the block time. - /// - /// Generation 1 records a lifecycle entry for a keep-history document. That - /// entry names the deletion time, which this signature carries, and credits - /// the record's bytes to the deleter, which it does not: an entry written - /// through here belongs to nobody, as with the fee-applying wrappers, and - /// refunds nobody when erased. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_operations_v1( - &self, - document_id: Identifier, - 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> { - self.delete_document_for_contract_operations_with_lifecycle_v1( - document_id, - contract, - document_type, - &BlockInfo::default_with_time(block_time_ms), - None, - previous_batch_operations, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - ) - } - - /// The forced counterpart of [`Self::delete_document_for_contract_operations_v1`]. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn force_delete_document_for_contract_operations_v1( - &self, - document_id: Identifier, - 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> { - self.force_delete_document_for_contract_operations_with_lifecycle_v1( - document_id, - contract, - document_type, - &BlockInfo::default_with_time(block_time_ms), - None, - previous_batch_operations, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - ) - } - /// Prepares the operations for deleting a document. #[inline(always)] #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_operations_with_lifecycle_v1( + pub(super) fn delete_document_for_contract_operations_v1( &self, document_id: Identifier, contract: &DataContract, @@ -130,7 +65,7 @@ impl Drive { ))); } - self.force_delete_document_for_contract_operations_with_lifecycle_v1( + self.force_delete_document_for_contract_operations_v1( document_id, contract, document_type, @@ -154,7 +89,7 @@ impl Drive { /// Every other document type is deleted exactly as v0 deletes it. #[inline(always)] #[allow(clippy::too_many_arguments)] - pub(super) fn force_delete_document_for_contract_operations_with_lifecycle_v1( + pub(super) fn force_delete_document_for_contract_operations_v1( &self, document_id: Identifier, contract: &DataContract, diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs index 9526ec981ac..b50e9660115 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/mod.rs @@ -1,5 +1,4 @@ mod v0; -mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -21,67 +20,19 @@ impl Drive { /// * `document_id`: The ID of the document to delete. /// * `contract`: The contract that contains the document. /// * `document_type_name`: The name of the document type. + /// * `block_info`: The block this delete belongs to. + /// * `deleter_id`: The identity credited with the lifecycle record a + /// keep-history delete writes; `None` credits nobody. /// * `previous_batch_operations`: Previous batch operations to include. /// * `estimated_costs_only_with_layer_info`: Estimated costs with layer info. /// * `transaction`: The transaction argument. - /// * `drive_version`: The drive version to select the correct function version to run. + /// * `platform_version`: The platform version to select the correct function version to run. /// /// # Returns /// * `Ok(Vec)` if the operation was successful. /// * `Err(DriveError::UnknownVersionMismatch)` if the drive version does not match known versions. #[allow(clippy::too_many_arguments)] pub(crate) fn delete_document_for_contract_with_named_type_operations( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - 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_document_for_contract_with_named_type_operations - { - 0 => self.delete_document_for_contract_with_named_type_operations_v0( - document_id, - contract, - document_type_name, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ), - 1 => self.delete_document_for_contract_with_named_type_operations_v1( - document_id, - contract, - document_type_name, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ), - version => Err(Error::Drive(DriveError::UnknownVersionMismatch { - method: "delete_document_for_contract_with_named_type_operations".to_string(), - known_versions: vec![0, 1], - received: version, - })), - } - } - - /// Prepares a lifecycle-aware delete of a document named by its type, using - /// the block and deleter that authored the state transition. - #[allow(clippy::too_many_arguments)] - pub(crate) fn delete_document_for_contract_with_named_type_operations_with_lifecycle( &self, document_id: Identifier, contract: &DataContract, @@ -92,7 +43,6 @@ impl Drive { estimated_costs_only_with_layer_info: &mut Option< HashMap, >, - block_time_ms: u64, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -104,16 +54,6 @@ impl Drive { .delete_document_for_contract_with_named_type_operations { 0 => self.delete_document_for_contract_with_named_type_operations_v0( - document_id, - contract, - document_type_name, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ), - 1 => self.delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( document_id, contract, document_type_name, @@ -121,13 +61,12 @@ impl Drive { deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "delete_document_for_contract_with_named_type_operations".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, })), } diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs index cddde01f1b0..47e05699e6b 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v0/mod.rs @@ -4,6 +4,7 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; +use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; use crate::drive::Drive; @@ -25,11 +26,12 @@ impl Drive { document_id: Identifier, contract: &DataContract, document_type_name: &str, + block_info: &BlockInfo, + deleter_id: Option, 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> { @@ -38,9 +40,10 @@ impl Drive { document_id, contract, document_type, + block_info, + deleter_id, previous_batch_operations, estimated_costs_only_with_layer_info, - block_time_ms, transaction, platform_version, ) diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs deleted file mode 100644 index b30bb409044..00000000000 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_with_named_type_operations/v1/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -use grovedb::batch::KeyInfoPath; - -use grovedb::{EstimatedLayerInformation, TransactionArg}; - -use std::collections::HashMap; - -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::DataContract; - -use crate::drive::Drive; - -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; - -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::identifier::Identifier; - -use dpp::version::PlatformVersion; - -impl Drive { - /// Prepares the operations for deleting a document named by its type - /// through the entry point that carries only the block time. - /// - /// Generation 1 records a lifecycle entry for a keep-history document. That - /// entry names the deletion time, which this signature carries, and credits - /// the record's bytes to the deleter, which it does not: an entry written - /// through here belongs to nobody, as with the fee-applying wrappers, and - /// refunds nobody when erased. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_with_named_type_operations_v1( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - 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> { - self.delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( - document_id, - contract, - document_type_name, - &BlockInfo::default_with_time(block_time_ms), - None, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ) - } - - /// Prepares the operations for deleting a document. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn delete_document_for_contract_with_named_type_operations_with_lifecycle_v1( - &self, - document_id: Identifier, - contract: &DataContract, - document_type_name: &str, - block_info: &BlockInfo, - deleter_id: Option, - 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> { - let document_type = contract.document_type_for_name(document_type_name)?; - self.delete_document_for_contract_operations_with_lifecycle( - document_id, - contract, - document_type, - block_info, - deleter_id, - previous_batch_operations, - estimated_costs_only_with_layer_info, - block_time_ms, - transaction, - platform_version, - ) - } -} diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 62272e5c067..1e8d2ba6b55 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -1506,9 +1506,10 @@ mod tests { document_id, &contract, document_type, + &BlockInfo::default_with_time(1_000), + None, None, &mut None, - 1_000, None, platform_version, ) @@ -1530,9 +1531,10 @@ mod tests { document_id, &contract, "person", + &BlockInfo::default_with_time(1_000), + None, None, &mut None, - 1_000, None, platform_version, ) @@ -1554,8 +1556,9 @@ mod tests { document_id, &contract, "person", + &BlockInfo::default_with_time(1_000), + None, None, - 1_000, None, &mut vec![], platform_version, @@ -1658,10 +1661,10 @@ mod tests { random_document_id, nonexistent_contract_id, "profile", - &epoch, + &BlockInfo::default_with_epoch(epoch), + None, None, &mut estimated_costs_only_with_layer_info, - 0, None, platform_version, ) diff --git a/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs index 5deaf230552..7282109e6fd 100644 --- a/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs +++ b/packages/rs-drive/src/drive/document/insert/add_document_for_contract/time_range_index_e2e_tests.rs @@ -505,6 +505,7 @@ fn assert_ttl_document_batch(case: TtlBatchCase, ttl: bool) { .map(|i| { DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { document_id: docs[i].id(), + deleter_id: None, contract_info: DataContractInfo::BorrowedDataContract(&contract), document_type_info: DocumentTypeInfo::DocumentTypeRef(dt), }) @@ -736,6 +737,7 @@ fn raw_conversion_refuses_ttl_operations_and_requires_a_transaction() { vec![DriveOperation::DocumentOperation( DocumentOperationType::DeleteDocument { document_id: doc.id(), + deleter_id: None, contract_info: DataContractInfo::BorrowedDataContract(&contract), document_type_info: DocumentTypeInfo::DocumentTypeRef(dt), }, @@ -3819,9 +3821,10 @@ fn ttl_budget_boundary_after_zero_tree_keeps_deletes_exact() { document.id(), &contract, document_type, + &BlockInfo::default_with_time(after_expiry_ms), + None, None, &mut None, - after_expiry_ms, None, platform_version, ) @@ -4887,6 +4890,7 @@ fn apply_drive_operations_rolls_back_ttl_preparation_when_conversion_fails_witho vec![DriveOperation::DocumentOperation( DocumentOperationType::DeleteDocument { document_id: missing, + deleter_id: None, contract_info: DataContractInfo::BorrowedDataContract(&contract), document_type_info: DocumentTypeInfo::DocumentTypeRef(document_type), }, diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index f6e75461b5f..deb7ffb0ef5 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -177,7 +177,7 @@ fn delete( let mut operations = vec![]; let block_info = BlockInfo::default_with_time(time_ms); let batch = drive - .delete_document_for_contract_operations_with_lifecycle( + .delete_document_for_contract_operations( id, contract, document_type_of(contract), @@ -185,7 +185,6 @@ fn delete( Some(deleter), None, &mut None, - time_ms, None, latest(), ) @@ -926,7 +925,7 @@ fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_delete ); let batch = drive - .delete_document_for_contract_operations_with_lifecycle( + .delete_document_for_contract_operations( document.id(), &contract, document_type, @@ -934,7 +933,6 @@ fn should_decrement_the_count_and_the_sum_when_a_keep_history_document_is_delete Some(Identifier::new([7; 32])), None, &mut None, - 5_000, None, version, ) @@ -1249,7 +1247,7 @@ fn should_refund_each_writer_under_the_epoch_they_wrote_in() { ..BlockInfo::default_with_time(5_000) }; let batch = drive - .delete_document_for_contract_operations_with_lifecycle( + .delete_document_for_contract_operations( id, &contract, document_type, @@ -1257,7 +1255,6 @@ fn should_refund_each_writer_under_the_epoch_they_wrote_in() { Some(Identifier::new(early_writer)), None, &mut None, - block_info.time_ms, None, version, ) @@ -1528,7 +1525,7 @@ fn should_refuse_rather_than_half_apply_two_deletes_sharing_a_new_container() { .expect("expected a second document"); let delete_operation = |id: Identifier| { - DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { + DriveOperation::DocumentOperation(DocumentOperationType::DeleteDocument { document_id: id, deleter_id: Some(Identifier::new(owner)), contract_info: DataContractInfo::BorrowedDataContract(&contract), @@ -1817,7 +1814,7 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { }; apply( drive - .delete_document_for_contract_operations_with_lifecycle( + .delete_document_for_contract_operations( id, &contract, document_type, @@ -1825,7 +1822,6 @@ fn should_keep_refusing_by_revision_reads_of_a_gapped_history_after_deletion() { Some(Identifier::new(owner)), None, &mut None, - 5_000, None, new, ) @@ -1911,7 +1907,7 @@ fn should_keep_the_contract_fetch_cost_of_a_lifecycle_delete_by_contract_id() { let block_info = BlockInfo::default_with_time(5_000); let delete_with = |contract_info: DataContractInfo| { - DocumentOperationType::DeleteDocumentWithLifecycle { + DocumentOperationType::DeleteDocument { document_id: id, deleter_id: Some(Identifier::new(owner)), contract_info, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs index faf8216f940..d0d42d34423 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs @@ -49,6 +49,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { }), DocumentOperation(DocumentOperationType::DeleteDocument { document_id: base.id(), + deleter_id: None, contract_info: DataContractInfo::DataContractFetchInfo( base.data_contract_fetch_info(), ), @@ -96,7 +97,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { contract_id: data_contract_id.into_buffer(), nonce: identity_contract_nonce, }), - DocumentOperation(DocumentOperationType::DeleteDocumentWithLifecycle { + DocumentOperation(DocumentOperationType::DeleteDocument { document_id: base.id(), deleter_id: Some(owner_id), contract_info: DataContractInfo::DataContractFetchInfo( 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 d82ec5b50a8..6633d182b8d 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 @@ -98,17 +98,8 @@ pub enum DocumentOperationType<'a> { DeleteDocument { /// The document id document_id: Identifier, - /// Data Contract info to potentially be resolved if needed - contract_info: DataContractInfo<'a>, - /// Document type - document_type_info: DocumentTypeInfo<'a>, - }, - /// Deletes a document while retaining the lifecycle metadata needed by a - /// keep-history document. - DeleteDocumentWithLifecycle { - /// The document id - document_id: Identifier, - /// The identity credited with the lifecycle record's bytes. + /// The identity credited with the lifecycle record a keep-history + /// delete writes; `None` credits nobody. deleter_id: Option, /// Data Contract info to potentially be resolved if needed contract_info: DataContractInfo<'a>, @@ -234,11 +225,6 @@ impl DocumentOperationType<'_> { document_type_info, .. } - | Self::DeleteDocumentWithLifecycle { - contract_info, - document_type_info, - .. - } | Self::DeleteIndexOnlyDocument { contract_info, document_type_info, @@ -453,33 +439,6 @@ impl DocumentOperationType<'_> { Ok(batch_operations) } DocumentOperationType::DeleteDocument { - document_id, - contract_info, - document_type_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 = document_type_info.resolve(contract)?; - - drive.delete_document_for_contract_operations_without_ttl_drain( - document_id, - contract, - document_type, - None, - estimated_costs_only_with_layer_info, - block_info.time_ms, - transaction, - platform_version, - ) - } - DocumentOperationType::DeleteDocumentWithLifecycle { document_id, deleter_id, contract_info, @@ -500,7 +459,7 @@ impl DocumentOperationType<'_> { // its operations ahead of the delete's so the caller pays for // both. let mut operations = drive - .delete_document_for_contract_operations_with_lifecycle_without_ttl_drain( + .delete_document_for_contract_operations_without_ttl_drain( document_id, contract, document_type, @@ -508,7 +467,6 @@ impl DocumentOperationType<'_> { deleter_id, None, estimated_costs_only_with_layer_info, - block_info.time_ms, transaction, platform_version, )?; diff --git a/packages/rs-drive/tests/drive_storage_ops_coverage.rs b/packages/rs-drive/tests/drive_storage_ops_coverage.rs index 6403e728fec..08d95bb4a92 100644 --- a/packages/rs-drive/tests/drive_storage_ops_coverage.rs +++ b/packages/rs-drive/tests/drive_storage_ops_coverage.rs @@ -871,6 +871,7 @@ mod document_operation_tests { let id = dpp::prelude::Identifier::new([2u8; 32]); let _op = DocumentOperationType::DeleteDocument { document_id: id, + deleter_id: None, contract_info: drive::util::object_size_info::DataContractInfo::DataContractId( dpp::prelude::Identifier::new([3u8; 32]), ), 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 index 4f3ff5d9972..66c4e384550 100644 --- 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 @@ -27,11 +27,7 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V5: DriveDocumentMethodVersions = delete: DriveDocumentDeleteMethodVersions { remove_reference_for_index_level_for_contract_operations: 2, add_estimation_costs_for_remove_document_to_primary_storage: 1, - delete_document_for_contract: 1, - delete_document_for_contract_id: 1, - delete_document_for_contract_apply_and_add_to_operations: 1, remove_document_from_primary_storage: 1, - delete_document_for_contract_with_named_type_operations: 1, delete_document_for_contract_operations: 1, erase_document_for_contract_operations: Some(0), add_estimation_costs_for_erase_document: Some(0), From c599ee2868db85d5ebed8ab6205d896092798410 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 02:24:13 +0700 Subject: [PATCH 45/48] docs(book): a new input changes a method's signature, not the method count State the rule that the delete lifecycle work first missed: when a new generation needs an input the callers do not pass yet, the method's signature grows and the callers change; a twin method, a twin operation variant or a wrapper that fabricates the value are the wrong shapes. The shipped generation keeps its own signature and receives the part of the input it always had. The delete entry points serve as the sample. Co-Authored-By: Claude Fable 5.1 --- book/src/contributing/coding-conventions.md | 71 +++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/book/src/contributing/coding-conventions.md b/book/src/contributing/coding-conventions.md index 19b6fda27e1..8b7a38607cb 100644 --- a/book/src/contributing/coding-conventions.md +++ b/book/src/contributing/coding-conventions.md @@ -92,6 +92,77 @@ the method's number in the new protocol version's tables only. Duplication between generations is the accepted cost; it is cheaper than a drift-prone flag. +### A new input changes the signature, not the method count + +When the new generation of a method needs something its callers do not pass +yet (the block, the signer, a mode), add the parameter to the method and +update the callers. Do not add a second method beside it (`*_with_lifecycle`, +`*_with_block`), a second operation variant beside the existing one +(`DeleteDocumentWithLifecycle` next to `DeleteDocument`), or a wrapper that +fabricates the missing value on the way in. One method per directory means +one signature, the one the current generation needs. + +The shipped generation keeps its own signature. The dispatcher hands it the +part of the new input it always had and drops the rest: + +```rust +pub fn delete_document_for_contract_operations( + &self, + document_id: Identifier, + contract: &DataContract, + document_type: DocumentTypeRef, + block_info: &BlockInfo, // was `block_time_ms: u64` + deleter_id: Option, // new: whom the lifecycle record credits + previous_batch_operations: Option<&mut Vec>, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result, Error> { + match platform_version.drive.methods.document.delete.delete_document_for_contract_operations { + // The shipped generation never knew a deleter; it reads the time off + // the block exactly as it read `block_time_ms` before. + 0 => self.delete_document_for_contract_operations_v0( + document_id, + contract, + document_type, + previous_batch_operations, + estimated_costs_only_with_layer_info, + block_info.time_ms, + transaction, + platform_version, + ), + 1 => self.delete_document_for_contract_operations_v1( + document_id, + contract, + document_type, + block_info, + deleter_id, + previous_batch_operations, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { .. })), + } +} +``` + +Why: a twin method is a second name for the same behaviour with the version +decision split across two dispatchers, and callers that reach the old name +silently get the old capability at the new protocol version, or a value +somebody made up to satisfy the old signature. Changing the signature makes +the compiler find every caller, and each one then states what it passes. + +How: change the dispatcher's signature, re-point the callers, and let +generation 0 keep receiving what it received before. If a wrapper's new +generation would only forward the new parameter to the method that has the +behaviour, it is not a new generation: forward it from the wrapper's existing +body and leave the wrapper's version slot alone. The one exception is a +public client-facing API that must stay source-compatible across a release; +there, the SDK's builder pattern absorbs the new input. + ### Table versions follow protocol-version boundaries, not PRs Version-table constants (`DRIVE_ABCI_VALIDATION_VERSIONS_V10`, From aa432602398980020a7211169c45933e8c315997 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 02:50:42 +0700 Subject: [PATCH 46/48] refactor(wasm-sdk): drop the exact-integer selector type the deserializer already enforces The history selectors were wrapped in a type that refused a JavaScript number past `Number.MAX_SAFE_INTEGER` and took a `bigint` instead. The deserializer every query input already goes through does exactly that for a plain `u64`: a number is accepted only while it is a safe integer, a `bigint` at any `u64`, and anything else is an invalid type. The wrapper duplicated that rule behind a friendlier message, and its host test pinned `serde_json` rather than the deserializer the binding uses. The selectors are plain `u64` again. A wasm32 test builds the query object the way a caller does and pins the deserializer's rule: a `bigint` past the safe range arrives exact, and a number past it, a negative number or a fraction is refused. Co-Authored-By: Claude Fable 5.1 --- packages/wasm-sdk/src/queries/document.rs | 138 +++++++--------------- 1 file changed, 41 insertions(+), 97 deletions(-) diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 137f39904a5..37f3b4706bc 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -240,71 +240,18 @@ struct DocumentHistoryQueryInput { data_contract_id: IdentifierWasm, document_type_name: String, document_id: IdentifierWasm, - start_at_ms: Option, + start_at_ms: Option, start_after: Option, - start_at_revision: Option, - revision: Option, + start_at_revision: Option, + revision: Option, limit: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct DocumentHistoryCursorInput { - time_ms: ExactU64, - revision: ExactU64, -} - -/// A `u64` selector that a JavaScript caller may pass as a `bigint` or as a -/// `number`, where a `number` is accepted only while it is a safe integer. -/// -/// JavaScript has already rounded any `number` past `Number.MAX_SAFE_INTEGER` -/// by the time it reaches Rust, so silently converting it would query and -/// verify a different timestamp or revision than the caller asked for. A -/// `bigint` carries the exact value. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct ExactU64(u64); - -impl<'de> serde::Deserialize<'de> for ExactU64 { - fn deserialize>(deserializer: D) -> Result { - struct Visitor; - - impl serde::de::Visitor<'_> for Visitor { - type Value = ExactU64; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a non-negative integer: a bigint, or a number no larger than Number.MAX_SAFE_INTEGER") - } - - fn visit_u64(self, value: u64) -> Result { - Ok(ExactU64(value)) - } - - fn visit_i64(self, value: i64) -> Result { - u64::try_from(value) - .map(ExactU64) - .map_err(|_| E::custom("history selectors cannot be negative")) - } - - fn visit_f64(self, value: f64) -> Result { - const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; - if value.fract() != 0.0 || !(0.0..=MAX_SAFE_INTEGER).contains(&value) { - return Err(E::custom( - "history selectors must be non-negative integers; pass a bigint for \ - values past Number.MAX_SAFE_INTEGER", - )); - } - Ok(ExactU64(value as u64)) - } - } - - deserializer.deserialize_any(Visitor) - } -} - -impl From for u64 { - fn from(value: ExactU64) -> Self { - value.0 - } + time_ms: u64, + revision: u64, } fn parse_document_history_query( @@ -324,16 +271,16 @@ fn parse_document_history_query( )); } let filter = if let Some(time) = input.start_at_ms { - DocumentHistoryFilter::StartAtTime(time.into()) + DocumentHistoryFilter::StartAtTime(time) } else if let Some(cursor) = input.start_after { DocumentHistoryFilter::StartAfter { - time_ms: cursor.time_ms.into(), - revision: cursor.revision.into(), + time_ms: cursor.time_ms, + revision: cursor.revision, } } else if let Some(revision) = input.start_at_revision { - DocumentHistoryFilter::StartAtRevision(revision.into()) + DocumentHistoryFilter::StartAtRevision(revision) } else { - DocumentHistoryFilter::Revision(input.revision.expect("one filter was checked").into()) + DocumentHistoryFilter::Revision(input.revision.expect("one filter was checked")) }; Ok(DocumentHistoryQuery { data_contract_id: input.data_contract_id.into(), @@ -1690,6 +1637,37 @@ mod history_wasm_tests { ); } + /// A `u64` selector crosses from JavaScript exactly or not at all: the + /// deserializer takes a `number` only while it is a safe integer, since a + /// larger one was rounded before it reached Rust, and takes a `bigint` at + /// any `u64`. Nothing in this crate adds to that; this pins that it holds. + #[wasm_bindgen_test] + fn should_take_history_selectors_as_exact_integers_only() { + let query = |start_at_ms: JsValue| { + let object = js_sys::Object::new(); + let id = IdentifierWasm::from([1u8; 32]).to_base58(); + Reflect::set(&object, &"dataContractId".into(), &id.clone().into()).unwrap(); + Reflect::set(&object, &"documentTypeName".into(), &"note".into()).unwrap(); + Reflect::set(&object, &"documentId".into(), &id.into()).unwrap(); + Reflect::set(&object, &"startAtMs".into(), &start_at_ms).unwrap(); + parse_document_history_query(JsValue::from(object).unchecked_into()) + }; + let past_safe = (1u64 << 53) + 1; + + let parsed = query(JsValue::from(js_sys::BigInt::from(past_safe))) + .expect("a bigint carries the exact value"); + assert_eq!( + parsed.filter, + DocumentHistoryFilter::StartAtTime(past_safe), + "the selector must be the value the caller passed" + ); + + query(JsValue::from(past_safe as f64)) + .expect_err("a number past Number.MAX_SAFE_INTEGER was already rounded and is refused"); + query(JsValue::from(-1.0)).expect_err("a negative number is refused"); + query(JsValue::from(1.5)).expect_err("a fractional number is refused"); + } + #[wasm_bindgen_test] fn should_preserve_same_time_revisions_and_exact_lifecycle_counts_in_javascript() { let count = (1u64 << 53) + 1; @@ -1852,37 +1830,3 @@ mod history_wasm_tests { ); } } - -#[cfg(test)] -mod exact_u64_tests { - use super::ExactU64; - - /// A safe integer passes whether it arrives as an integer or as a whole - /// floating-point number; anything a JavaScript `number` has already - /// rounded, and anything fractional or negative, is refused. - #[test] - fn should_accept_only_exact_non_negative_integers() { - assert_eq!( - serde_json::from_str::("12").unwrap(), - ExactU64(12) - ); - assert_eq!( - serde_json::from_str::("9007199254740991").unwrap(), - ExactU64(9_007_199_254_740_991) - ); - assert_eq!( - serde_json::from_str::("18446744073709551615").unwrap(), - ExactU64(u64::MAX) - ); - assert_eq!( - serde_json::from_str::("12.0").unwrap(), - ExactU64(12) - ); - for rejected in ["12.5", "-1", "-1.0", "9007199254740992.0", "1e300"] { - assert!( - serde_json::from_str::(rejected).is_err(), - "{rejected} must be refused" - ); - } - } -} From 6cdd76f14a9428652c56961947bd3e6dc5de4bd0 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 04:42:27 +0700 Subject: [PATCH 47/48] refactor(drive)!: keep the lifecycle record in dpp and serialize it with platform serialization The lifecycle record of a keep-history document is read by proof verifiers as well as by Drive, so it belongs with the other stored types in rs-dpp rather than in Drive's own module. It moves to `dpp::document::lifecycle::DocumentLifecycleRecord` as a versioned enum over `DocumentLifecycleRecordV0` with the ordinary PlatformSerialize / PlatformDeserialize derive stack, and the hand-rolled fixed-width codec goes away. The fixed width existed so that an erase start could overwrite the record without changing its size. Nothing else in the codebase encodes that way, and a size change on overwrite is already handled: the batch apply merges the new bytes into the element's storage flags, so the deleter stays the beneficiary of what it paid for and the erase is charged for what its fields add. The estimator and the dry run now use the record's maximum encoded size, so the admission estimate stays an upper bound. Wire-visible only through stored bytes at protocol 15, which is unreleased. Co-Authored-By: Claude Fable 5.1 --- packages/rs-dpp/src/document/lifecycle/mod.rs | 209 ++++++++++++++ .../rs-dpp/src/document/lifecycle/v0/mod.rs | 32 +++ packages/rs-dpp/src/document/mod.rs | 1 + .../v1/mod.rs | 9 +- .../v0/mod.rs | 20 +- .../v0/mod.rs | 4 +- .../drive/document/lifecycle/fetch/v0/mod.rs | 5 +- .../src/drive/document/lifecycle/mod.rs | 269 +----------------- .../src/drive/document/lifecycle/tests.rs | 14 +- .../src/query/document_history_drive_query.rs | 7 +- 10 files changed, 280 insertions(+), 290 deletions(-) create mode 100644 packages/rs-dpp/src/document/lifecycle/mod.rs create mode 100644 packages/rs-dpp/src/document/lifecycle/v0/mod.rs diff --git a/packages/rs-dpp/src/document/lifecycle/mod.rs b/packages/rs-dpp/src/document/lifecycle/mod.rs new file mode 100644 index 00000000000..84cccde5177 --- /dev/null +++ b/packages/rs-dpp/src/document/lifecycle/mod.rs @@ -0,0 +1,209 @@ +//! The lifecycle record of a keep-history document. +//! +//! A keep-history document that has been deleted keeps its retained revisions +//! but loses its current pointer and its index references, so nothing in the +//! primary-key tree distinguishes it from a document that never existed. The +//! record supplies that distinction: it exists exactly while the document is +//! deleted or erasing, names the block time it was deleted at, and, once an +//! erasure has been authorized, the block time that erasure started and the +//! revision it started from. Drive stores it beside the document's history and +//! proof verifiers decode it from a proof. + +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_versioning::PlatformVersioned; + +pub mod v0; + +pub use v0::DocumentLifecycleRecordV0; + +/// Largest number of bytes an encoded record can occupy: the format +/// discriminant and six variable-length `u64`s at their widest. Estimates +/// that have to price a record without reading it use this bound. +pub const DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE: u32 = 1 + 6 * 9; + +/// A lifecycle record in whichever layout it was written with. +#[derive( + Debug, + Clone, + Copy, + Encode, + Decode, + DecodeUntrusted, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSerialize, + PlatformVersioned, + From, + PartialEq, + Eq, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "$formatVersion") +)] +#[platform_serialize(unversioned)] +pub enum DocumentLifecycleRecord { + /// The original layout. + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(DocumentLifecycleRecordV0), +} + +impl DocumentLifecycleRecord { + /// Builds the record a delete writes: the deletion time, what the deleted + /// document's revision was and how many revisions its history retained, + /// and no erasure. + pub fn deleted_at(deleted_at_ms: u64, latest_revision: u64, revision_count: u64) -> Self { + DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { + deleted_at_ms, + latest_revision, + revision_count, + ..Default::default() + }) + } + + /// Block time the document was deleted at. + pub fn deleted_at_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.deleted_at_ms, + } + } + + /// Revision the document carried when it was deleted. + pub fn latest_revision(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.latest_revision, + } + } + + /// Number of revisions the history retained when the document was deleted. + pub fn revision_count(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.revision_count, + } + } + + /// Whether the revisions retained at deletion numbered one through the + /// deleted revision without a gap, which is what lets a by-revision read + /// map a revision onto a position in the history. + /// + /// A gap can only come from a history written before protocol 15, where + /// two writes in one block overwrote each other's revision; an erase + /// removes the newest revisions first and so never opens one. + pub fn revisions_are_contiguous(&self) -> bool { + self.latest_revision() == self.revision_count() + } + + /// Block time an authorized erasure started at, or zero while none has. + pub fn erasing_started_at_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_started_at_ms, + } + } + + /// Timestamp component of the newest revision retained at erase start. + pub fn erasing_from_time_ms(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_from_time_ms, + } + } + + /// History sequence of the newest revision retained at erase start. + pub fn erasing_from_revision(&self) -> u64 { + match self { + DocumentLifecycleRecord::V0(v0) => v0.erasing_from_revision, + } + } + + /// Whether an authorized erasure has already started, which is what lets a + /// continuation run without any authorization of its own. + /// + /// Read off the revision the erasure started from rather than the time it + /// started at: a history sequence is one or more for every real revision, + /// so zero can only mean the field was never written, while a block time of + /// zero is a value a clock could in principle produce. + pub fn is_erasing(&self) -> bool { + self.erasing_from_revision() != 0 + } + + /// Returns the record an erase start writes over this one, keeping the + /// deletion time and recording the erasure it authorizes. + pub fn starting_erase_at( + &self, + erasing_started_at_ms: u64, + erasing_from_time_ms: u64, + erasing_from_revision: u64, + ) -> Self { + DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { + deleted_at_ms: self.deleted_at_ms(), + latest_revision: self.latest_revision(), + revision_count: self.revision_count(), + erasing_started_at_ms, + erasing_from_time_ms, + erasing_from_revision, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; + + #[test] + fn should_round_trip_every_field() { + let record = DocumentLifecycleRecord::deleted_at(7, 12, 11).starting_erase_at(8, 9, 10); + let bytes = record.serialize_to_bytes().expect("serialize"); + let recovered = + DocumentLifecycleRecord::deserialize_from_bytes_untrusted(&bytes).expect("round trip"); + assert_eq!(record, recovered); + assert!(recovered.is_erasing()); + assert_eq!(recovered.latest_revision(), 12); + assert_eq!(recovered.revision_count(), 11); + assert!(!recovered.revisions_are_contiguous()); + assert!(DocumentLifecycleRecord::deleted_at(7, 11, 11).revisions_are_contiguous()); + } + + /// The bound prices a record nobody has read yet, so it must hold for the + /// widest values every field can take. + #[test] + fn should_never_encode_past_the_size_bound() { + let widest = DocumentLifecycleRecord::deleted_at(u64::MAX, u64::MAX, u64::MAX) + .starting_erase_at(u64::MAX, u64::MAX, u64::MAX); + assert_eq!( + widest.serialize_to_bytes().expect("serialize").len(), + DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE as usize + ); + let narrowest = DocumentLifecycleRecord::deleted_at(0, 0, 0); + assert!( + narrowest.serialize_to_bytes().expect("serialize").len() + <= DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE as usize + ); + } + + #[test] + fn should_report_a_record_without_erase_fields_as_deleted_not_erasing() { + let record = DocumentLifecycleRecord::deleted_at(7, 1, 1); + assert!(!record.is_erasing()); + assert_eq!(record.erasing_from_revision(), 0); + } + + /// A block whose time is zero must not make a committed erasure look like + /// one that never started. + #[test] + fn should_report_an_erasure_started_in_a_block_at_time_zero_as_erasing() { + let record = DocumentLifecycleRecord::deleted_at(0, 1, 1).starting_erase_at(0, 0, 1); + assert!(record.is_erasing()); + } + + #[test] + fn should_reject_bytes_that_are_not_a_record() { + assert!(DocumentLifecycleRecord::deserialize_from_bytes_untrusted(&[]).is_err()); + assert!(DocumentLifecycleRecord::deserialize_from_bytes_untrusted(&[9]).is_err()); + } +} diff --git a/packages/rs-dpp/src/document/lifecycle/v0/mod.rs b/packages/rs-dpp/src/document/lifecycle/v0/mod.rs new file mode 100644 index 00000000000..c06cbdc7c26 --- /dev/null +++ b/packages/rs-dpp/src/document/lifecycle/v0/mod.rs @@ -0,0 +1,32 @@ +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; + +/// What the committed state says about one keep-history document that is no +/// longer visible to ordinary reads. +#[derive(Debug, Clone, Copy, Encode, Decode, DecodeUntrusted, From, PartialEq, Eq, Default)] +#[cfg_attr( + feature = "serde-conversion", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +pub struct DocumentLifecycleRecordV0 { + /// Block time the document was deleted at. + pub deleted_at_ms: u64, + /// Revision the document carried when it was deleted. + pub latest_revision: u64, + /// Number of revisions the history retained when the document was deleted. + /// + /// Together with `latest_revision` this says whether the retained history + /// was contiguous: an erase removes the newest revisions first, so what + /// survives afterwards is a prefix of what was retained here, and a + /// by-revision read stays meaningful exactly when the two are equal. + pub revision_count: u64, + /// Block time an authorized erasure started at, or zero while none has. + pub erasing_started_at_ms: u64, + /// Timestamp component of the newest revision retained when the erasure + /// started, or zero while none has. + pub erasing_from_time_ms: u64, + /// History sequence of the newest revision retained when the erasure + /// started, or zero while none has. + pub erasing_from_revision: u64, +} diff --git a/packages/rs-dpp/src/document/mod.rs b/packages/rs-dpp/src/document/mod.rs index ce4d97fba80..b573937b34b 100644 --- a/packages/rs-dpp/src/document/mod.rs +++ b/packages/rs-dpp/src/document/mod.rs @@ -13,6 +13,7 @@ pub mod errors; pub mod extended_document; mod fields; pub mod generate_document_id; +pub mod lifecycle; pub mod serialization_traits; #[cfg(feature = "factories")] pub mod specialized_document_factory; diff --git a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs index 0f1a4d29573..4a025fa4a83 100644 --- a/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/delete_document_for_contract_operations/v1/mod.rs @@ -6,7 +6,6 @@ use dpp::data_contract::document_type::DocumentTypeRef; use std::collections::HashMap; use crate::drive::constants::{DOCUMENT_HISTORY_CURRENT_REFERENCE_PATH_SIZE, STORAGE_FLAGS_SIZE}; -use crate::drive::document::lifecycle::DocumentLifecycleRecord; use crate::drive::document::paths::{ contract_documents_primary_key_path, document_history_path, document_lifecycle_path, DOCUMENT_LIFECYCLE_TREE_KEY, @@ -16,6 +15,8 @@ use crate::util::object_size_info::DocumentInfo::{ DocumentEstimatedAverageSize, DocumentOwnedInfo, }; use crate::util::storage_flags::StorageFlags; +use dpp::document::lifecycle::DocumentLifecycleRecord; +use dpp::serialization::PlatformSerializable; use dpp::block::block_info::BlockInfo; use dpp::data_contract::DataContract; @@ -349,13 +350,13 @@ impl Drive { batch_operations.append(&mut container_operations); // The record's content is fully known whether or not this is a dry - // run: it is a fixed-width encoding of the deletion time, and the - // deleter it is flagged to is an input to the delete. + // run: the deletion time and the revision counts come from the delete + // itself, and the deleter it is flagged to is an input to the delete. self.batch_insert::<0>( PathKeyElement(( lifecycle_path, document_id.to_vec(), - Element::Item(record.serialize(), element_flags), + Element::Item(record.serialize_to_bytes()?, element_flags), )), batch_operations, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs index 054996b7956..db7a4d0fe21 100644 --- a/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/erase_document_for_contract_operations/v0/mod.rs @@ -16,7 +16,6 @@ use grovedb::{ TreeType, }; -use crate::drive::document::lifecycle::{DocumentLifecycleRecord, DOCUMENT_LIFECYCLE_RECORD_SIZE}; use crate::drive::document::paths::{document_history_path, document_lifecycle_path}; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -29,6 +28,8 @@ use crate::util::grove_operations::BatchDeleteApplyType::{ use crate::util::grove_operations::{DirectQueryType, QueryTarget}; use crate::util::object_size_info::PathKeyElementInfo::{PathKeyElement, PathKeyElementSize}; use crate::util::storage_flags::StorageFlags; +use dpp::document::lifecycle::{DocumentLifecycleRecord, DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE}; +use dpp::serialization::{PlatformDeserializableTrusted, PlatformSerializable}; /// Length of a revision key: a block timestamp followed by a history sequence. const REVISION_KEY_LENGTH: usize = 16; @@ -199,8 +200,10 @@ impl Drive { return Ok(batch_operations); } let (time_ms, revision) = Self::decode_revision_key(&removable[0])?; - // Equal in size to the record it replaces, and carrying the same - // flags, so the deleter stays the beneficiary of its bytes. + // Written with the flags of the record it replaces, so the deleter + // stays the beneficiary of the bytes it paid for; any bytes the new + // fields add are charged to this erase and flagged as such by the + // batch apply. self.batch_insert::<0>( PathKeyElement(( lifecycle_path, @@ -208,7 +211,7 @@ impl Drive { Element::Item( record .starting_erase_at(block_info.time_ms, time_ms, revision) - .serialize(), + .serialize_to_bytes()?, flags, ), )), @@ -289,7 +292,10 @@ impl Drive { "a lifecycle record is not an item", ))); }; - Ok((DocumentLifecycleRecord::deserialize(&bytes)?, flags)) + Ok(( + DocumentLifecycleRecord::deserialize_from_bytes_trusted(&bytes)?, + flags, + )) } /// Splits a revision key into the block time and history sequence it @@ -359,7 +365,7 @@ impl Drive { document_id.as_slice(), DirectQueryType::StatelessDirectQuery { in_tree_type: TreeType::NormalTree, - query_target: QueryTarget::QueryTargetValue(DOCUMENT_LIFECYCLE_RECORD_SIZE), + query_target: QueryTarget::QueryTargetValue(DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE), }, transaction, &mut batch_operations, @@ -398,7 +404,7 @@ impl Drive { KeyInfoPath::from_known_owned_path(lifecycle_path.to_vec()), KeyInfo::KnownKey(document_id.to_vec()), Element::Item( - vec![0u8; DOCUMENT_LIFECYCLE_RECORD_SIZE as usize], + vec![0u8; DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE as usize], Some(vec![0u8; flags_len]), ), )), diff --git a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs index 7012b4e4e12..258a0aad544 100644 --- a/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/estimation_costs/add_estimation_costs_for_lifecycle_record/v0/mod.rs @@ -11,11 +11,11 @@ use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees}; use grovedb::EstimatedSumTrees::NoSumTrees; use grovedb::TreeType; -use crate::drive::document::lifecycle::DOCUMENT_LIFECYCLE_RECORD_SIZE; use crate::drive::document::paths::{contract_document_type_path_vec, document_lifecycle_path}; use crate::drive::Drive; use crate::error::Error; use crate::util::storage_flags::StorageFlags; +use dpp::document::lifecycle::DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE; impl Drive { /// Registers the layers a lifecycle record write or removal touches: the @@ -53,7 +53,7 @@ impl Drive { EstimatedLayerInformation { tree_type: TreeType::NormalTree, estimated_layer_count: PotentiallyAtMaxElements, - estimated_layer_sizes: AllItems(32, DOCUMENT_LIFECYCLE_RECORD_SIZE, flags_size), + estimated_layer_sizes: AllItems(32, DOCUMENT_LIFECYCLE_RECORD_MAX_SIZE, flags_size), }, ); Ok(()) diff --git a/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs index fb33daf317a..d3b5c2d6383 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/fetch/v0/mod.rs @@ -12,7 +12,6 @@ use grovedb::query_result_type::QueryResultType; use grovedb::{Element, PathQuery, Query, SizedQuery, TransactionArg}; use crate::drive::document::lifecycle::fetch::DocumentLifecycleState; -use crate::drive::document::lifecycle::DocumentLifecycleRecord; use crate::drive::document::paths::{ contract_documents_primary_key_path, document_history_path, document_lifecycle_path, }; @@ -21,6 +20,8 @@ use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::util::grove_operations::{DirectQueryType, QueryType}; +use dpp::document::lifecycle::DocumentLifecycleRecord; +use dpp::serialization::PlatformDeserializableTrusted; impl Drive { #[inline(always)] @@ -121,7 +122,7 @@ impl Drive { let Some(record_bytes) = record_bytes else { return Ok(DocumentLifecycleState::Absent); }; - let record = DocumentLifecycleRecord::deserialize(&record_bytes)?; + let record = DocumentLifecycleRecord::deserialize_from_bytes_trusted(&record_bytes)?; if record.is_erasing() { return Ok(DocumentLifecycleState::Erasing); } diff --git a/packages/rs-drive/src/drive/document/lifecycle/mod.rs b/packages/rs-drive/src/drive/document/lifecycle/mod.rs index b7204c4feb0..c3576b7906d 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/mod.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/mod.rs @@ -1,13 +1,7 @@ -//! The lifecycle record of a keep-history document, and the per-type tree that holds it. -//! -//! A keep-history document that has been deleted keeps its retained revisions -//! but loses its current pointer and its index references, so nothing in the -//! primary-key tree distinguishes it from a document that never existed. The -//! record supplies that distinction: it exists exactly while the document is -//! deleted or erasing, names the block time it was deleted at, and — once an -//! erasure has been authorized — the block time that erasure started and the -//! revision it started from. +//! The per-type tree that holds the lifecycle records of keep-history +//! documents, and the reads over it. //! +//! The record itself is `dpp::document::lifecycle::DocumentLifecycleRecord`. //! It lives in its own tree under the document type rather than inside the //! document's history, so every key in the history stays in the revision //! domain and deleted documents stay enumerable per type. @@ -23,260 +17,3 @@ mod tests; #[cfg(feature = "server")] pub use fetch::DocumentLifecycleState; - -use crate::error::{drive::DriveError, Error}; - -/// Byte length of an encoded lifecycle record: one version byte and six -/// big-endian `u64`s. -pub const DOCUMENT_LIFECYCLE_RECORD_SIZE: u32 = 49; - -/// Version byte of the only record layout that exists. -const DOCUMENT_LIFECYCLE_RECORD_V0: u8 = 0; - -/// What the committed state says about one keep-history document that is no -/// longer visible to ordinary reads. -/// -/// The encoding is fixed width so that the deleted and erasing forms occupy -/// exactly the same number of bytes: an erase start overwrites the record in -/// place, and an overwrite of a different size would re-price storage and drop -/// the original writer's flags. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct DocumentLifecycleRecordV0 { - /// Block time the document was deleted at. - pub deleted_at_ms: u64, - /// Revision the document carried when it was deleted. - pub latest_revision: u64, - /// Number of revisions the history retained when the document was deleted. - /// - /// Together with `latest_revision` this says whether the retained history - /// was contiguous: an erase removes the newest revisions first, so what - /// survives afterwards is a prefix of what was retained here, and a - /// by-revision read stays meaningful exactly when the two are equal. - pub revision_count: u64, - /// Block time an authorized erasure started at, or zero while none has. - pub erasing_started_at_ms: u64, - /// Timestamp component of the newest revision retained when the erasure - /// started, or zero while none has. - pub erasing_from_time_ms: u64, - /// History sequence of the newest revision retained when the erasure - /// started, or zero while none has. - pub erasing_from_revision: u64, -} - -/// A lifecycle record in whichever layout it was written with. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DocumentLifecycleRecord { - /// The original layout. - V0(DocumentLifecycleRecordV0), -} - -impl DocumentLifecycleRecord { - /// Builds the record a delete writes: the deletion time, what the deleted - /// document's revision was and how many revisions its history retained, - /// and no erasure. - pub fn deleted_at(deleted_at_ms: u64, latest_revision: u64, revision_count: u64) -> Self { - DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { - deleted_at_ms, - latest_revision, - revision_count, - ..Default::default() - }) - } - - /// Block time the document was deleted at. - pub fn deleted_at_ms(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.deleted_at_ms, - } - } - - /// Revision the document carried when it was deleted. - pub fn latest_revision(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.latest_revision, - } - } - - /// Number of revisions the history retained when the document was deleted. - pub fn revision_count(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.revision_count, - } - } - - /// Whether the revisions retained at deletion numbered one through the - /// deleted revision without a gap, which is what lets a by-revision read - /// map a revision onto a position in the history. - /// - /// A gap can only come from a history written before protocol 15, where - /// two writes in one block overwrote each other's revision; an erase - /// removes the newest revisions first and so never opens one. - pub fn revisions_are_contiguous(&self) -> bool { - self.latest_revision() == self.revision_count() - } - - /// Block time an authorized erasure started at, or zero while none has. - pub fn erasing_started_at_ms(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.erasing_started_at_ms, - } - } - - /// Timestamp component of the newest revision retained at erase start. - pub fn erasing_from_time_ms(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.erasing_from_time_ms, - } - } - - /// History sequence of the newest revision retained at erase start. - pub fn erasing_from_revision(&self) -> u64 { - match self { - DocumentLifecycleRecord::V0(v0) => v0.erasing_from_revision, - } - } - - /// Whether an authorized erasure has already started, which is what lets a - /// continuation run without any authorization of its own. - /// - /// Read off the revision the erasure started from rather than the time it - /// started at: a history sequence is one or more for every real revision, - /// so zero can only mean the field was never written, while a block time of - /// zero is a value a clock could in principle produce. - pub fn is_erasing(&self) -> bool { - self.erasing_from_revision() != 0 - } - - /// Returns the record an erase start writes over this one, keeping the - /// deletion time and recording the erasure it authorizes. - pub fn starting_erase_at( - &self, - erasing_started_at_ms: u64, - erasing_from_time_ms: u64, - erasing_from_revision: u64, - ) -> Self { - DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { - deleted_at_ms: self.deleted_at_ms(), - latest_revision: self.latest_revision(), - revision_count: self.revision_count(), - erasing_started_at_ms, - erasing_from_time_ms, - erasing_from_revision, - }) - } - - /// Encodes the record as one version byte followed by six big-endian - /// `u64`s. - /// - /// The layout is fixed width rather than the variable-length integer - /// encoding used elsewhere, because a record whose erase fields are zero - /// must occupy exactly as many bytes as one whose erase fields are real - /// timestamps. - pub fn serialize(&self) -> Vec { - let mut bytes = Vec::with_capacity(DOCUMENT_LIFECYCLE_RECORD_SIZE as usize); - match self { - DocumentLifecycleRecord::V0(v0) => { - bytes.push(DOCUMENT_LIFECYCLE_RECORD_V0); - bytes.extend(v0.deleted_at_ms.to_be_bytes()); - bytes.extend(v0.latest_revision.to_be_bytes()); - bytes.extend(v0.revision_count.to_be_bytes()); - bytes.extend(v0.erasing_started_at_ms.to_be_bytes()); - bytes.extend(v0.erasing_from_time_ms.to_be_bytes()); - bytes.extend(v0.erasing_from_revision.to_be_bytes()); - } - } - bytes - } - - /// Decodes a record, requiring the whole input to be consumed. - /// - /// Trailing bytes are a decode failure rather than something to ignore: a - /// record is written only by consensus, so anything else at that key means - /// the state is not what this code believes it is. - pub fn deserialize(bytes: &[u8]) -> Result { - let corrupt = |message: &'static str| { - Error::Drive(DriveError::CorruptedDriveState(message.to_string())) - }; - let Some((version, fields)) = bytes.split_first() else { - return Err(corrupt("document lifecycle record is empty")); - }; - if *version != DOCUMENT_LIFECYCLE_RECORD_V0 { - return Err(corrupt("unknown document lifecycle record version")); - } - if fields.len() != 48 { - return Err(corrupt( - "document lifecycle record does not contain six fields", - )); - } - let field = |index: usize| -> u64 { - let mut buffer = [0u8; 8]; - buffer.copy_from_slice(&fields[index * 8..(index + 1) * 8]); - u64::from_be_bytes(buffer) - }; - Ok(DocumentLifecycleRecord::V0(DocumentLifecycleRecordV0 { - deleted_at_ms: field(0), - latest_revision: field(1), - revision_count: field(2), - erasing_started_at_ms: field(3), - erasing_from_time_ms: field(4), - erasing_from_revision: field(5), - })) - } -} - -#[cfg(test)] -mod record_tests { - use super::*; - - #[test] - fn should_encode_deleted_and_erasing_records_to_the_same_length() { - let deleted = DocumentLifecycleRecord::deleted_at(1_700_000_000_000, 42, 42); - let erasing = deleted.starting_erase_at(1_700_000_500_000, 1_699_000_000_000, 42); - assert_eq!( - deleted.serialize().len(), - erasing.serialize().len(), - "an erase start overwrites the record in place and must not change its size" - ); - assert_eq!( - deleted.serialize().len(), - DOCUMENT_LIFECYCLE_RECORD_SIZE as usize - ); - } - - #[test] - fn should_round_trip_every_field() { - let record = DocumentLifecycleRecord::deleted_at(7, 12, 11).starting_erase_at(8, 9, 10); - let recovered = - DocumentLifecycleRecord::deserialize(&record.serialize()).expect("round trip"); - assert_eq!(record, recovered); - assert!(recovered.is_erasing()); - assert_eq!(recovered.latest_revision(), 12); - assert_eq!(recovered.revision_count(), 11); - assert!(!recovered.revisions_are_contiguous()); - assert!(DocumentLifecycleRecord::deleted_at(7, 11, 11).revisions_are_contiguous()); - } - - #[test] - fn should_report_a_record_without_erase_fields_as_deleted_not_erasing() { - let record = DocumentLifecycleRecord::deleted_at(7, 1, 1); - assert!(!record.is_erasing()); - assert_eq!(record.erasing_from_revision(), 0); - } - - /// A block whose time is zero must not make a committed erasure look like - /// one that never started. - #[test] - fn should_report_an_erasure_started_in_a_block_at_time_zero_as_erasing() { - let record = DocumentLifecycleRecord::deleted_at(0, 1, 1).starting_erase_at(0, 0, 1); - assert!(record.is_erasing()); - } - - #[test] - fn should_reject_records_that_do_not_consume_their_whole_input() { - let mut bytes = DocumentLifecycleRecord::deleted_at(7, 1, 1).serialize(); - bytes.push(0); - assert!(DocumentLifecycleRecord::deserialize(&bytes).is_err()); - assert!(DocumentLifecycleRecord::deserialize(&[]).is_err()); - assert!(DocumentLifecycleRecord::deserialize(&[9; 49]).is_err()); - } -} diff --git a/packages/rs-drive/src/drive/document/lifecycle/tests.rs b/packages/rs-drive/src/drive/document/lifecycle/tests.rs index deb7ffb0ef5..2951eb4ddcc 100644 --- a/packages/rs-drive/src/drive/document/lifecycle/tests.rs +++ b/packages/rs-drive/src/drive/document/lifecycle/tests.rs @@ -3,7 +3,6 @@ //! from one transition to the next. use super::fetch::DocumentLifecycleState; -use super::DocumentLifecycleRecord; use crate::drive::document::paths::{document_history_path, document_lifecycle_path}; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -24,8 +23,10 @@ use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; +use dpp::document::lifecycle::DocumentLifecycleRecord; use dpp::document::{DocumentV0Getters, DocumentV0Setters}; use dpp::identifier::Identifier; +use dpp::serialization::PlatformDeserializableUntrusted; use dpp::tests::json_document::{json_document_to_contract, json_document_to_document}; use dpp::version::PlatformVersion; use grovedb::{MaybeTree, TransactionArg, TreeType}; @@ -125,7 +126,10 @@ fn record_of( &latest().drive, ) .expect("expected to read the lifecycle tree") - .map(|bytes| DocumentLifecycleRecord::deserialize(&bytes).expect("expected a valid record")) + .map(|bytes| { + DocumentLifecycleRecord::deserialize_from_bytes_untrusted(&bytes) + .expect("expected a valid record") + }) } fn visible_document_count(drive: &Drive, contract: &DataContract) -> usize { @@ -716,11 +720,7 @@ fn should_keep_the_deleter_as_the_records_beneficiary_across_an_erase_start() { else { panic!("a lifecycle record is an item"); }; - assert_eq!( - before.len(), - after.len(), - "the overwrite must be the same size" - ); + assert_ne!(before, after, "the erase start rewrote the record"); assert_eq!( before_flags, after_flags, "the deleter must stay the beneficiary of the record's bytes" diff --git a/packages/rs-drive/src/query/document_history_drive_query.rs b/packages/rs-drive/src/query/document_history_drive_query.rs index 77327e395e1..ccf0c132e65 100644 --- a/packages/rs-drive/src/query/document_history_drive_query.rs +++ b/packages/rs-drive/src/query/document_history_drive_query.rs @@ -1,7 +1,6 @@ //! A page of a document's retained revisions with its lifecycle, read from //! whichever history layout the protocol version stores. -use crate::drive::document::lifecycle::DocumentLifecycleRecord; use crate::drive::document::paths::{ contract_document_type_path_vec, DOCUMENT_HISTORY_TREE_KEY, DOCUMENT_LIFECYCLE_TREE_KEY, }; @@ -12,8 +11,10 @@ use crate::error::query::QuerySyntaxError; use crate::error::Error; use crate::verify::RootHash; use dpp::data_contract::document_type::{DocumentPropertyType, DocumentTypeRef}; +use dpp::document::lifecycle::DocumentLifecycleRecord; use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0; use dpp::document::{Document, DocumentV0Getters}; +use dpp::serialization::PlatformDeserializableUntrusted; use dpp::version::PlatformVersion; #[cfg(feature = "server")] use grovedb::TransactionArg; @@ -279,7 +280,9 @@ impl DocumentHistoryDriveQuery { return Err(corrupt("a lifecycle record is not an item")); }; if record - .replace(DocumentLifecycleRecord::deserialize(&bytes)?) + .replace(DocumentLifecycleRecord::deserialize_from_bytes_untrusted( + &bytes, + )?) .is_some() { return Err(corrupt("duplicate lifecycle record")); From b3e8fe23bdb50f31881691df2b7ac3a25f4ad0d5 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 22:06:25 +0700 Subject: [PATCH 48/48] docs(book): describe the keep-history document lifecycle and history query The book had no chapter on keep-history document types: nothing on the protocol 15 storage layout (history tree, primary-key reference, lifecycle tree), on deleting and erasing such documents, on the four lifecycle states, or on what getDocumentHistory reports. This adds a Drive chapter covering the layout, the contract grammar (canBeDeleted on a keep-history type, canBeErased, the contested-index refusal), the delete and erase semantics with chunking, authorization-once and refund accounting, the query's selectors, lifecycle fields and two-proof envelope, the Rust and JavaScript client surfaces, and the versioning boundary. The evo-sdk state-transitions page now lists erase() and history() and links to it. Co-Authored-By: Claude Fable 5.1 --- book/src/SUMMARY.md | 1 + book/src/drive/keep-history-documents.md | 292 +++++++++++++++++++++++ book/src/evo-sdk/state-transitions.md | 9 +- 3 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 book/src/drive/keep-history-documents.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 9d130158ca9..5d918c949c7 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -74,6 +74,7 @@ - [Ranked Index Examples](drive/ranked-index-examples.md) - [Time-Range Index TTL](drive/time-range-ttl.md) - [Index-Only Document Types](drive/index-only-document-types.md) +- [Keep-History Documents](drive/keep-history-documents.md) # Testing diff --git a/book/src/drive/keep-history-documents.md b/book/src/drive/keep-history-documents.md new file mode 100644 index 00000000000..1d5c4cf66cd --- /dev/null +++ b/book/src/drive/keep-history-documents.md @@ -0,0 +1,292 @@ +# Keep-History Documents + +> **Status:** implemented and gated at protocol version 15 (meta-schema v4 / +> parser generation 4). The storage layout, the delete and erase paths and +> the fee accounting are pinned against a real grovedb by +> [`lifecycle/tests.rs`](https://github.com/dashpay/platform/blob/v4.3-dev/packages/rs-drive/src/drive/document/lifecycle/tests.rs) +> in rs-drive; the full ABCI pipeline (transitions, validation, proofs, +> refunds) by the `keep_history`, `deletion`, `erase` and +> `lifecycle_contracts` modules under rs-drive-abci's batch tests; and the +> whole story against a running network by +> [`KeepHistoryDocument.spec.js`](https://github.com/dashpay/platform/blob/v4.3-dev/packages/platform-test-suite/test/functional/platform/KeepHistoryDocument.spec.js). + +A document type declared with `documentsKeepHistory: true` retains every +revision of every document instead of overwriting the previous one. This +chapter is the reference for how those revisions are stored, how a +keep-history document is deleted and later erased, what the four lifecycle +states mean, and how `getDocumentHistory` reports all of it. + +## Storage layout + +Every document type owns three reserved single-byte keys under its +document-type tree. Index trees are keyed by property names, which cannot +start below `0x30`, so no index can collide with them. + +```text +[DataContractDocuments, contract_id, 1, , 0, ] + → Reference to the current revision (the primary-key tree) + +[DataContractDocuments, contract_id, 1, , 1, ] + → Item() (the lifecycle tree) + +[DataContractDocuments, contract_id, 1, , 2, ] + → ProvableCountTree (the history tree) + [] → Item() +``` + +For a type that does not keep history the primary-key tree holds the +serialized document itself and keys `1` and `2` do not exist. For a +keep-history type: + +- **The history tree** holds every retained revision of one document, keyed + by the block time it was written at followed by its revision, both + big-endian `u64`. Two revisions written in one block therefore stay + distinct. It is a provable count tree, so how many revisions a document + retains is a single hash-bound read, and its absence is provable. +- **The primary-key entry** is a reference to the newest revision, so every + ordinary by-id read and every secondary index resolve through it. For a + summable type the reference also carries the document's sum contribution. +- **The lifecycle record** exists exactly while the document is deleted or + erasing. It is what tells a deleted document apart from an id that was + never used: both are absent from the primary-key tree. The record is + `DocumentLifecycleRecord` in `rs-dpp` (`packages/rs-dpp/src/document/lifecycle`), + a versioned enum with the ordinary platform-serialization derives, stored + with the storage flags of the identity that deleted the document. + +The record carries the block time the document was deleted at, the revision +it carried then, how many revisions the history retained at that moment, +and, once an erasure has been authorized, the block time the erasure started +and the newest revision that was retained when it started. The revision +count matters because a history written before protocol 15 can have gaps +(two writes in one block overwrote each other's revision); a by-revision +read is only meaningful when the retained revisions number one through the +deleted revision without a gap, and the record is where that is decided. + +The reserved key `1` and the whole lifecycle tree are per type and +unflagged, so no single deleter pays for structure nobody refunds; each +record inside it carries the deleter's flags and is what an erase refunds. + +## Contract grammar + +Protocol 15's parser generation 4 adds two rules to keep-history types: + +| Keyword | Meaning | Constraints | +|---|---|---| +| `canBeDeleted: true` | The owner may delete a document. Previously refused on a keep-history type, since the storage layer could not remove one. | Ordinary keyword, now allowed together with `documentsKeepHistory`. | +| `canBeErased: true` | The retained revisions of a deleted document may be purged. | Requires `documentsKeepHistory` and `canBeDeleted`; defaults to `false`; immutable across contract updates. | + +Declaring `canBeErased` on a type that keeps no history, or whose documents +can never be deleted, is a contract-structure error rather than an ignored +default: a type whose declared behaviour and reachable behaviour disagree +is an authoring mistake. The check runs even without full validation, +because the flag governs an irreversible operation. + +A keep-history type may **not** carry a contested index. A contested +resource is awarded by block processing, outside transition validation, at +an id derived from the winner rather than from the contested values, and on +a keep-history type that award could land on an id whose retained history +already exists. Lifting this needs changes to the contested machinery +itself; until then the parser refuses the combination. + +Types registered under protocol 14 or earlier keep their frozen grammar: a +keep-history type that allows deletion is still refused by the generation 3 +parser, so nothing that exists today can enter the lifecycle without a +contract update at protocol 15. + +## The four lifecycle states + +```text + delete erase (first chunk, erase (terminal chunk) + Active ──────────► Deleted ──────────────────────► Erasing ──────────────────────► Absent + │ by the owner) by anyone + │ + └── erase of a history that fits in one chunk goes straight to Absent +``` + +| State | Primary-key entry | Lifecycle record | History tree | +|---|---|---|---| +| **Active** | reference | none | every revision | +| **Deleted** | none | deleted-at only | every revision retained at deletion | +| **Erasing** | none | deleted-at and erasing-from | shrinking, newest revisions removed first | +| **Absent** | none | none | none; the id is free again | + +`DocumentLifecycleState` in `packages/rs-drive/src/drive/document/lifecycle/fetch` +is the one read that classifies a document. It is deliberately not folded +into the by-id fetch every document action performs: only stateful +validation of an action on a keep-history type needs the extra reads, and +they are ordered by how likely they are to settle the question. The +ordinary by-id read answers for every live document; only a miss pays for +the lifecycle record; only a deleted document pays a third read, for the +newest revision it retains, which is where its owner is read from. + +## Delete + +A delete on a keep-history document removes the primary-key reference and +every index reference, and writes the lifecycle record in their place. The +revisions are untouched, so `getDocumentHistory` keeps serving them. + +Two properties follow from the design: + +- **Delete never escalates.** It has no code path that removes a revision, + so a second delete of the same document is a paid consensus error + (`DocumentNotFoundError`, exactly as for an id that holds nothing) rather + than a deeper removal. +- **A deleted document keeps its id.** Creating over it is refused in + transition validation and, independently, in the storage writer: + appending to a history that still retains revisions would silently merge + a new document into a deleted one's record, and the writer's check is + what covers callers that never pass through validation. + +Every Drive delete entry point takes the block the delete belongs to and the +deleter's identity, because the record has to name both. + +## Erase + +An erase purges the retained revisions of a document that has already been +deleted. It is a separate transition kind, `DocumentTransition::Erase`, +appended to the shipped enum the same way every earlier kind was, carrying +nothing beyond the base transition. Its structure validation requires the +type to keep history and to declare `canBeErased`; its state validation +depends on the lifecycle state: + +| Lifecycle state | Result | +|---|---| +| Active | refused; deleting is a separate intent with its own permission and cost | +| Deleted | allowed only for the document's owner, read from the newest retained revision | +| Erasing | allowed for **any** identity | +| Absent | `DocumentNotFoundError` | + +**Erasure is authorized once.** The owner commits the document to erasure +with the first chunk; the record that commitment leaves in state is then the +evidence that destruction was authorized, so any identity may submit the +remaining chunks and pay for them. An owner who loses their keys, their +funds or their permission cannot strand a half-erased document. For the same +reason an erase carries no token payment: the deletion it follows was +charged when the document was deleted, and a payment on a transition anyone +may submit would let a continuation move tokens. + +**Chunks.** One transition removes at most +`max_document_revisions_erased_per_transition` revisions (100 from +`SYSTEM_LIMITS_V5`), newest first, so a partial erasure leaves the oldest +content behind and the retained sequence stays contiguous from one. The +enumeration reads one revision more than it may remove so that it knows, +before emitting anything, whether this chunk is the last one: + +- a **non-terminal first chunk** overwrites the record with the erasure it + authorizes; +- a **terminal chunk** removes the record and the now empty history subtree, + which GroveDB accepts only because the revision deletes are in the same + batch and refuses otherwise. + +The two never happen together, so one batch never carries two operations on +the record's key. + +**Accounting.** Removing a revision credits whoever paid for it, through the +storage flags every revision carries, in balance updates applied after the +transition's fee result is formed. The admission estimate cannot know how +many revisions a document retains, so it prices a full chunk of the type's +largest documents plus both endings; every erase is admitted against the +same worst-case estimate and the actual fee is what it removed. GroveDB's +query surface has no key-only result shape over a range, so the enumeration +reads revision bodies it discards, and the estimate charges for that read +honestly. + +## The `getDocumentHistory` query + +The query reads a page of a keep-history document's retained revisions +together with the document's lifecycle as of the same block. + +**Request.** A contract id, document type name and document id, an optional +`limit` (at most 10, the default), and exactly one selector: + +| Selector | Meaning | +|---|---| +| `startAtMs` | inclusive lower bound on block time | +| `startAfter { timeMs, revision }` | exclusive cursor returned with the previous page; the revision is needed because several revisions can share a block time | +| `startAtRevision` | inclusive lower bound on revision; refused on a history with gaps | +| `revision` | exactly one revision; requires limit one | + +**Response.** A page of entries, each the block time, revision and +serialized document, and a `Lifecycle` message: + +| Field | Meaning | +|---|---| +| `state` | `ACTIVE`, `DELETED`, `ERASING` or `ABSENT` | +| `remaining_revisions` | how many revisions the history still retains | +| `deleted_at_ms` | zero unless the document has been deleted | +| `erasing_started_at_ms` | zero unless an authorized erasure has started | +| `erasing_from_time_ms`, `erasing_from_revision` | the newest revision retained when the erasure started | + +The state is derived identically in Drive's fetch and in the proof verifier, +and the verifier checks every claimed field against the proof. + +**Proof.** The proved response carries one proof object whose grovedb proof +holds two GroveDB proofs in a small versioned envelope, because they answer +two queries GroveDB cannot merge: the offset-paginated proof over the +document's history tree, and the exact-key proof over the current pointer, +the lifecycle record and the history tree's count. Both commit to the same +root hash, signed once. + +## Client surfaces + +**Rust SDK.** `DocumentHistory::fetch` with a `DocumentHistoryQuery` from +`dash-platform-queries` reads a page. `DocumentEraseTransitionBuilder` and +`Sdk::document_erase` submit one chunk; the result is named for what its +proof authenticates, which is that the document is absent by id, something +that was already true before the erase ran. That is why the call takes the +affected-state wait rather than the strict one. `Sdk::document_current_lifecycle` +is the separately named read for how much history is left. + +**JavaScript.** The evo-sdk `documents` facade gains `history()`, +`historyWithProof()` and `erase()`; the WASM SDK exposes them as +`getDocumentHistory`, `getDocumentHistoryWithProofInfo` and `documentErase`. +Every history selector is an exact `u64`: a `number` is accepted only up to +`Number.MAX_SAFE_INTEGER`, anything larger must be a `bigint`. + +```typescript +// Delete, then erase in chunks until the history is gone. The first erase +// is signed by the owner; later ones may be signed by any identity. +await sdk.documents.delete({ document, identityKey: ownerKey, signer }); + +let lifecycle; +do { + await sdk.documents.erase({ + document: { id, ownerId, dataContractId, documentTypeName }, + identityKey: ownerKey, + signer, + }); + ({ lifecycle } = await sdk.documents.history({ + dataContractId, documentTypeName, documentId: id, startAtMs: 0n, limit: 1, + })); +} while (lifecycle?.state === 'ERASING'); +``` + +## Versioning + +Everything above is selected only from `PLATFORM_V15`. Released tables gain +dormant slots, and protocols 12 through 14 replay unchanged: a keep-history +delete still ends in `InvalidDeletionOfDocumentThatKeepsHistory` at 12 and +13 and in a paid rejection at 14, and an erase transition is refused at +basic-structure validation below 15 by its per-kind bounds slot. A +keep-history type registered under protocol 14 can never become deletable, +so the migrated storage layout and the lifecycle are only ever exercised +together on types updated at 15. + +## Rules and guidelines + +**Do:** +- Read a keep-history document's state through `fetch_document_lifecycle` + whenever the difference between deleted and never-existed matters. An + ordinary by-id read cannot tell them apart. +- Pass the block and the deleter to every Drive delete entry point; the + record needs both. +- Treat an erase's proof as an observation of the affected state and read + the lifecycle separately to learn what is left. + +**Do not:** +- Declare `canBeErased` without `documentsKeepHistory` and `canBeDeleted`, + or a contested index on a keep-history type. The parser refuses both. +- Add a token cost to an erase. Any identity may submit a continuation. +- Rely on a by-revision selector over a history written before protocol 15 + without checking `remaining_revisions` against the deleted revision; a + gapped history refuses it. diff --git a/book/src/evo-sdk/state-transitions.md b/book/src/evo-sdk/state-transitions.md index 4526d7085db..aadb2810db3 100644 --- a/book/src/evo-sdk/state-transitions.md +++ b/book/src/evo-sdk/state-transitions.md @@ -84,12 +84,15 @@ await sdk.documents.create({ }); ``` -### Replace, delete, transfer +### Replace, delete, transfer, erase The `sdk.documents` facade also provides `replace()`, `delete()`, -`transfer()`, `purchase()`, and `setPrice()` methods. See the +`transfer()`, `purchase()`, `setPrice()` and, from protocol version 15, +`erase()` for purging the retained revisions of a deleted keep-history +document, with `history()` to read what is left. See the [API reference](https://dashpay.github.io/evo-sdk-website/docs.html) for -parameters. +parameters and the [Keep-History Documents](../drive/keep-history-documents.md) +chapter for the lifecycle those calls drive. ## Token operations