From 970b63a8343cbfab0dc2ec4698d07231b478888c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 10:06:06 +0700 Subject: [PATCH 1/8] feat(platform)!: DashPay contact requests declare their checks (PV14) DashPay v2's contactRequest now states in its schema what consensus checks, using the PV14 keywords, instead of a hand-written data trigger: - toUserId: distinctFrom "$ownerId" (no request to oneself) and an identityPublicKey refersTo naming recipientKeyIndex (the recipient identity exists, holds that key, and the key is not disabled; the trigger only checked the identity). - encryptedPublicKey / encryptedAccountLabel: encryptedFor with the ecdh-secp256k1-aes256-cbc scheme, so wallets read the recipe from the contract and consensus refuses bytes that are not an IV plus whole AES blocks. senderKeyIndex / recipientKeyIndex gain maximum u32::MAX, which encryptedFor requires; DashPay keeps sizedIntegerTypes off, so both are still stored as i64. - Data trigger bindings 2 (PV14 only) drop the contact request trigger. The three new profile address fields lose their schema descriptions to keep the stored contract small; their formats move to a code comment. DashPay v2 replaces v1 in place at the PV14 upgrade without the contract update checks, so new tests pin that every v1 property keeps its stored encoding, run the registration checks system contracts skip, and read v1 contact documents back unchanged across the upgrade. Co-Authored-By: Claude Opus 5.5 --- book/src/evo-sdk/dashpay-contact-requests.md | 16 ++ .../schema/v2/dashpay.schema.json | 24 +- packages/dashpay-contract/src/v2/mod.rs | 22 +- packages/rs-dpp/src/system_data_contracts.rs | 91 +++++++ .../src/execution/check_tx/v0/mod.rs | 14 +- .../v0/mod.rs | 131 +++++++++- .../data_triggers/bindings/list/v2/mod.rs | 14 +- .../tests/document/dashpay_contact_request.rs | 229 ++++++++++++++++++ .../batch/tests/document/deletion.rs | 2 + .../batch/tests/document/mod.rs | 1 + .../batch/tests/document/replacement.rs | 2 + .../data_contract_reference_validation/mod.rs | 37 +++ .../rs-drive/tests/deterministic_root_hash.rs | 2 +- .../drive_abci_validation_versions/v10.rs | 3 + .../system_data_contract_versions/v3.rs | 5 +- .../rs-platform-version/src/version/v14.rs | 20 +- 16 files changed, 586 insertions(+), 27 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_contact_request.rs diff --git a/book/src/evo-sdk/dashpay-contact-requests.md b/book/src/evo-sdk/dashpay-contact-requests.md index beaf9e0953e..885f61c031f 100644 --- a/book/src/evo-sdk/dashpay-contact-requests.md +++ b/book/src/evo-sdk/dashpay-contact-requests.md @@ -28,6 +28,22 @@ The current DashPay contract schema requires the system field `$createdAtCoreBlockHeight`. Older external references may use `coreHeightCreatedAt`; do not submit that name to the current contract. +### What consensus checks + +From protocol version 14 the contract declares these checks itself, and a +contact request that fails one is refused (and the fee charged): + +| Rule | Declared as | Error | +| --- | --- | --- | +| `toUserId` is not the sender | `distinctFrom: "$ownerId"` on `toUserId` | `DocumentPropertyNotDistinctError` (10419) | +| The recipient identity exists and has the key `recipientKeyIndex` | `refersTo` of type `identityPublicKey` on `toUserId`, `keyIdProperty: "recipientKeyIndex"` | `ReferencedIdentityKeyNotFoundError` (40123) | +| That key is not disabled | the same `refersTo` | `ReferencedIdentityKeyDisabledError` (40124) | +| `encryptedPublicKey` and `encryptedAccountLabel` are a 16-byte IV plus whole 16-byte blocks | `encryptedFor` with scheme `ecdh-secp256k1-aes256-cbc` | `InvalidEncryptedPropertyShapeError` (10420) | + +Up to protocol version 13 a data trigger made only the first two checks and +reported both as a `DataTriggerConditionError`. Neither version checks the +purpose or contract bounds of either key, or that the bytes decrypt. + `encryptedPublicKey` is exactly 96 bytes: - 16 bytes: AES-CBC initialization vector diff --git a/packages/dashpay-contract/schema/v2/dashpay.schema.json b/packages/dashpay-contract/schema/v2/dashpay.schema.json index 74ea27bdf9c..1524641ca89 100644 --- a/packages/dashpay-contract/schema/v2/dashpay.schema.json +++ b/packages/dashpay-contract/schema/v2/dashpay.schema.json @@ -64,7 +64,6 @@ "byteArray": true, "minItems": 21, "maxItems": 21, - "description": "Core chain address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The type byte is consensus-enforced by a data trigger; clients render the address as Base58Check for the network they are on. Payments to it are publicly linkable to this profile.", "position": 5 }, "platformPaymentAddress": { @@ -72,7 +71,6 @@ "byteArray": true, "minItems": 21, "maxItems": 21, - "description": "Platform address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The type byte is consensus-enforced by a data trigger.", "position": 6 }, "shieldedAddress": { @@ -80,7 +78,6 @@ "byteArray": true, "minItems": 43, "maxItems": 43, - "description": "Raw Orchard receiving address: 11-byte diversifier followed by 32-byte diversified transmission key. Clients validate before payment; wallets should use a dedicated tip account.", "position": 7 } }, @@ -226,23 +223,36 @@ "minItems": 32, "maxItems": 32, "position": 0, - "contentMediaType": "application/x.dash.dpp.identifier" + "contentMediaType": "application/x.dash.dpp.identifier", + "refersTo": { + "type": "identityPublicKey", + "keyIdProperty": "recipientKeyIndex" + }, + "distinctFrom": "$ownerId" }, "encryptedPublicKey": { "type": "array", "byteArray": true, "minItems": 96, "maxItems": 96, + "encryptedFor": { + "recipient": "toUserId", + "recipientKey": "recipientKeyIndex", + "senderKey": "senderKeyIndex", + "scheme": "ecdh-secp256k1-aes256-cbc" + }, "position": 1 }, "senderKeyIndex": { "type": "integer", "minimum": 0, + "maximum": 4294967295, "position": 2 }, "recipientKeyIndex": { "type": "integer", "minimum": 0, + "maximum": 4294967295, "position": 3 }, "accountReference": { @@ -255,6 +265,12 @@ "byteArray": true, "minItems": 48, "maxItems": 80, + "encryptedFor": { + "recipient": "toUserId", + "recipientKey": "recipientKeyIndex", + "senderKey": "senderKeyIndex", + "scheme": "ecdh-secp256k1-aes256-cbc" + }, "position": 5 }, "autoAcceptProof": { diff --git a/packages/dashpay-contract/src/v2/mod.rs b/packages/dashpay-contract/src/v2/mod.rs index a9611b407fb..022f8d82312 100644 --- a/packages/dashpay-contract/src/v2/mod.rs +++ b/packages/dashpay-contract/src/v2/mod.rs @@ -2,9 +2,27 @@ use crate::error::Error; use serde_json::Value; // Document-type name and property constants live in `crate::v1::document_types`; -// v2 does not change any names v1 defined, it only adds the optional +// v2 does not change any names v1 defined. It adds the optional // `corePaymentAddress`, `platformPaymentAddress`, and `shieldedAddress` -// properties to `profile`. +// properties to `profile`, and declares on `contactRequest` what consensus +// checks (`toUserId`: `distinctFrom` and an `identityPublicKey` `refersTo`; +// `encryptedPublicKey` / `encryptedAccountLabel`: `encryptedFor`). +// +// v2 replaces v1 in place at protocol version 14 without the contract update +// checks, and the contract keeps `sizedIntegerTypes` off, so every property v1 +// declares must keep its stored encoding: a key reference on the key id +// property itself (`identityProperty`) would store it as a u32 instead of an +// i64 and misread every stored document. +// +// The schema carries no descriptions for the three profile addresses, to keep +// the stored contract small. Their formats: +// - `corePaymentAddress` / `platformPaymentAddress`: 21 bytes, a type byte +// (0x00 P2PKH, 0x01 P2SH, enforced by the profile data trigger) followed by +// the 20-byte HASH160. Clients render the Core one as Base58Check for their +// network. Payments to either are publicly linkable to the profile. +// - `shieldedAddress`: 43 bytes, a raw Orchard address (11-byte diversifier +// then the 32-byte diversified transmission key). Only the length is checked +// on chain; clients validate it before paying. pub fn load_documents_schemas() -> Result { serde_json::from_str(include_str!("../../schema/v2/dashpay.schema.json")) diff --git a/packages/rs-dpp/src/system_data_contracts.rs b/packages/rs-dpp/src/system_data_contracts.rs index 66b865d0012..e2d76992222 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -296,3 +296,94 @@ mod app_connect_tests { ); } } + +#[cfg(all(test, feature = "dashpay-contract"))] +mod dashpay_tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::accessors::DocumentTypeV0Getters; + use crate::data_contract::document_type::{ + DistinctFrom, DocumentPropertyReferenceTarget, DocumentPropertyType, EncryptedFor, + EncryptedForRecipient, EncryptionScheme, IdentityKeyReferenceRequirements, + }; + + /// DashPay v2 replaces v1 in place at protocol version 14 (`apply_contract` in + /// `transition_to_version_14`), which never runs the contract update checks, so + /// documents written under v1 are read with v2's types from then on. Every + /// property v1 declares must be stored exactly as v1 stored it. + #[test] + fn should_store_every_dashpay_v1_property_as_v1_did() { + let v1 = load_system_data_contract( + SystemDataContract::Dashpay, + PlatformVersion::get(13).expect("protocol version 13"), + ) + .expect("dashpay v1"); + let v2 = load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest()) + .expect("dashpay v2"); + + for (type_name, v1_type) in v1.document_types() { + let v2_type = v2 + .document_type_for_name(type_name) + .expect("v2 keeps every v1 document type"); + for (path, v1_property) in v1_type.flattened_properties() { + let v2_property = v2_type + .flattened_properties() + .get(path) + .unwrap_or_else(|| panic!("v2 keeps {type_name}.{path}")); + assert_eq!( + v1_property.property_type.stored_encoding(), + v2_property.property_type.stored_encoding(), + "{type_name}.{path} must be stored as v1 stored it" + ); + } + } + } + + /// From protocol version 14 the contact request's recipient checks are schema + /// declarations instead of a data trigger, and the two ECDH fields name their + /// encryption recipe. + #[test] + fn should_declare_the_contact_request_checks_and_encryption() { + let contract = + load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest()) + .expect("dashpay v2"); + let contact_request = contract + .document_type_for_name("contactRequest") + .expect("contactRequest"); + let to_user_id = contact_request + .flattened_properties() + .get("toUserId") + .expect("toUserId"); + + assert_eq!( + to_user_id.property_type, + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::IdentityPublicKey { + key_id_property: "recipientKeyIndex".to_string(), + key_requirements: IdentityKeyReferenceRequirements::default(), + } + ) + ); + assert_eq!(to_user_id.distinct_from, Some(DistinctFrom::OwnerId)); + + let recipe = EncryptedFor { + recipient: EncryptedForRecipient::Property("toUserId".to_string()), + recipient_key: "recipientKeyIndex".to_string(), + sender_key: "senderKeyIndex".to_string(), + scheme: EncryptionScheme::EcdhSecp256k1Aes256Cbc, + }; + let mut encrypted = contact_request + .encrypted_properties() + .into_iter() + .map(|(path, encrypted_for)| (path.as_str(), encrypted_for.clone())) + .collect::>(); + encrypted.sort_by_key(|(path, _)| *path); + assert_eq!( + encrypted, + vec![ + ("encryptedAccountLabel", recipe.clone()), + ("encryptedPublicKey", recipe), + ] + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index da4dbce479c..d1e2701b75d 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -691,7 +691,7 @@ mod tests { // from protocol version 14 the contract's version item is stored in the contract's // other tree (one more tree insert), the config is version 2 (one more byte), and // the larger DashPay v2 schema adds byte-billed contract bytes - 24003037140 + 24002860070 ); let check_result = platform @@ -1045,7 +1045,7 @@ mod tests { // We have one invalid paid for state transition assert_eq!(processing_result.invalid_paid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 346660); + assert_eq!(processing_result.aggregated_fees().processing_fee, 346820); let check_result = platform .check_tx( @@ -1358,7 +1358,7 @@ mod tests { // from protocol version 14 the contract's version item is stored in the contract's // other tree (one more tree insert), the config is version 2 (one more byte), and // the larger DashPay v2 schema adds byte-billed contract bytes - 24006074280 + 24005720140 ); let check_result = platform @@ -1836,7 +1836,7 @@ mod tests { // from protocol version 14 the contract's version item is stored in the contract's // other tree (one more tree insert), the config is version 2 (one more byte), and // the larger DashPay v2 schema adds byte-billed contract bytes - 24003037140 + 24002860070 ); platform @@ -1927,7 +1927,7 @@ mod tests { // other tree (an update reads what key `2` holds, billed, before writing under it), // the config is version 2, and the larger DashPay v2 schema adds byte-billed // contract bytes - 27003119120 + 27002916290 ); let check_result = platform @@ -2518,7 +2518,7 @@ mod tests { // from protocol version 14 the contract's version item is stored in the contract's // other tree (one more tree insert), the config is version 2 (one more byte), and // the larger DashPay v2 schema adds byte-billed contract bytes - 24003037140 + 24002860070 ); platform @@ -2641,7 +2641,7 @@ mod tests { // We have one invalid paid for state transition assert_eq!(processing_result.invalid_paid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 448640); + assert_eq!(processing_result.aggregated_fees().processing_fee, 448800); let check_result = platform .check_tx( diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 5820f88c0c3..3c64b9c5cd0 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -1551,7 +1551,7 @@ mod tests { /// payment address fields, and the legacy profile bytes must stay readable /// through normal Drive queries against the refreshed contract. #[tokio::test] - async fn test_protocol_change_v13_to_v14_upgrades_dashpay_and_keeps_v1_profiles_readable() { + async fn test_protocol_change_v13_to_v14_upgrades_dashpay_and_keeps_v1_documents_readable() { use crate::execution::validation::state_transition::tests::setup_identity; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use assert_matches::assert_matches; @@ -1561,9 +1561,10 @@ mod tests { use dpp::data_contract::document_type::random_document::{ CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, }; - use dpp::document::{DocumentV0Getters, DocumentV0Setters}; + use dpp::data_contract::DataContract; + use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; use dpp::identity::accessors::IdentityGettersV0; - use dpp::platform_value::Bytes32; + use dpp::platform_value::{Bytes32, Value}; use dpp::serialization::PlatformSerializable; use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use dpp::state_transition::batch_transition::BatchTransition; @@ -1654,6 +1655,115 @@ mod tests { .unwrap() .expect("expected to commit"); + // A v1 contact request and a v1 contact info. DashPay stores integers as + // i64 (no sized integer types) and v2 replaces v1 in place without the + // contract update checks, so v2 must read both back unchanged + let (recipient, ..) = setup_identity(&mut platform, 495, dash_to_credits!(0.1)); + let mut contact_transitions = vec![]; + for (nonce, document_type_name) in [(3, "contactRequest"), (4, "contactInfo")] { + let document_type = dashpay_v1 + .document_type_for_name(document_type_name) + .expect("expected the document type"); + let entropy = Bytes32::random_with_rng(&mut rng); + let mut document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version_13, + ) + .expect("expected a random v1 document"); + document + .set_id_for_creation(document_type, &entropy.0, nonce, platform_version_13) + .expect("expected to set the document id"); + // Random i64 integers ignore the schema's `minimum`, so the + // integers are set; `accountReference` takes all eight bytes + if document_type_name == "contactRequest" { + // the v1 contact request trigger requires an existing recipient + document.set("toUserId", Value::Identifier(recipient.id().to_buffer())); + document.set("senderKeyIndex", Value::I64(1)); + document.set("recipientKeyIndex", Value::I64(0)); + document.set("accountReference", Value::I64(1 << 40)); + } else { + document.set("rootEncryptionKeyIndex", Value::I64(3)); + document.set("derivationEncryptionKeyIndex", Value::I64(5)); + } + contact_transitions.push( + BatchTransition::new_document_creation_transition_from_document( + document, + document_type, + entropy.0, + &key, + nonce, + 0, + None, + &signer, + platform_version_13, + None, + ) + .await + .expect("expect to create documents batch transition") + .serialize_to_bytes() + .expect("expected serialized transition"), + ); + } + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &contact_transitions, + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version_13, + false, + None, + ) + .expect("expected to process state transitions"); + assert_matches!( + processing_result.execution_results().as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + let stored_documents = |contract: &DataContract, + document_type_name: &str, + platform_version: &PlatformVersion| + -> Vec { + let query = DriveDocumentQuery::from_sql_expr( + &format!("select * from {document_type_name}"), + contract, + Some(&platform.config.drive), + platform_version, + ) + .expect("expected a document query"); + platform + .drive + .query_documents(query, None, false, None, None) + .expect("expected to query documents") + .documents() + .to_vec() + }; + let contact_documents_v1 = ["contactRequest", "contactInfo"].map(|document_type_name| { + let documents = stored_documents(&dashpay_v1, document_type_name, platform_version_13); + assert_eq!( + documents.len(), + 1, + "expected one stored {document_type_name}" + ); + documents + }); + // Warm the drive contract cache with the v1 contract and confirm the // payment address fields are absent pre-upgrade let transaction = platform.drive.grove.start_transaction(); @@ -1769,6 +1879,21 @@ mod tests { stored_profile_id, "the surviving profile must be the pre-upgrade document" ); + + for (document_type_name, documents_v1) in ["contactRequest", "contactInfo"] + .into_iter() + .zip(contact_documents_v1) + { + assert_eq!( + stored_documents( + &dashpay_v2_fetch_info.contract, + document_type_name, + platform_version_14 + ), + documents_v1, + "v2 must read the v1 {document_type_name} back unchanged" + ); + } } // test_transition_to_version_9 removed: requires prior state from versions 4-8 diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v2/mod.rs index 4b7963db392..1acc04e99af 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v2/mod.rs @@ -1,5 +1,5 @@ use crate::execution::validation::state_transition::batch::data_triggers::bindings::data_trigger_binding::DataTriggerBindingV0; -use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::{create_contact_request_data_trigger, validate_profile_payment_addresses_data_trigger}; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::validate_profile_payment_addresses_data_trigger; use crate::execution::validation::state_transition::batch::data_triggers::triggers::dpns::create_domain_data_trigger; use crate::execution::validation::state_transition::batch::data_triggers::triggers::reject::reject_data_trigger; use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::delete_withdrawal_data_trigger; @@ -17,7 +17,11 @@ use drive::state_transition_action::batch::batched_transition::document_transiti /// v2 (PROTOCOL_VERSION_14): DashPay `profile` documents gain Create and /// Replace triggers enforcing the DIP-33 payment-address type byte /// (`0x00` P2PKH / `0x01` P2SH) that the schema vocabulary cannot express. -/// Everything else is unchanged from v1. +/// DashPay `contactRequest` creation loses its trigger: the DashPay v2 schema +/// declares both of its checks on `toUserId`, `distinctFrom: "$ownerId"` (no +/// request to oneself) and an `identityPublicKey` `refersTo` (the recipient +/// identity and its `recipientKeyIndex` key exist, and the key is not +/// disabled). Everything else is unchanged from v1. /// /// # Returns /// @@ -49,12 +53,6 @@ pub(super) fn data_trigger_bindings_list_v2() -> Result, + dashpay: Arc, + sender: Identity, + signer: SimpleSigner, + key: IdentityPublicKey, + recipient: Identity, + } + + impl Fixture { + fn new() -> Self { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let (sender, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + // Keys 0 and 1 + let (recipient, ..) = setup_identity(&mut platform, 495, dash_to_credits!(0.1)); + + let dashpay = platform + .drive + .cache + .system_data_contracts + .load_dashpay(platform_version) + .expect("expected the dashpay system contract"); + + Fixture { + platform, + dashpay, + sender, + signer, + key, + recipient, + } + } + + /// A contact request from the sender to `to_user_id`'s key + /// `recipient_key_index` whose account label is `label_length` bytes. + fn contact_request( + &self, + to_user_id: Identifier, + recipient_key_index: u32, + label_length: usize, + ) -> (Document, Bytes32) { + let platform_version = PlatformVersion::latest(); + let contact_request = self + .dashpay + .document_type_for_name("contactRequest") + .expect("expected the contactRequest document type"); + + let mut rng = StdRng::seed_from_u64(437); + let entropy = Bytes32::random_with_rng(&mut rng); + let mut document = contact_request + .random_document_with_identifier_and_entropy( + &mut rng, + self.sender.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + document + .set_id_for_creation(contact_request, &entropy.0, 2, platform_version) + .expect("expected to set the document id"); + + document.set("toUserId", Value::Identifier(to_user_id.to_buffer())); + document.set("senderKeyIndex", Value::U32(1)); + document.set("recipientKeyIndex", Value::U32(recipient_key_index)); + document.set("accountReference", Value::U32(0)); + document.set("encryptedPublicKey", Value::Bytes(vec![7u8; 96])); + document.set( + "encryptedAccountLabel", + Value::Bytes(vec![8u8; label_length]), + ); + (document, entropy) + } + + async fn create( + &self, + document: Document, + entropy: Bytes32, + ) -> StateTransitionExecutionResult { + let platform_version = PlatformVersion::latest(); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + self.dashpay + .document_type_for_name("contactRequest") + .expect("expected the contactRequest document type"), + entropy.0, + &self.key, + 2, + 0, + None, + &self.signer, + platform_version, + None, + ) + .await + .expect("expect to create documents batch transition"); + + let platform_state = self.platform.state.load(); + let transaction = self.platform.drive.grove.start_transaction(); + let processing_result = self + .platform + .platform + .process_raw_state_transitions( + &vec![transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition")], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + self.platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + processing_result.into_execution_results().remove(0) + } + } + + fn paid_error(result: StateTransitionExecutionResult) -> ConsensusError { + let StateTransitionExecutionResult::PaidConsensusError { error, .. } = result else { + panic!("expected a paid consensus error, got {result:?}"); + }; + error + } + + #[tokio::test] + async fn should_create_a_contact_request_to_an_existing_key_of_another_identity() { + let fixture = Fixture::new(); + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 1, 48); + + assert_matches!( + fixture.create(document, entropy).await, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + } + + /// The data trigger's first check, now `distinctFrom: "$ownerId"`. + #[tokio::test] + async fn should_refuse_a_contact_request_to_oneself() { + let fixture = Fixture::new(); + let (document, entropy) = fixture.contact_request(fixture.sender.id(), 1, 48); + + assert_matches!( + paid_error(fixture.create(document, entropy).await), + ConsensusError::BasicError(BasicError::DocumentPropertyNotDistinctError(e)) + if e.document_type_name() == "contactRequest" + && e.property() == "toUserId" + && e.distinct_from() == "$ownerId" + ); + } + + /// The data trigger's second check, now the `identityPublicKey` reference: + /// an identity that does not exist has no key either. + #[tokio::test] + async fn should_refuse_a_contact_request_to_an_identity_that_does_not_exist() { + let fixture = Fixture::new(); + let missing = Identifier::from([0xAB; 32]); + let (document, entropy) = fixture.contact_request(missing, 1, 48); + + assert_matches!( + paid_error(fixture.create(document, entropy).await), + ConsensusError::StateError(StateError::ReferencedIdentityKeyNotFoundError(e)) + if *e.identity_id() == missing && e.key_id() == 1 && e.path() == "toUserId" + ); + } + + /// New from protocol version 14: the trigger only asked whether the + /// recipient identity exists, never whether it has the named key. + #[tokio::test] + async fn should_refuse_a_contact_request_to_a_key_the_recipient_does_not_have() { + let fixture = Fixture::new(); + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 7, 48); + + assert_matches!( + paid_error(fixture.create(document, entropy).await), + ConsensusError::StateError(StateError::ReferencedIdentityKeyNotFoundError(e)) + if *e.identity_id() == fixture.recipient.id() + && e.key_id() == 7 + && e.path() == "toUserId" + ); + } + + /// New from protocol version 14: an account label of 50 bytes is within the + /// schema's 48 to 80 but is not an IV plus whole AES-CBC blocks. + #[tokio::test] + async fn should_refuse_an_account_label_that_is_not_an_iv_plus_whole_blocks() { + let fixture = Fixture::new(); + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 1, 50); + + assert_matches!( + paid_error(fixture.create(document, entropy).await), + ConsensusError::BasicError(BasicError::InvalidEncryptedPropertyShapeError(e)) + if e.property() == "encryptedAccountLabel" && e.actual_length() == 50 + ); + } +} 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 1ba8d5de0ed..6e87b4c3d6f 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 @@ -894,6 +894,8 @@ mod deletion_tests { document.set("recipientKeyIndex", Value::U32(1)); document.set("senderKeyIndex", Value::U32(1)); document.set("accountReference", Value::U32(0)); + // an IV plus whole AES blocks, as the label's `encryptedFor` requires + document.set("encryptedAccountLabel", Value::Bytes(vec![0u8; 48])); let mut altered_document = document.clone(); 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 23e8c230af1..c0fe7ccf1f9 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,5 +1,6 @@ mod action_fees; mod creation; +mod dashpay_contact_request; mod deletable_document_reference; mod deletion; mod distinct_from; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 13c73623208..2738cdd2027 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -1141,6 +1141,8 @@ mod replacement_tests { document.set("recipientKeyIndex", Value::U32(1)); document.set("senderKeyIndex", Value::U32(1)); document.set("accountReference", Value::U32(0)); + // an IV plus whole AES blocks, as the label's `encryptedFor` requires + document.set("encryptedAccountLabel", Value::Bytes(vec![0u8; 48])); let mut altered_document = document.clone(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs index f129339b307..3ee23bfeb91 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/mod.rs @@ -48,3 +48,40 @@ pub(in crate::execution::validation::state_transition) fn validate_data_contract })), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + use dpp::version::DefaultForPlatformVersion; + + /// A system contract is stored without passing contract create validation, + /// so nothing else runs the registration checks on DashPay's `refersTo` + /// declarations (the `contactRequest.toUserId` key reference from v2). + #[test] + fn should_accept_the_references_the_dashpay_system_contract_declares() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let dashpay = load_system_data_contract(SystemDataContract::Dashpay, platform_version) + .expect("expected the dashpay system contract"); + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("expected an execution context"); + + let result = validate_data_contract_references( + &dashpay, + &platform.drive, + &BlockInfo::default(), + &mut execution_context, + None, + platform_version, + ) + .expect("expected the reference validation to run"); + + assert!(result.is_valid(), "{:?}", result.errors); + } +} diff --git a/packages/rs-drive/tests/deterministic_root_hash.rs b/packages/rs-drive/tests/deterministic_root_hash.rs index e7878ecebe3..538ed84572f 100644 --- a/packages/rs-drive/tests/deterministic_root_hash.rs +++ b/packages/rs-drive/tests/deterministic_root_hash.rs @@ -310,7 +310,7 @@ mod tests { // item in the contract's other tree (`[64, id, 2] / 64`), one more tree under // the contract's root subtree, serializes every contract with config version 2, // and ships DashPay contract v2 with the optional `shieldedAddress` profile field. - _ => "c25bc863d30a4185311546d51bacfdc030ee57671dbae774775d0efd4474220c", + _ => "d98bafb506e748c8b1f45ce174f08153e2df671dc505d781d5ef4c9c88353845", }; assert_eq!( 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 e7802f8a616..fab360b65bb 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 @@ -206,6 +206,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // It keeps v1's PROTOCOL_VERSION_13 change: no reject // bindings for Transfer, Purchase and UpdatePrice on // DPNS `domain` documents (username transfers/sales). + // v2 also drops the DashPay contactRequest create + // binding: the DashPay v2 schema declares its checks. bindings: 2, triggers: DriveAbciValidationDataTriggerVersions { // PROTOCOL_VERSION_12 (v3.1 hard fork): triggers @@ -213,6 +215,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // that bill the cost via add_operation on the // outer execution_context. v0 versions remain // byte-identical to PV11 (don't bill). + // Unbound from PROTOCOL_VERSION_14 (bindings v2). create_contact_request_data_trigger: 1, validate_profile_payment_addresses_data_trigger: 0, create_domain_data_trigger: 1, diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs index 57f8f319595..9df0423e8d6 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v3.rs @@ -3,7 +3,10 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // PROTOCOL_VERSION_14: DashPay contract v2 adds the optional public payment // address fields to the `profile` document type (`corePaymentAddress`, // `platformPaymentAddress`) per DIP-33 plus the optional 43-byte Orchard -// `shieldedAddress`, and the withdrawals contract v2 admits +// `shieldedAddress`; its `contactRequest` declares the checks the contact +// request data trigger made (`distinctFrom` and an `identityPublicKey` +// `refersTo` on `toUserId`) and `encryptedFor` on its two ECDH fields. The +// withdrawals contract v2 admits // the terminal FAILED (5) value of the `status` property, written for // withdrawals whose asset unlock Core can never mine. The token history // contract v2 admits the value 2 (OncePerIdentity) of the claim document's diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 8f9acd35690..307f2b49277 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -759,6 +759,24 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// by-id joins refuse a lookup reference as a join property, and /// preallocated indexes are never bound through one. /// +/// 33. **The DashPay contact request declares its checks**: DashPay v2's +/// `contactRequest.toUserId` carries `distinctFrom: "$ownerId"` and an +/// `identityPublicKey` `refersTo` naming `recipientKeyIndex`, and +/// `encryptedPublicKey` and `encryptedAccountLabel` declare `encryptedFor` +/// (`ecdh-secp256k1-aes256-cbc`, recipient `toUserId`, keys +/// `recipientKeyIndex` and `senderKeyIndex`, which gain `maximum` +/// 4294967295). Data trigger bindings 2 (edited in place, only selected +/// from this version) drop the contact request create trigger, whose two +/// checks the declarations now make: a request to oneself is refused with +/// `DocumentPropertyNotDistinctError` (10419) instead of a +/// `DataTriggerConditionError`, and one to a missing identity with +/// `ReferencedIdentityKeyNotFoundError` (40123). New from this version: the +/// recipient must hold the named key (40123) and it must not be disabled +/// (40124), and both encrypted fields must be an IV plus whole AES blocks +/// (10420). No `keyRequirements`: most testnet contact requests use an +/// unbound encryption key for both indexes. DashPay keeps +/// `sizedIntegerTypes` off, so both key indexes stay stored as i64. +/// /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by /// the app's ephemeral key hash and the responding identity, with the wallet's @@ -847,7 +865,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { methods: DPP_METHOD_VERSIONS_V3, // changed: daily_withdrawal_limit v2 — a percentage of the total credits a day ago factory_versions: DPP_FACTORY_VERSIONS_V1, }, - system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, // changed: DashPay v2 adds profile payment address fields (DIP-33); withdrawals v2 admits the terminal FAILED status + system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, // changed: DashPay v2 adds profile payment address fields (DIP-33) and declares the contact request checks (item 33); withdrawals v2 admits the terminal FAILED status // The TTL ephemeral-bytes rate (270 credits/byte to processing) rides // the shared storage table; it is dead below v14 (the `ttl` grammar // does not parse), so no table fork is needed. From 64fb59cb436ffeba8ace3855571b8eeb612a67f0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 23 Sep 2026 10:16:52 +0700 Subject: [PATCH 2/8] test(drive-abci): re-pin the not-mutable replace fees for the fixed-size account label Co-Authored-By: Claude Opus 5.5 --- .../state_transitions/batch/tests/document/replacement.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 2738cdd2027..0aaac734416 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -1254,7 +1254,7 @@ mod replacement_tests { async fn test_document_replace_on_document_type_that_is_not_mutable() { run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version( PlatformVersion::latest().protocol_version, - 460940, // v14: stamped documents (see happy-path baseline note) + 460740, // v14: stamped documents (see happy-path baseline note) ) .await; } @@ -1265,7 +1265,7 @@ mod replacement_tests { /// v13 chain history stays bit-for-bit reproducible. #[tokio::test] async fn test_document_replace_on_document_type_that_is_not_mutable_protocol_version_13() { - run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version(13, 460920) + run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version(13, 460720) .await; } From fdcd1609c70c41f6a436187b6500c3f6040c2a64 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 31 Oct 2026 12:19:46 +0300 Subject: [PATCH 3/8] feat(platform)!: dashpay v3 declares the checks of its data triggers (PV15) From protocol version 15 the DashPay contract v3 states in its schema what its two data triggers checked, and data trigger bindings 3 stop running them: - contactRequest.toUserId refers to an identity (refersTo) other than the writer (distinctFrom "$ownerId"): 40120 and 10419 instead of the trigger's 40500. - encryptedPublicKey and encryptedAccountLabel declare encryptedFor (ecdh-secp256k1-aes256-cbc between senderKeyIndex and recipientKeyIndex), so each must be an IV plus whole AES blocks (10420); both key indexes gain the u32 maximum it requires and stay stored as i64. - profile rules corePaymentAddressType and platformPaymentAddressType hold each DIP-33 address's first byte to 0x00 or 0x01 (byteAt, 10422). - The three profile address descriptions move to a code comment. transition_to_version_15 re-stores DashPay with apply_contract, which runs no contract update checks, so v3 only adds declarations: a test pins that every v1 and v2 property keeps its stored encoding, and an upgrade test reads v2 documents back unchanged under v3. Bindings 3 are v2 without the DashPay bindings. v15 changelog note 14. Tests pinned at the latest version move with the contract: the check_tx DashPay fees, the deterministic root hash, the nullifier batch guard settled roots. Fixture contact requests and profiles get valid labels and addresses from protocol version 15, and the shielded failing-trigger test fails a DPNS domain create's parent lookup instead of the contact request trigger. Co-Authored-By: Claude Opus 5.5 --- book/src/evo-sdk/dashpay-contact-requests.md | 17 + .../schema/v3/dashpay.schema.json | 298 ++++++++++++++ packages/dashpay-contract/src/lib.rs | 8 +- packages/dashpay-contract/src/v3/mod.rs | 36 ++ packages/rs-dpp/src/system_data_contracts.rs | 133 +++++++ .../src/execution/check_tx/v0/mod.rs | 37 +- .../v0/mod.rs | 254 +++++++++++- .../batch/data_triggers/bindings/list/mod.rs | 7 +- .../data_triggers/bindings/list/v3/mod.rs | 29 ++ .../batch/tests/document/creation.rs | 38 +- .../batch/tests/document/dashpay_v3.rs | 365 ++++++++++++++++++ .../batch/tests/document/deletion.rs | 3 + .../batch/tests/document/mod.rs | 1 + .../batch/tests/document/replacement.rs | 7 +- .../state_transitions/batch/tests/mod.rs | 5 +- .../token/shielded/nullifier_batch_guard.rs | 61 ++- .../rs-drive/tests/deterministic_root_hash.rs | 5 +- .../drive_abci_validation_versions/v11.rs | 4 + .../system_data_contract_versions/v4.rs | 9 +- .../rs-platform-version/src/version/v15.rs | 26 +- 20 files changed, 1291 insertions(+), 52 deletions(-) create mode 100644 packages/dashpay-contract/schema/v3/dashpay.schema.json create mode 100644 packages/dashpay-contract/src/v3/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v3/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_v3.rs diff --git a/book/src/evo-sdk/dashpay-contact-requests.md b/book/src/evo-sdk/dashpay-contact-requests.md index 10f1a88081d..8add70f5f37 100644 --- a/book/src/evo-sdk/dashpay-contact-requests.md +++ b/book/src/evo-sdk/dashpay-contact-requests.md @@ -46,6 +46,23 @@ identity, account, and address index. The sender then encrypts that xpub with an ECDH shared secret from the sender's identity encryption private key and the recipient's identity decryption public key. +## What consensus checks + +From protocol version 15 the DashPay contract (v3) states these checks in its +schema, and a contact request that fails one is refused (and the fee charged): + +| Rule | Declared as | Error | +| --- | --- | --- | +| `toUserId` is not the sender | `distinctFrom: "$ownerId"` on `toUserId` | `DocumentPropertyNotDistinctError` (10419) | +| The recipient identity exists | `refersTo` of type `identity` on `toUserId` | `ReferencedEntityNotFoundError` (40120) | +| `encryptedPublicKey` and `encryptedAccountLabel` are a 16-byte IV plus whole 16-byte blocks | `encryptedFor` with scheme `ecdh-secp256k1-aes256-cbc` | `InvalidEncryptedPropertyShapeError` (10420) | + +Up to protocol version 14 a data trigger made only the first two checks and +reported both as a `DataTriggerConditionError` (40500). Neither version checks +that the two key indexes name keys the identities hold, the purpose of those +keys, or that the bytes decrypt. Wallets and SDKs can read the encryption +recipe from the contract (`encrypted_properties()` in Rust). + ## Derive the contact payment xpub ```typescript diff --git a/packages/dashpay-contract/schema/v3/dashpay.schema.json b/packages/dashpay-contract/schema/v3/dashpay.schema.json new file mode 100644 index 00000000000..bede6943639 --- /dev/null +++ b/packages/dashpay-contract/schema/v3/dashpay.schema.json @@ -0,0 +1,298 @@ +{ + "profile": { + "type": "object", + "indices": [ + { + "name": "ownerId", + "properties": [ + { + "$ownerId": "asc" + } + ], + "unique": true + }, + { + "name": "ownerIdAndUpdatedAt", + "properties": [ + { + "$ownerId": "asc" + }, + { + "$updatedAt": "asc" + } + ] + } + ], + "properties": { + "avatarUrl": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048, + "position": 0 + }, + "avatarHash": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "description": "SHA256 hash of the bytes of the image specified by avatarUrl", + "position": 1 + }, + "avatarFingerprint": { + "type": "array", + "byteArray": true, + "minItems": 8, + "maxItems": 8, + "description": "dHash the image specified by avatarUrl", + "position": 2 + }, + "publicMessage": { + "type": "string", + "minLength": 1, + "maxLength": 140, + "position": 3 + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 25, + "position": 4 + }, + "corePaymentAddress": { + "type": "array", + "byteArray": true, + "minItems": 21, + "maxItems": 21, + "position": 5 + }, + "platformPaymentAddress": { + "type": "array", + "byteArray": true, + "minItems": 21, + "maxItems": 21, + "position": 6 + }, + "shieldedAddress": { + "type": "array", + "byteArray": true, + "minItems": 43, + "maxItems": 43, + "position": 7 + } + }, + "minProperties": 1, + "dependentRequired": { + "avatarUrl": ["avatarHash", "avatarFingerprint"], + "avatarHash": ["avatarUrl", "avatarFingerprint"], + "avatarFingerprint": ["avatarUrl", "avatarHash"] + }, + "propertyConstraints": { + "corePaymentAddressType": { "in": [{ "byteAt": ["corePaymentAddress", 0] }, [0, 1]] }, + "platformPaymentAddressType": { "in": [{ "byteAt": ["platformPaymentAddress", 0] }, [0, 1]] } + }, + "required": [ + "$createdAt", + "$updatedAt" + ], + "additionalProperties": false + }, + "contactInfo": { + "type": "object", + "indices": [ + { + "name": "ownerIdAndKeys", + "properties": [ + { + "$ownerId": "asc" + }, + { + "rootEncryptionKeyIndex": "asc" + }, + { + "derivationEncryptionKeyIndex": "asc" + } + ], + "unique": true + }, + { + "name": "ownerIdAndUpdatedAt", + "properties": [ + { + "$ownerId": "asc" + }, + { + "$updatedAt": "asc" + } + ] + } + ], + "properties": { + "encToUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 0 + }, + "rootEncryptionKeyIndex": { + "type": "integer", + "minimum": 0, + "position": 1 + }, + "derivationEncryptionKeyIndex": { + "type": "integer", + "minimum": 0, + "position": 2 + }, + "privateData": { + "type": "array", + "byteArray": true, + "minItems": 48, + "maxItems": 2048, + "position": 3, + "description": "This is the encrypted values of aliasName + note + displayHidden encoded as an array in cbor" + } + }, + "required": [ + "$createdAt", + "$updatedAt", + "encToUserId", + "privateData", + "rootEncryptionKeyIndex", + "derivationEncryptionKeyIndex" + ], + "additionalProperties": false + }, + "contactRequest": { + "documentsMutable": false, + "canBeDeleted": false, + "requiresIdentityEncryptionBoundedKey": 2, + "requiresIdentityDecryptionBoundedKey": 2, + "type": "object", + "indices": [ + { + "name": "ownerIdUserIdAndAccountRef", + "properties": [ + { + "$ownerId": "asc" + }, + { + "toUserId": "asc" + }, + { + "accountReference": "asc" + } + ], + "unique": true + }, + { + "name": "ownerIdUserId", + "properties": [ + { + "$ownerId": "asc" + }, + { + "toUserId": "asc" + } + ] + }, + { + "name": "userIdCreatedAt", + "properties": [ + { + "toUserId": "asc" + }, + { + "$createdAt": "asc" + } + ] + }, + { + "name": "ownerIdCreatedAt", + "properties": [ + { + "$ownerId": "asc" + }, + { + "$createdAt": "asc" + } + ] + } + ], + "properties": { + "toUserId": { + "type": "array", + "byteArray": true, + "minItems": 32, + "maxItems": 32, + "position": 0, + "contentMediaType": "application/x.dash.dpp.identifier", + "refersTo": { + "type": "identity" + }, + "distinctFrom": "$ownerId" + }, + "encryptedPublicKey": { + "type": "array", + "byteArray": true, + "minItems": 96, + "maxItems": 96, + "encryptedFor": { + "recipient": "toUserId", + "recipientKey": "recipientKeyIndex", + "senderKey": "senderKeyIndex", + "scheme": "ecdh-secp256k1-aes256-cbc" + }, + "position": 1 + }, + "senderKeyIndex": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "position": 2 + }, + "recipientKeyIndex": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "position": 3 + }, + "accountReference": { + "type": "integer", + "minimum": 0, + "position": 4 + }, + "encryptedAccountLabel": { + "type": "array", + "byteArray": true, + "minItems": 48, + "maxItems": 80, + "encryptedFor": { + "recipient": "toUserId", + "recipientKey": "recipientKeyIndex", + "senderKey": "senderKeyIndex", + "scheme": "ecdh-secp256k1-aes256-cbc" + }, + "position": 5 + }, + "autoAcceptProof": { + "type": "array", + "byteArray": true, + "minItems": 38, + "maxItems": 102, + "position": 6 + } + }, + "required": [ + "$createdAt", + "$createdAtCoreBlockHeight", + "toUserId", + "encryptedPublicKey", + "senderKeyIndex", + "recipientKeyIndex", + "accountReference" + ], + "additionalProperties": false + } +} diff --git a/packages/dashpay-contract/src/lib.rs b/packages/dashpay-contract/src/lib.rs index 3132e0acd50..b32b8ab3921 100644 --- a/packages/dashpay-contract/src/lib.rs +++ b/packages/dashpay-contract/src/lib.rs @@ -1,6 +1,7 @@ mod error; pub mod v1; pub mod v2; +pub mod v3; pub use crate::error::Error; use platform_value::{Identifier, IdentifierBytes32}; @@ -19,10 +20,10 @@ pub const OWNER_ID: Identifier = Identifier(IdentifierBytes32(OWNER_ID_BYTES)); pub fn load_definitions(platform_version: &PlatformVersion) -> Result, Error> { match platform_version.system_data_contracts.dashpay { - 1 | 2 => Ok(None), + 1..=3 => Ok(None), version => Err(Error::UnknownVersionMismatch { method: "dashpay_contract::load_definitions".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1, 2, 3], received: version, }), } @@ -31,9 +32,10 @@ pub fn load_documents_schemas(platform_version: &PlatformVersion) -> Result v1::load_documents_schemas(), 2 => v2::load_documents_schemas(), + 3 => v3::load_documents_schemas(), version => Err(Error::UnknownVersionMismatch { method: "dashpay_contract::load_documents_schemas".to_string(), - known_versions: vec![1, 2], + known_versions: vec![1, 2, 3], received: version, }), } diff --git a/packages/dashpay-contract/src/v3/mod.rs b/packages/dashpay-contract/src/v3/mod.rs new file mode 100644 index 00000000000..3d99beaa4c1 --- /dev/null +++ b/packages/dashpay-contract/src/v3/mod.rs @@ -0,0 +1,36 @@ +use crate::error::Error; +use serde_json::Value; + +// Document-type name and property constants live in `crate::v1::document_types`; +// v3 keeps every name and property v2 declares, stored as v2 stores it, and adds +// declarations only: +// +// - `contactRequest.toUserId` refers to an identity and must differ from the +// writer (`refersTo` identity, `distinctFrom` `$ownerId`), the two checks the +// contact request data trigger made up to protocol version 14. +// - `contactRequest.encryptedPublicKey` and `encryptedAccountLabel` name their +// encryption (`encryptedFor`, ECDH between the sender's `senderKeyIndex` key and +// the recipient's `recipientKeyIndex` key, then AES-256-CBC with the IV first), +// so each must be an IV plus whole AES blocks. Both key indexes gain the u32 +// `maximum` that `encryptedFor` requires of a key id; DashPay keeps sized integer +// types off, so they are still stored as i64. +// - `profile`'s `corePaymentAddressType` and `platformPaymentAddressType` rules +// hold the first byte of each DIP-33 payment address to the address type, the +// check the profile data trigger made up to protocol version 14. +// +// The DIP-33 profile address formats, kept here rather than as schema +// descriptions, which are stored and billed with the contract: +// +// - `corePaymentAddress` and `platformPaymentAddress`: the storage form of an +// address, a type byte (0x00 P2PKH, 0x01 P2SH) followed by the 20-byte HASH160 +// (RIPEMD160 of SHA256) of the public key or redeem script. Clients render a +// Core address as Base58Check for the network they are on. Payments to either +// are publicly linkable to the profile. +// - `shieldedAddress`: a raw Orchard receiving address, the 11-byte diversifier +// followed by the 32-byte diversified transmission key. Clients validate it +// before paying; wallets should use a dedicated tip account. + +pub fn load_documents_schemas() -> Result { + serde_json::from_str(include_str!("../../schema/v3/dashpay.schema.json")) + .map_err(Error::InvalidSchemaJson) +} diff --git a/packages/rs-dpp/src/system_data_contracts.rs b/packages/rs-dpp/src/system_data_contracts.rs index df426efcf30..66277a28950 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -1459,3 +1459,136 @@ mod dpns_v3_tests { ); } } + +#[cfg(test)] +mod dashpay_v3_tests { + use super::*; + use crate::data_contract::accessors::v0::DataContractV0Getters; + use crate::data_contract::document_type::accessors::{ + DocumentTypeV0Getters, DocumentTypeV2Getters, + }; + use crate::data_contract::document_type::property_constraints::{ + ConstraintExpression, PropertyConstraint, + }; + use crate::data_contract::document_type::{ + DistinctFrom, DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentTypeRef, + EncryptedFor, EncryptedForRecipient, EncryptionScheme, + }; + + /// DashPay v3, registered as the chain registers it, under full validation. + fn dashpay() -> DataContract { + load_system_data_contract(SystemDataContract::Dashpay, PlatformVersion::latest()) + .expect("DashPay v3 registers under full validation") + } + + fn document_type<'a>(contract: &'a DataContract, name: &str) -> DocumentTypeRef<'a> { + contract + .document_type_for_name(name) + .unwrap_or_else(|_| panic!("the {name} type")) + } + + /// The upgrade to protocol version 15 replaces DashPay v2 with v3 through + /// `apply_contract`, which runs no contract update checks, as the upgrade to 14 + /// replaced v1 with v2. Documents written under either are read with v3's types + /// from then on, so every property v1 or v2 declares must be stored exactly as + /// it was. DashPay keeps sized integer types off: an integer is an i64 whatever + /// its bounds, so the key indexes' new `maximum` changes nothing stored. + #[test] + fn should_store_every_dashpay_v1_and_v2_property_as_it_was_stored() { + let v3 = dashpay(); + for protocol_version in [13, 14] { + let older = load_system_data_contract( + SystemDataContract::Dashpay, + PlatformVersion::get(protocol_version).expect("a known protocol version"), + ) + .expect("an older DashPay"); + for (type_name, older_type) in older.document_types() { + let v3_type = v3 + .document_type_for_name(type_name) + .unwrap_or_else(|_| panic!("v3 keeps the {type_name} type")); + for (path, older_property) in older_type.flattened_properties() { + let v3_property = v3_type + .flattened_properties() + .get(path) + .unwrap_or_else(|| panic!("v3 keeps {type_name}.{path}")); + assert_eq!( + older_property.property_type.stored_encoding(), + v3_property.property_type.stored_encoding(), + "{type_name}.{path} must be stored as DashPay at protocol version \ + {protocol_version} stored it" + ); + } + } + } + } + + /// The two checks of the contact request data trigger, as declarations: the + /// recipient is an identity, and not the writer. The two ECDH fields name their + /// encryption, so their lengths must be an IV plus whole AES blocks. + #[test] + fn should_declare_the_contact_request_checks_and_encryption() { + let contract = dashpay(); + let contact_request = document_type(&contract, "contactRequest"); + let to_user_id = contact_request + .flattened_properties() + .get("toUserId") + .expect("toUserId"); + + assert_eq!( + to_user_id.property_type, + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Identity + ) + ); + assert_eq!(to_user_id.distinct_from, Some(DistinctFrom::OwnerId)); + + let recipe = EncryptedFor { + recipient: EncryptedForRecipient::Property("toUserId".to_string()), + recipient_key: "recipientKeyIndex".to_string(), + sender_key: "senderKeyIndex".to_string(), + scheme: EncryptionScheme::EcdhSecp256k1Aes256Cbc, + }; + let mut encrypted = contact_request + .encrypted_properties() + .into_iter() + .map(|(path, encrypted_for)| (path.as_str(), encrypted_for.clone())) + .collect::>(); + encrypted.sort_by_key(|(path, _)| *path); + assert_eq!( + encrypted, + vec![ + ("encryptedAccountLabel", recipe.clone()), + ("encryptedPublicKey", recipe), + ] + ); + } + + /// The check of the profile data trigger, as two rules: each DIP-33 payment + /// address starts with its type byte, 0x00 (P2PKH) or 0x01 (P2SH). A profile + /// without the address reads its first byte as 0, so the rule holds, as the + /// trigger skipped an absent address. + #[test] + fn should_hold_each_profile_payment_address_to_its_type_byte() { + let contract = dashpay(); + let profile = document_type(&contract, "profile"); + let rules = profile.property_constraints(); + + assert_eq!(rules.len(), 2); + for (rule_name, address) in [ + ("corePaymentAddressType", "corePaymentAddress"), + ("platformPaymentAddressType", "platformPaymentAddress"), + ] { + assert_eq!( + rules.get(rule_name), + Some(&PropertyConstraint::In { + operand: ConstraintExpression::ByteAt { + path: address.to_string(), + index: 0, + }, + values: [0, 1].into(), + }), + "the {rule_name} rule" + ); + } + } +} diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index 7870dd11be0..46db0a1d6a4 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -300,6 +300,7 @@ mod tests { use dpp::secp256k1::Keypair; use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::random_document::{ CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, }; @@ -734,8 +735,8 @@ mod tests { // the larger DashPay v2 schema adds byte-billed contract bytes // from protocol version 15 the contract is stored compiled (format version 2), about // 1.8 times its bytes, which are byte-billed, and checking its compiled size is - // charged per compiled byte - 24004476680 + // charged per compiled byte; the DashPay v3 schema adds its declarations + 24004512230 ); let check_result = platform @@ -1089,7 +1090,8 @@ mod tests { // We have one invalid paid for state transition assert_eq!(processing_result.invalid_paid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 346660); + // DashPay v3's declarations add to the schema the validation is billed for + assert_eq!(processing_result.aggregated_fees().processing_fee, 346810); let check_result = platform .check_tx( @@ -1404,8 +1406,8 @@ mod tests { // the larger DashPay v2 schema adds byte-billed contract bytes // from protocol version 15 the contract is stored compiled (format version 2), about // 1.8 times its bytes, which are byte-billed, and checking its compiled size is - // charged per compiled byte - 24008953360 + // charged per compiled byte; the DashPay v3 schema adds its declarations + 24009024460 ); let check_result = platform @@ -1489,8 +1491,8 @@ mod tests { public_keys: BTreeMap::from([(1, key.clone())]), // we have enough balance only for 1 insertion (this is where this test is different); // from protocol version 15 one insertion stores the contract compiled, about 1.8 - // times its bytes, so it needs more - balance: 24_300_000_000, + // times its bytes, and DashPay v3 adds its declarations, so it needs more + balance: 24_400_000_000, revision: 0, } .into(); @@ -1888,8 +1890,8 @@ mod tests { // the larger DashPay v2 schema adds byte-billed contract bytes // from protocol version 15 the contract is stored compiled (format version 2), about // 1.8 times its bytes, which are byte-billed, and checking its compiled size is - // charged per compiled byte - 24004476680 + // charged per compiled byte; the DashPay v3 schema adds its declarations + 24004512230 ); platform @@ -1985,8 +1987,8 @@ mod tests { // `$createdAtBlockHeight` required, a ttl, the `atMostAThousandPerName` rule); from // protocol version 15 the contract is read and stored compiled (format version 2), // about 1.8 times its bytes, which are byte-billed, and checking its compiled size - // is charged per compiled byte - 29005214560 + // is charged per compiled byte; the DashPay v3 schema adds its declarations + 29005254750 ); let check_result = platform @@ -2579,8 +2581,8 @@ mod tests { // the larger DashPay v2 schema adds byte-billed contract bytes // from protocol version 15 the contract is stored compiled (format version 2), about // 1.8 times its bytes, which are byte-billed, and checking its compiled size is - // charged per compiled byte - 24004476680 + // charged per compiled byte; the DashPay v3 schema adds its declarations + 24004512230 ); platform @@ -2703,7 +2705,8 @@ mod tests { // We have one invalid paid for state transition assert_eq!(processing_result.invalid_paid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 448640); + // DashPay v3's declarations add to the schema the validation is billed for + assert_eq!(processing_result.aggregated_fees().processing_fee, 448790); let check_result = platform .check_tx( @@ -2856,6 +2859,12 @@ mod tests { .expect("expected to set the document id"); document.set("avatarUrl", "http://test.com/bob.jpg".into()); + // The generator fills the payment addresses with random bytes, and from DashPay v3 + // (protocol version 15) a rule holds each to its type byte, 0x00 or 0x01 + if profile.properties().contains_key("corePaymentAddress") { + document.set("corePaymentAddress", vec![0u8; 21].into()); + document.set("platformPaymentAddress", vec![1u8; 21].into()); + } let mut altered_document = document.clone(); diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 78094eaa3b1..9c4598a1d46 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -915,8 +915,10 @@ impl Platform { /// When transitioning to version 15 we re-store the DPNS contract whose v3 /// schema checks a domain create with keywords instead of most of its data - /// trigger, create the vote decisions trees that yes/no polls live in, and - /// insert the decision proposals contract whose proposals open those polls. + /// trigger, re-store the DashPay contract whose v3 schema states the checks of + /// both its data triggers, create the vote decisions trees that yes/no polls + /// live in, and insert the decision proposals contract whose proposals open + /// those polls. fn transition_to_version_15( &self, block_info: &BlockInfo, @@ -936,6 +938,22 @@ impl Platform { platform_version, )?; + // DashPay contract v3 (v15 note 14): the checks of its two data triggers become schema + // keywords. It only adds declarations and keeps every property stored as v2 stores it, + // so the documents written under v1 and v2 read back unchanged; the upgrade replaces the + // contract without the contract update checks, as the one to v2 did. + let dashpay_contract = + load_system_data_contract(SystemDataContract::Dashpay, platform_version)?; + + self.drive.apply_contract( + &dashpay_contract, + *block_info, + true, + None, + Some(transaction), + platform_version, + )?; + // Vote decisions trees: the active yes/no polls and the identity votes index of the // decisions branch. Fresh chains call the same helper from // `create_initial_state_structure` v4, behind a `>= 15` check on the chain's initial @@ -2373,6 +2391,238 @@ mod tests { ); } + /// The v14→v15 boundary through the production dispatcher: DashPay v3 replaces v2 + /// without the contract update checks, so the profile, contact request and contact + /// info written under v2 must read back unchanged under v3. DashPay stores every + /// integer as an i64; the contact request's `accountReference` takes all eight bytes. + #[tokio::test] + async fn test_protocol_change_v14_to_v15_upgrades_dashpay_and_keeps_v2_documents_readable() { + use crate::execution::validation::state_transition::tests::setup_identity; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use assert_matches::assert_matches; + use dpp::dash_to_credits; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; + use dpp::data_contract::document_type::random_document::{ + CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, + }; + use dpp::data_contract::document_type::{ + DocumentPropertyReferenceTarget, DocumentPropertyType, + }; + use dpp::data_contract::DataContract; + use dpp::document::{Document, DocumentV0Setters}; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::platform_value::{Bytes32, Value}; + use dpp::serialization::PlatformSerializable; + use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; + use dpp::state_transition::batch_transition::BatchTransition; + use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::query::DriveDocumentQuery; + use rand::rngs::StdRng; + use rand::SeedableRng; + + const DOCUMENT_TYPES: [&str; 3] = ["profile", "contactRequest", "contactInfo"]; + + let platform_version_14 = PlatformVersion::get(14).expect("expected platform version 14"); + let platform_version_15 = PlatformVersion::get(15).expect("expected platform version 15"); + + 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, 958, dash_to_credits!(0.1)); + let (recipient, ..) = setup_identity(&mut platform, 495, dash_to_credits!(0.1)); + + let dashpay_v2 = platform + .drive + .cache + .system_data_contracts + .load_dashpay(platform_version_14) + .expect("expected the dashpay system contract"); + + // One document of each type through the normal pipeline at protocol version 14 + let mut rng = StdRng::seed_from_u64(439); + let mut transitions = vec![]; + for (nonce, document_type_name) in (2..).zip(DOCUMENT_TYPES) { + let document_type = dashpay_v2 + .document_type_for_name(document_type_name) + .expect("expected the document type"); + let entropy = Bytes32::random_with_rng(&mut rng); + let mut document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version_14, + ) + .expect("expected a random v2 document"); + document + .set_id_for_creation(document_type, &entropy.0, nonce, platform_version_14) + .expect("expected to set the document id"); + // Random i64 integers ignore the schema's `minimum`, so the integers are set + match document_type_name { + "profile" => { + // the v2 profile trigger requires a known address type byte + let mut core_address = vec![0u8]; + core_address.extend([3u8; 20]); + let mut platform_address = vec![1u8]; + platform_address.extend([4u8; 20]); + document.set("corePaymentAddress", Value::Bytes(core_address)); + document.set("platformPaymentAddress", Value::Bytes(platform_address)); + document.set("avatarUrl", "http://test.com/bob.jpg".into()); + } + "contactRequest" => { + // the v2 contact request trigger requires an existing recipient + document.set("toUserId", Value::Identifier(recipient.id().to_buffer())); + document.set("senderKeyIndex", Value::I64(1)); + document.set("recipientKeyIndex", Value::I64(0)); + document.set("accountReference", Value::I64(1 << 40)); + } + _ => { + document.set("rootEncryptionKeyIndex", Value::I64(3)); + document.set("derivationEncryptionKeyIndex", Value::I64(5)); + } + } + transitions.push( + BatchTransition::new_document_creation_transition_from_document( + document, + document_type, + entropy.0, + &key, + nonce, + 0, + None, + &signer, + platform_version_14, + None, + ) + .await + .expect("expect to create documents batch transition") + .serialize_to_bytes() + .expect("expected serialized transition"), + ); + } + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &transitions, + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version_14, + false, + None, + ) + .expect("expected to process state transitions"); + assert_matches!( + processing_result.execution_results().as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit"); + + let stored_documents = |contract: &DataContract, + document_type_name: &str, + platform_version: &PlatformVersion| + -> Vec { + let query = DriveDocumentQuery::from_sql_expr( + &format!("select * from {document_type_name}"), + contract, + Some(&platform.config.drive), + platform_version, + ) + .expect("expected a document query"); + platform + .drive + .query_documents(query, None, false, None, None) + .expect("expected to query documents") + .documents() + .to_vec() + }; + let documents_v2 = DOCUMENT_TYPES.map(|document_type_name| { + let documents = stored_documents(&dashpay_v2, document_type_name, platform_version_14); + assert_eq!( + documents.len(), + 1, + "expected one stored {document_type_name}" + ); + documents + }); + + let block_info = BlockInfo { + time_ms: 2_000_000, + height: 200, + core_height: 200, + epoch: Epoch::new(1).expect("expected epoch"), + }; + let transaction = platform.drive.grove.start_transaction(); + platform + .perform_events_on_first_block_of_protocol_change( + &platform_state, + &block_info, + &transaction, + 14, + platform_version_15, + ) + .expect("expected the protocol change events to succeed"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the upgrade"); + + let (_fee, fetch_info) = platform + .drive + .get_contract_with_fetch_info_and_fee( + *SystemDataContract::Dashpay.id().as_bytes(), + None, + true, + None, + platform_version_15, + ) + .expect("expected to fetch DashPay contract post-upgrade"); + let dashpay_v3 = fetch_info + .expect("expected the DashPay contract") + .contract + .clone(); + assert_eq!(dashpay_v3.version(), 3, "the upgrade stores DashPay v3"); + assert_eq!( + dashpay_v3 + .document_type_for_name("contactRequest") + .expect("expected the contactRequest document type") + .flattened_properties() + .get("toUserId") + .expect("expected toUserId") + .property_type, + DocumentPropertyType::IdentifierWithReference( + DocumentPropertyReferenceTarget::Identity + ), + "the stored DashPay v3 declares the contact request's identity reference" + ); + + for (document_type_name, documents) in DOCUMENT_TYPES.into_iter().zip(documents_v2) { + assert_eq!( + stored_documents(&dashpay_v3, document_type_name, platform_version_15), + documents, + "v3 must read the v2 {document_type_name} back unchanged" + ); + } + } + // test_transition_to_version_9 removed: requires prior state from versions 4-8 #[test] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/mod.rs index e7e67702fd8..e80eed9163f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/mod.rs @@ -5,6 +5,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::bindin mod v0; mod v1; mod v2; +mod v3; pub fn data_trigger_bindings_list( platform_version: &PlatformVersion, @@ -29,9 +30,13 @@ pub fn data_trigger_bindings_list( .into_iter() .map(|binding| binding.into()) .collect()), + 3 => Ok(v3::data_trigger_bindings_list_v3()? + .into_iter() + .map(|binding| binding.into()) + .collect()), version => Err(ProtocolError::UnknownVersionMismatch { method: "data_trigger_bindings".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/data_triggers/bindings/list/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v3/mod.rs new file mode 100644 index 00000000000..14a7a2db3e2 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v3/mod.rs @@ -0,0 +1,29 @@ +use crate::execution::validation::state_transition::batch::data_triggers::bindings::data_trigger_binding::DataTriggerBindingV0; +use crate::execution::validation::state_transition::batch::data_triggers::bindings::list::v2::data_trigger_bindings_list_v2; + +use dpp::errors::ProtocolError; +use dpp::system_data_contracts::dashpay_contract; + +/// Retrieves a list of data triggers binding with matching params. +/// +/// v3 (PROTOCOL_VERSION_15) is v2 without the DashPay bindings. The DashPay +/// contract v3 that protocol version selects declares what both triggers checked: +/// `contactRequest.toUserId` refers to an identity (`refersTo`) other than the +/// writer (`distinctFrom`), and the `profile` rules `corePaymentAddressType` and +/// `platformPaymentAddressType` hold each payment address's type byte to 0x00 or +/// 0x01. Every other binding is v2's. +/// +/// # Returns +/// +/// A `Vec` containing all known data triggers. +/// +/// # Errors +/// +/// Returns a `ProtocolError` if there was an error. +#[inline(always)] +pub(super) fn data_trigger_bindings_list_v3() -> Result, ProtocolError> { + Ok(data_trigger_bindings_list_v2()? + .into_iter() + .filter(|binding| binding.data_contract_id != dashpay_contract::ID) + .collect()) +} 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 7465295af98..6abda31b596 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 @@ -168,14 +168,25 @@ mod creation_tests { .expect("expected to commit transaction"); } + /// Up to protocol version 14 the profile data trigger checks the type byte of + /// both DIP-33 payment addresses; from 15 the DashPay v3 rules + /// `corePaymentAddressType` and `platformPaymentAddressType` do. #[tokio::test] async fn should_enforce_profile_payment_address_type_bytes() { + for protocol_version in [14, PlatformVersion::latest().protocol_version] { + enforce_profile_payment_address_type_bytes_at(protocol_version).await; + } + } + + async fn enforce_profile_payment_address_type_bytes_at(protocol_version: u32) { + use dpp::consensus::basic::BasicError; use dpp::consensus::state::data_trigger::DataTriggerError; use dpp::consensus::state::state_error::StateError; - let platform_version = PlatformVersion::latest(); + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); let mut platform = TestPlatformBuilder::new() - .with_latest_protocol_version() + .with_initial_protocol_version(protocol_version) .build_with_mock_rpc() .set_genesis_state(); @@ -197,7 +208,7 @@ mod creation_tests { .document_type_for_name("profile") .expect("expected a profile document type"); - // (field under test, leading type byte, accepted by the data trigger) + // (field under test, leading type byte, accepted) let cases = [ ("corePaymentAddress", 0x00u8, true), ("corePaymentAddress", 0x01, true), @@ -275,9 +286,10 @@ mod creation_tests { assert_matches!( processing_result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }], - "{field} with type byte 0x{leading_byte:02x} must be accepted" + "{field} with type byte 0x{leading_byte:02x} must be accepted at protocol \ + version {protocol_version}" ); - } else { + } else if protocol_version <= 14 { assert_matches!( processing_result.execution_results().as_slice(), [StateTransitionExecutionResult::PaidConsensusError { @@ -286,7 +298,21 @@ mod creation_tests { )), .. }], - "{field} with type byte 0x{leading_byte:02x} must be rejected" + "{field} with type byte 0x{leading_byte:02x} must be rejected by the data \ + trigger at protocol version {protocol_version}" + ); + } else { + let rule = format!("{field}Type"); + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError( + BasicError::DocumentPropertyConstraintViolatedError(e) + ), + .. + }] if e.constraint() == rule, + "{field} with type byte 0x{leading_byte:02x} must be rejected by the \ + {rule} rule at protocol version {protocol_version}" ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_v3.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_v3.rs new file mode 100644 index 00000000000..11e8d5854d1 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_v3.rs @@ -0,0 +1,365 @@ +//! The DashPay system contract's checks as the contract states them from +//! protocol version 15 (DashPay v3), against the data triggers that made them up +//! to protocol version 14 (DashPay v2, data trigger bindings 2). +//! +//! - A contact request to oneself: `distinctFrom: "$ownerId"` on `toUserId` +//! (10419), where the trigger refused it (40500). +//! - A contact request to an identity that does not exist: `refersTo` identity on +//! `toUserId` (40120), where the trigger refused it (40500). +//! - An account label that is not an IV plus whole AES blocks: `encryptedFor` +//! (10420), accepted up to protocol version 14. +//! - A profile payment address whose type byte is not 0x00 or 0x01, on create and +//! on replace: the `byteAt` rules (10422), where the trigger refused it (40500). + +use super::*; + +mod dashpay_v3_tests { + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::consensus::basic::BasicError; + use dpp::consensus::state::data_trigger::DataTriggerError; + use dpp::data_contract::document_type::DocumentPropertyReferenceTarget; + use dpp::document::Document; + use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::prelude::{DataContract, IdentityNonce}; + use dpp::state_transition::StateTransition; + use simple_signer::signer::SimpleSigner; + use std::sync::Arc; + + /// The last protocol version whose DashPay checks are data triggers. + const PROTOCOL_VERSION_WITH_TRIGGERS: u32 = 14; + + struct Fixture { + platform: TempPlatform, + platform_version: &'static PlatformVersion, + dashpay: Arc, + sender: Identity, + signer: SimpleSigner, + key: IdentityPublicKey, + recipient: Identity, + } + + impl Fixture { + fn new(protocol_version: u32) -> Self { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let (sender, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (recipient, ..) = setup_identity(&mut platform, 495, dash_to_credits!(0.1)); + + let dashpay = platform + .drive + .cache + .system_data_contracts + .load_dashpay(platform_version) + .expect("expected the dashpay system contract"); + + Fixture { + platform, + platform_version, + dashpay, + sender, + signer, + key, + recipient, + } + } + + fn latest() -> Self { + Self::new(PlatformVersion::latest().protocol_version) + } + + fn with_triggers() -> Self { + Self::new(PROTOCOL_VERSION_WITH_TRIGGERS) + } + + /// A random document of the sender, its id derived for a create under `nonce`. + fn random_document( + &self, + document_type_name: &str, + seed: u64, + nonce: IdentityNonce, + ) -> (Document, Bytes32) { + let document_type = self + .dashpay + .document_type_for_name(document_type_name) + .expect("expected the document type"); + let mut rng = StdRng::seed_from_u64(seed); + let entropy = Bytes32::random_with_rng(&mut rng); + let mut document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + self.sender.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + self.platform_version, + ) + .expect("expected a random document"); + document + .set_id_for_creation(document_type, &entropy.0, nonce, self.platform_version) + .expect("expected to set the document id"); + (document, entropy) + } + + /// A contact request from the sender to `to_user_id` whose account label + /// is `label_length` bytes. + fn contact_request( + &self, + to_user_id: Identifier, + label_length: usize, + ) -> (Document, Bytes32) { + let (mut document, entropy) = self.random_document("contactRequest", 437, 2); + document.set("toUserId", Value::Identifier(to_user_id.to_buffer())); + document.set("senderKeyIndex", Value::U32(1)); + document.set("recipientKeyIndex", Value::U32(1)); + document.set("accountReference", Value::U32(0)); + document.set("encryptedPublicKey", Value::Bytes(vec![7u8; 96])); + document.set( + "encryptedAccountLabel", + Value::Bytes(vec![8u8; label_length]), + ); + (document, entropy) + } + + /// A profile of the sender whose two payment addresses are valid, to be + /// created under `nonce`. + fn profile(&self, nonce: IdentityNonce) -> (Document, Bytes32) { + let (mut document, entropy) = self.random_document("profile", 438, nonce); + set_valid_profile_payment_addresses( + &mut document, + self.dashpay + .document_type_for_name("profile") + .expect("expected the profile document type"), + ); + document.set("avatarUrl", "http://test.com/bob.jpg".into()); + (document, entropy) + } + + async fn create( + &self, + document_type_name: &str, + document: Document, + entropy: Bytes32, + nonce: IdentityNonce, + ) -> StateTransitionExecutionResult { + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + self.dashpay + .document_type_for_name(document_type_name) + .expect("expected the document type"), + entropy.0, + &self.key, + nonce, + 0, + None, + &self.signer, + self.platform_version, + None, + ) + .await + .expect("expect to create documents batch transition"); + self.process(transition) + } + + async fn replace( + &self, + document_type_name: &str, + document: Document, + nonce: IdentityNonce, + ) -> StateTransitionExecutionResult { + let transition = BatchTransition::new_document_replacement_transition_from_document( + document, + self.dashpay + .document_type_for_name(document_type_name) + .expect("expected the document type"), + &self.key, + nonce, + 0, + None, + &self.signer, + self.platform_version, + None, + ) + .await + .expect("expect to create documents batch transition"); + self.process(transition) + } + + fn process(&self, transition: StateTransition) -> StateTransitionExecutionResult { + let platform_state = self.platform.state.load(); + let transaction = self.platform.drive.grove.start_transaction(); + let processing_result = self + .platform + .platform + .process_raw_state_transitions( + &vec![transition + .serialize_to_bytes() + .expect("expected documents batch serialized state transition")], + &platform_state, + &BlockInfo::default(), + &transaction, + self.platform_version, + false, + None, + ) + .expect("expected to process state transition"); + self.platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + processing_result.into_execution_results().remove(0) + } + } + + fn paid_error(result: StateTransitionExecutionResult) -> ConsensusError { + let StateTransitionExecutionResult::PaidConsensusError { error, .. } = result else { + panic!("expected a paid consensus error, got {result:?}"); + }; + error + } + + fn is_data_trigger_condition_error(error: &ConsensusError) -> bool { + matches!( + error, + ConsensusError::StateError(StateError::DataTriggerError( + DataTriggerError::DataTriggerConditionError(_) + )) + ) + } + + /// A payment address of the given type byte followed by a 20-byte hash. + fn payment_address(type_byte: u8) -> Value { + let mut address = vec![type_byte]; + address.extend([0u8; 20]); + Value::Bytes(address) + } + + #[tokio::test] + async fn should_create_a_contact_request_to_another_identity() { + for fixture in [Fixture::with_triggers(), Fixture::latest()] { + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 48); + + assert_matches!( + fixture.create("contactRequest", document, entropy, 2).await, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "at protocol version {}", + fixture.platform_version.protocol_version + ); + } + } + + /// The trigger's first check, from protocol version 15 `distinctFrom: "$ownerId"`. + #[tokio::test] + async fn should_refuse_a_contact_request_to_oneself() { + let fixture = Fixture::with_triggers(); + let (document, entropy) = fixture.contact_request(fixture.sender.id(), 48); + let error = paid_error(fixture.create("contactRequest", document, entropy, 2).await); + assert!(is_data_trigger_condition_error(&error), "{error:?}"); + + let fixture = Fixture::latest(); + let (document, entropy) = fixture.contact_request(fixture.sender.id(), 48); + assert_matches!( + paid_error(fixture.create("contactRequest", document, entropy, 2).await), + ConsensusError::BasicError(BasicError::DocumentPropertyNotDistinctError(e)) + if e.document_type_name() == "contactRequest" + && e.property() == "toUserId" + && e.distinct_from() == "$ownerId" + ); + } + + /// The trigger's second check, from protocol version 15 the `refersTo` + /// identity declaration on `toUserId`. + #[tokio::test] + async fn should_refuse_a_contact_request_to_an_identity_that_does_not_exist() { + let missing = Identifier::from([0xAB; 32]); + + let fixture = Fixture::with_triggers(); + let (document, entropy) = fixture.contact_request(missing, 48); + let error = paid_error(fixture.create("contactRequest", document, entropy, 2).await); + assert!(is_data_trigger_condition_error(&error), "{error:?}"); + + let fixture = Fixture::latest(); + let (document, entropy) = fixture.contact_request(missing, 48); + assert_matches!( + paid_error(fixture.create("contactRequest", document, entropy, 2).await), + ConsensusError::StateError(StateError::ReferencedEntityNotFoundError(e)) + if *e.entity_id() == missing + && *e.entity_type() == DocumentPropertyReferenceTarget::Identity + && e.path() == "toUserId" + ); + } + + /// New from protocol version 15: an account label of 50 bytes is within the + /// schema's 48 to 80 but is not an IV plus whole AES-CBC blocks. + #[tokio::test] + async fn should_refuse_an_account_label_that_is_not_an_iv_plus_whole_blocks() { + let fixture = Fixture::with_triggers(); + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 50); + assert_matches!( + fixture.create("contactRequest", document, entropy, 2).await, + StateTransitionExecutionResult::SuccessfulExecution { .. } + ); + + let fixture = Fixture::latest(); + let (document, entropy) = fixture.contact_request(fixture.recipient.id(), 50); + assert_matches!( + paid_error(fixture.create("contactRequest", document, entropy, 2).await), + ConsensusError::BasicError(BasicError::InvalidEncryptedPropertyShapeError(e)) + if e.property() == "encryptedAccountLabel" && e.actual_length() == 50 + ); + } + + /// The profile trigger checked both addresses on create and on replace; from + /// protocol version 15 the rules do. + #[tokio::test] + async fn should_refuse_a_profile_payment_address_of_an_unknown_type_on_create_and_replace() { + for (field, rule) in [ + ("corePaymentAddress", "corePaymentAddressType"), + ("platformPaymentAddress", "platformPaymentAddressType"), + ] { + for fixture in [Fixture::with_triggers(), Fixture::latest()] { + let protocol_version = fixture.platform_version.protocol_version; + let expect_refusal = |error: ConsensusError| { + if protocol_version <= PROTOCOL_VERSION_WITH_TRIGGERS { + assert!(is_data_trigger_condition_error(&error), "{error:?}"); + } else { + assert_matches!( + error, + ConsensusError::BasicError( + BasicError::DocumentPropertyConstraintViolatedError(e) + ) if e.document_type_name() == "profile" && e.constraint() == rule + ); + } + }; + + // a refused write is paid for and uses its nonce, so each write + // takes the next one + let (mut document, entropy) = fixture.profile(2); + document.set(field, payment_address(0x02)); + expect_refusal(paid_error( + fixture.create("profile", document, entropy, 2).await, + )); + + let (mut document, entropy) = fixture.profile(3); + assert_matches!( + fixture + .create("profile", document.clone(), entropy, 3) + .await, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "a profile with valid addresses at protocol version {protocol_version}" + ); + document.increment_revision().expect("expected a revision"); + document.set(field, payment_address(0xff)); + expect_refusal(paid_error(fixture.replace("profile", document, 4).await)); + } + } + } +} 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 e34a17c55b8..ce6133b88fa 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 @@ -897,6 +897,9 @@ mod deletion_tests { document.set("recipientKeyIndex", Value::U32(1)); document.set("senderKeyIndex", Value::U32(1)); document.set("accountReference", Value::U32(0)); + // an IV plus whole AES blocks, as the label's `encryptedFor` requires from protocol + // version 15 + document.set("encryptedAccountLabel", Value::Bytes(vec![0u8; 48])); let mut altered_document = document.clone(); 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 cea507b13de..78d63511ee8 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 @@ -3,6 +3,7 @@ mod agreement_values; mod commit_reveal_lookup; mod contract_owner_requirement; mod creation; +mod dashpay_v3; mod deletable_document_reference; mod delete_constraints; mod deletion; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 7ea76a4008f..bbdd64ea9b4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -1315,6 +1315,11 @@ mod replacement_tests { document.set("recipientKeyIndex", Value::U32(1)); document.set("senderKeyIndex", Value::U32(1)); document.set("accountReference", Value::U32(0)); + // an IV plus whole AES blocks, as the label's `encryptedFor` requires from protocol + // version 15; earlier versions keep the generated label their fee pins were taken with + if protocol_version >= 15 { + document.set("encryptedAccountLabel", Value::Bytes(vec![0u8; 48])); + } let mut altered_document = document.clone(); @@ -1426,7 +1431,7 @@ mod replacement_tests { async fn test_document_replace_on_document_type_that_is_not_mutable() { run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version( PlatformVersion::latest().protocol_version, - 460940, // v14: stamped documents (see happy-path baseline note) + 460740, // v14: stamped documents (see happy-path baseline note); from v15 a 48-byte label ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/mod.rs index 1d48bcf75fa..c6ab3cd6577 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/mod.rs @@ -3,8 +3,9 @@ mod token; /// Sets valid DIP-33 payment addresses on a random profile document when the /// schema version carries the fields (dashpay v2, protocol version 14+): the -/// generator fills them with random bytes, and the data trigger requires a -/// valid leading type byte (0x00 P2PKH / 0x01 P2SH). +/// generator fills them with random bytes, and a valid leading type byte (0x00 +/// P2PKH / 0x01 P2SH) is required, by the data trigger at protocol version 14 and +/// by the DashPay v3 rules from 15. pub(crate) fn set_valid_profile_payment_addresses( document: &mut dpp::document::Document, profile: dpp::data_contract::document_type::DocumentTypeRef, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs index e44bc9c7dbb..8578857ae32 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs @@ -39,7 +39,7 @@ use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_payment_info::TokenPaymentInfo; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tests::json_document::json_document_to_json_value; -use dpp::system_data_contracts::dashpay_contract; +use dpp::system_data_contracts::dpns_contract; use dpp::validation::ConsensusValidationResult; use dpp::util::hash::hash_single; use dpp::version::DefaultForPlatformVersion; @@ -734,7 +734,7 @@ async fn should_preserve_single_entry_full_processor_fees_and_result() { .value .unwrap() ), - "a1aba6416f1f58b9a2ae5bdad21b4aacba05a20ea4d698fb98d6aed180d2be57" + "40d65df3163982165992d950fe8480907ec4348d921aa881016ff4b8113bbf72" ); assert_eq!( pool.platform @@ -1589,16 +1589,29 @@ async fn should_reject_a_duplicate_payment_before_its_failing_trigger_without_ch let mut json = json_document_to_json_value( "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency-burn-tokens.json").unwrap(); json.as_object_mut().unwrap().remove("tokens"); - json["documentSchemas"] = serde_json::json!({ "contactRequest": { + // A `domain` under the DPNS contract's id, so the DPNS domain create trigger runs on it. + // From protocol version 15 that trigger is the one left that reads state before it fails + // a create: it looks the parent up, and a parent that does not exist refuses the name. + json["documentSchemas"] = serde_json::json!({ "domain": { "type": "object", "documentsMutable": false, "canBeDeleted": false, - "properties": { "toUserId": { "type": "identifier", "position": 0 } }, - "required": ["toUserId"], "additionalProperties": false, + "indices": [{ "name": "parentNameAndLabel", "properties": [ + { "normalizedParentDomainName": "asc" }, { "normalizedLabel": "asc" } + ] }], + "properties": { + "normalizedLabel": { "type": "string", "maxLength": 63, "position": 0 }, + "normalizedParentDomainName": { "type": "string", "maxLength": 63, "position": 1 }, + "subdomainRules": { "type": "object", "position": 2, + "properties": { "allowSubdomains": { "type": "boolean", "position": 0 } }, + "required": ["allowSubdomains"], "additionalProperties": false } + }, + "required": ["normalizedLabel", "normalizedParentDomainName", "subdomainRules"], + "additionalProperties": false, "tokenCost": { "create": { "contractId": pool.contract.id().to_buffer(), "tokenPosition": 0, "amount": 10, "effect": 0 } } } }); let mut contract = DataContract::from_json(json, true, version).expect("valid trigger/payment contract"); - contract.set_id(dashpay_contract::ID.into()); + contract.set_id(dpns_contract::ID.into()); contract.set_owner_id(pool.identity.id()); contract.set_created_at_epoch(Some(0)); contract.set_created_at(Some(0)); @@ -1614,7 +1627,7 @@ async fn should_reject_a_duplicate_payment_before_its_failing_trigger_without_ch version, ) .unwrap(); - let document_type = contract.document_type_for_name("contactRequest").unwrap(); + let document_type = contract.document_type_for_name("domain").unwrap(); let mut rng = StdRng::seed_from_u64(70_505); let entropy = Bytes32::random_with_rng(&mut rng); let mut document = document_type @@ -1627,7 +1640,19 @@ async fn should_reject_a_duplicate_payment_before_its_failing_trigger_without_ch version, ) .unwrap(); - document.set("toUserId", Value::Identifier([96; 32])); + // a name under `dash`, which this contract does not hold + document.set("normalizedLabel", Value::Text("alice".to_string())); + document.set( + "normalizedParentDomainName", + Value::Text("dash".to_string()), + ); + document.set( + "subdomainRules", + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ); document .set_id_for_creation(document_type, &entropy.0, 1, version) .unwrap(); @@ -1709,7 +1734,7 @@ async fn should_reject_a_duplicate_payment_before_its_failing_trigger_without_ch assert_eq!( natural_context.operations_slice().len(), disabled_natural_context.operations_slice().len() + 1, - "the failing recipient trigger must actually perform and bill its identity lookup" + "the failing parent domain trigger must actually perform and bill its parent lookup" ); assert_eq!( format!("{:?}", duplicate_context.operations_slice()), @@ -2158,39 +2183,39 @@ async fn should_preserve_single_entry_actions_errors_operations_and_settled_stat let (expected_validation, expected_settled) = match case { "shield" => ( "6a823c2db94612652926e6b9c1ae7feec9c2547dfd4cfc4e474318da1c16d59c", - "2fd1b754af2ac9b95cb06a74e36a2481c70ab460db1a8ddd5711033c1baab1a8", + "ebe68a4398dda43472c06c11a8a9f9508e117da94975c79f2b19374f01b5d8ac", ), "valid-payment" => ( "9388d5cf63c4a571b9fb009679e2e34d0b681acc88f0f5195e7287c7efbd92f7", - "e59a9cc954d4af978d3c798895b502c649583a61c51e9fc8ee52ee8a28de0902", + "959ccac8b8e2300b8435ab8ffa8ca8ec5076262a072e62701b3d2c7532fcc098", ), "failed-payment" => ( "d9b5fe191841bea952494f2a45e3833f925f4d367a1937b2ef1235a1aa01f51e", - "1eaeb4e54f91a4c95b41fc2966c8d9fce2b27ee01ade95fbeab66deeb2ba3859", + "6ecafed30959c5c18872547858a7b2c6f5194b89ca94de5028b8131e80f6d093", ), "within-bundle-repeat" => ( "8023e03a697a20f6a78396a05503a4bc583ca8f69e19bd7ee07a6ae0ce7be478", - "c7495c527f115ac6a78f8c709b1ac599a9ed683339f739a7e444cce767a180d7", + "32636790c508f5f1f6e20e90f2aec27c3063ace09cf977397e725e91beb785f2", ), "stored-repeat" => ( "05652ac85c972902e8c09d20a7d8586e17e9434a478b335da136ad85f73def01", - "0143d7edf2336ab89b1e83485eceeb29285d38492e16d8ae3645a1fa866eb881", + "333b1c1cae8ba67e345be84be74339e316b7b06475604787cc54a7724fc62487", ), "open-mint-group" => ( "5c68381297c9bf68c5e85ebe6bb9e0f1289e2c07718eecfb5e6773d6009455bb", - "d559cb03752fa942008a30d52efb82584b49d68cc64aebabfacf01bf72d48f48", + "fff8c4edec55b194dec5bf05544cd3916cf072bb72beec1e4cd2f03d46b1e38c", ), "closing-mint-group" => ( "2cedc16732c76e0605b5f46085db3d69c12936a5af55a2a82f52ec9e9ca67e38", - "7ecda0074e06c9c66c7f856680e06d2c9bd391ea8746d3ed90263580b665c115", + "38a9de7c5593318c29dcc221da2ecd0fbbb233f0bd91d2f0a0f59e99b535eb07", ), "open-burn-group" => ( "e664a045f020ff84de1ca98f364fbabccfc141ad46aa82c79491a340bb0db5f8", - "94961c4ec632dca33b79cffcbf9d08981fc77142d392118c21d37a9055feaacb", + "8823858db6d9b02226ab6150dfd0012ffc0e8854526d8407b1a114e073f358bf", ), "closing-burn-group" => ( "93e1732286f168c4a91d2765931e181c5721bfca1bfd5f93b8baacca03e00b2b", - "78bafb1ae5c4a7c804421ac3afbb192eb35a4f1bc093e9a02f1130d8c00dd4c3", + "01d445fc62c77f1a5f2964239f92394cf3f51097a749c13c7626c88a505d03b9", ), _ => unreachable!("enumerated characterization case"), }; diff --git a/packages/rs-drive/tests/deterministic_root_hash.rs b/packages/rs-drive/tests/deterministic_root_hash.rs index a7ce5a3cae7..ad10f4caee3 100644 --- a/packages/rs-drive/tests/deterministic_root_hash.rs +++ b/packages/rs-drive/tests/deterministic_root_hash.rs @@ -312,8 +312,9 @@ mod tests { // and ships DashPay contract v2 with the optional `shieldedAddress` profile field. 14 => "c25bc863d30a4185311546d51bacfdc030ee57671dbae774775d0efd4474220c", // From protocol version 15 the contract is stored compiled (format version 2), its - // document types beside its schemas. - _ => "d14106885e8c88fdc1c26e13cabe4965e79deca70e9cd293071a9523eefb6a50", + // document types beside its schemas, and is DashPay v3, which declares the checks of + // its data triggers. + _ => "d36453bbc0d13eebcabc74544f6b3a3e59cbaf8c2a261d6a0679b15f7870435b", }; assert_eq!( 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 be36896eedc..b738adf7ec6 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 @@ -15,6 +15,9 @@ use super::{ /// - `create_domain_data_trigger` 2, which keeps only the parent domain checks of a DPNS domain /// create: the DPNS contract v3 (`SYSTEM_DATA_CONTRACT_VERSIONS_V4`) checks the normalized /// names, the preorder and the identity record with schema keywords. +/// - the data trigger `bindings` 3: v2 without the DashPay bindings, the contact request create +/// and the profile create and replace triggers. The DashPay contract v3 +/// (`SYSTEM_DATA_CONTRACT_VERSIONS_V4`) declares their checks as schema keywords. /// - the batch `advanced_structure` 2: v1, then the structure checks of token actions protocol /// v15 introduces, which read only the contract the action carries and so run in check_tx too. /// A `TokenConfigUpdate` can make a token tradeable only if it is transferable. @@ -63,6 +66,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = document_reference_validation: 1, // a consumed commitment keeps what its lookup read document_create_transition_structure_validation: 2, // a decision proposal states its fund, a rule within the limits and an end date one to four weeks away data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { + bindings: 3, // no DashPay bindings: DashPay v3 declares their checks triggers: DriveAbciValidationDataTriggerVersions { create_domain_data_trigger: 2, ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs index 8035dfe6e7f..e059545cc0b 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs @@ -8,6 +8,13 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // no longer be deleted. v3 (dpns: 2) remains for PROTOCOL_VERSION_14 chain // replay. // +// The DashPay contract v3 states the checks of its two data triggers as schema +// keywords: a contact request's `toUserId` refers to an identity other than the +// writer, its two ECDH fields declare their encryption (`encryptedFor`, so each +// is an IV plus whole AES blocks), and a profile's payment addresses start with a +// supported type byte. Every property is stored as v2 stores it. v3 (dashpay: 2) +// remains for PROTOCOL_VERSION_14 chain replay. +// // The decision proposals contract (decision_proposals: 1) is registered at // genesis from protocol version 15 and written to state by the upgrade to it: // creating a proposal opens a yes/no masternode vote poll on it. @@ -15,7 +22,7 @@ pub const SYSTEM_DATA_CONTRACT_VERSIONS_V4: SystemDataContractVersions = SystemDataContractVersions { withdrawals: 2, dpns: 3, - dashpay: 2, + dashpay: 3, masternode_reward_shares: 1, wallet: 1, token_history: 2, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 0cd50a1ba8f..58d98810cf6 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -282,13 +282,35 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// it from protocol version 12 on (meta-schemas v1 to v3 close the doctype level), and no /// stored contract carries it (the protocol version 12 migration stripped unknown /// doctype-level keys from the contracts admitted under meta-schema v0). +/// +/// 14. **DashPay v3 states what its data triggers checked**: +/// `SYSTEM_DATA_CONTRACT_VERSIONS_V4` selects DashPay contract v3, loaded at +/// genesis and re-stored by `transition_to_version_15` (`apply_contract`, as the +/// upgrade to v2 did). A `contactRequest`'s `toUserId` refers to an identity +/// (`refersTo`, a missing one is `ReferencedEntityNotFoundError`, 40120) other than +/// the writer (`distinctFrom` `$ownerId`, `DocumentPropertyNotDistinctError`, +/// 10419); `encryptedPublicKey` and `encryptedAccountLabel` declare +/// `ecdh-secp256k1-aes256-cbc` between the `senderKeyIndex` and `recipientKeyIndex` +/// keys (`encryptedFor`), so each must be an IV plus whole AES blocks +/// (`InvalidEncryptedPropertyShapeError`, 10420), and both key indexes gain the +/// u32 `maximum` that requires. A `profile`'s rules `corePaymentAddressType` and +/// `platformPaymentAddressType` hold the first byte of each DIP-33 payment +/// address to 0x00 or 0x01 (`byteAt`, `DocumentPropertyConstraintViolatedError`, +/// 10422). The three profile address descriptions move to a code comment. Every +/// property is stored as v2 stores it (DashPay keeps sized integer types off, so +/// the key indexes stay i64), so documents written under v1 and v2 read back +/// unchanged. `DRIVE_ABCI_VALIDATION_VERSIONS_V11` selects the data trigger +/// `bindings` 3, v2 without the DashPay contact request create and profile create +/// and replace triggers, which made the same checks and refused with +/// `DataTriggerConditionError` (40500). Protocol v14 and earlier keep DashPay v2 +/// and bindings 2. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify (note 2); create_domain_data_trigger 2 (note 3); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 10); document_reference_validation 1 keeps what a consumed commitment's lookup read (note 11); document create structure_validation 2 holds a decision proposal to its fund, rule and end date (note 12) + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify (note 2); create_domain_data_trigger 2 (note 3); data trigger bindings 3 drop the DashPay bindings (note 14); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 10); document_reference_validation 1 keeps what a consumed commitment's lookup read (note 11); document create structure_validation 2 holds a decision proposal to its fund, rule and end date (note 12) withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, @@ -309,7 +331,7 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { methods: DPP_METHOD_VERSIONS_V3, factory_versions: DPP_FACTORY_VERSIONS_V1, }, - system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V4, // changed: DPNS v3 checks a name's create with keywords (note 3); the decision proposals contract (note 12) + system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V4, // changed: DPNS v3 checks a name's create with keywords (note 3); the decision proposals contract (note 12); DashPay v3 states what its data triggers checked (note 14) fee_version: FEE_VERSION4, // changed: decision proposals prefund their poll with 0.5 Dash (note 12) system_limits: SYSTEM_LIMITS_V5, // changed: max_compiled_contract_size caps a contract stored compiled at 1 MiB (note 8); max_private_document_entry_size caps a private type's entries at 4096 bytes (note 13) consensus: ConsensusVersions { From 8b16360fa7b5305aba85fc418b2f231c7be5f44a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 31 Oct 2026 12:24:45 +0300 Subject: [PATCH 4/8] fix(platform): drop the needless struct update in the protocol version 15 data trigger versions Both fields of the data trigger and binding versions are set, so clippy's needless_update refused the spread. Co-Authored-By: Claude Opus 5.5 --- .../drive_abci_versions/drive_abci_validation_versions/v11.rs | 4 ---- 1 file changed, 4 deletions(-) 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 b738adf7ec6..419b4606bba 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 @@ -75,10 +75,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = .data_triggers .triggers }, - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .batch_state_transition - .data_triggers }, ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 .state_transitions From ecf47d0050361b60423971e55d0a3b0c62e0105f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 31 Oct 2026 13:33:15 +0300 Subject: [PATCH 5/8] docs(platform): name the table that keeps DashPay v2 for protocol version 14 Co-Authored-By: Claude Opus 5.5 --- .../src/version/system_data_contract_versions/v4.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs b/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs index e059545cc0b..89a792583ae 100644 --- a/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs +++ b/packages/rs-platform-version/src/version/system_data_contract_versions/v4.rs @@ -12,8 +12,9 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // keywords: a contact request's `toUserId` refers to an identity other than the // writer, its two ECDH fields declare their encryption (`encryptedFor`, so each // is an IV plus whole AES blocks), and a profile's payment addresses start with a -// supported type byte. Every property is stored as v2 stores it. v3 (dashpay: 2) -// remains for PROTOCOL_VERSION_14 chain replay. +// supported type byte. Every property is stored as DashPay v2 stores it. +// `SYSTEM_DATA_CONTRACT_VERSIONS_V3` (dashpay: 2) remains for PROTOCOL_VERSION_14 +// chain replay. // // The decision proposals contract (decision_proposals: 1) is registered at // genesis from protocol version 15 and written to state by the upgrade to it: From 3489faf41e3e770bb699986c45edeb9c8287c6ff Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 1 Nov 2026 10:35:21 +0300 Subject: [PATCH 6/8] feat(platform)!: keep the DashPay v3 profile address descriptions in the schema Schema consumers (SDKs, explorers, the contract viewer) read the address formats from the contract, not from a code comment. The descriptions now name the type byte rules instead of the data trigger. Co-Authored-By: Claude Opus 5.5 --- .../schema/v3/dashpay.schema.json | 3 +++ packages/dashpay-contract/src/v3/mod.rs | 15 ++------------- packages/rs-platform-version/src/version/v15.rs | 8 ++++---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/dashpay-contract/schema/v3/dashpay.schema.json b/packages/dashpay-contract/schema/v3/dashpay.schema.json index bede6943639..433cd3756e5 100644 --- a/packages/dashpay-contract/schema/v3/dashpay.schema.json +++ b/packages/dashpay-contract/schema/v3/dashpay.schema.json @@ -64,6 +64,7 @@ "byteArray": true, "minItems": 21, "maxItems": 21, + "description": "Core chain address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The corePaymentAddressType rule holds the type byte; clients render the address as Base58Check for the network they are on. Payments to it are publicly linkable to this profile.", "position": 5 }, "platformPaymentAddress": { @@ -71,6 +72,7 @@ "byteArray": true, "minItems": 21, "maxItems": 21, + "description": "Platform address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by the 20-byte HASH160, i.e. RIPEMD160 of SHA256, of the public key or redeem script) for public payments. The platformPaymentAddressType rule holds the type byte.", "position": 6 }, "shieldedAddress": { @@ -78,6 +80,7 @@ "byteArray": true, "minItems": 43, "maxItems": 43, + "description": "Raw Orchard receiving address: 11-byte diversifier followed by 32-byte diversified transmission key. Clients validate before payment; wallets should use a dedicated tip account.", "position": 7 } }, diff --git a/packages/dashpay-contract/src/v3/mod.rs b/packages/dashpay-contract/src/v3/mod.rs index 3d99beaa4c1..628e3bac60e 100644 --- a/packages/dashpay-contract/src/v3/mod.rs +++ b/packages/dashpay-contract/src/v3/mod.rs @@ -16,19 +16,8 @@ use serde_json::Value; // types off, so they are still stored as i64. // - `profile`'s `corePaymentAddressType` and `platformPaymentAddressType` rules // hold the first byte of each DIP-33 payment address to the address type, the -// check the profile data trigger made up to protocol version 14. -// -// The DIP-33 profile address formats, kept here rather than as schema -// descriptions, which are stored and billed with the contract: -// -// - `corePaymentAddress` and `platformPaymentAddress`: the storage form of an -// address, a type byte (0x00 P2PKH, 0x01 P2SH) followed by the 20-byte HASH160 -// (RIPEMD160 of SHA256) of the public key or redeem script. Clients render a -// Core address as Base58Check for the network they are on. Payments to either -// are publicly linkable to the profile. -// - `shieldedAddress`: a raw Orchard receiving address, the 11-byte diversifier -// followed by the 32-byte diversified transmission key. Clients validate it -// before paying; wallets should use a dedicated tip account. +// check the profile data trigger made up to protocol version 14. Their descriptions say +// so in place of v2's mention of the data trigger. pub fn load_documents_schemas() -> Result { serde_json::from_str(include_str!("../../schema/v3/dashpay.schema.json")) diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 58d98810cf6..732d1ff9f54 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -296,10 +296,10 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// u32 `maximum` that requires. A `profile`'s rules `corePaymentAddressType` and /// `platformPaymentAddressType` hold the first byte of each DIP-33 payment /// address to 0x00 or 0x01 (`byteAt`, `DocumentPropertyConstraintViolatedError`, -/// 10422). The three profile address descriptions move to a code comment. Every -/// property is stored as v2 stores it (DashPay keeps sized integer types off, so -/// the key indexes stay i64), so documents written under v1 and v2 read back -/// unchanged. `DRIVE_ABCI_VALIDATION_VERSIONS_V11` selects the data trigger +/// 10422), and the two address descriptions name the rules instead of the data +/// trigger. Every property is stored as v2 stores it (DashPay keeps sized integer +/// types off, so the key indexes stay i64), so documents written under v1 and v2 +/// read back unchanged. `DRIVE_ABCI_VALIDATION_VERSIONS_V11` selects the data trigger /// `bindings` 3, v2 without the DashPay contact request create and profile create /// and replace triggers, which made the same checks and refused with /// `DataTriggerConditionError` (40500). Protocol v14 and earlier keep DashPay v2 From 29e2ad6c99a5fcb21a10b7ca69cd7d08c0a77f33 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 1 Nov 2026 10:57:20 +0300 Subject: [PATCH 7/8] test(platform): re-pin the latest-version fees and roots after the merge and the restored descriptions The DashPay contract fees in check_tx, the protocol version 15 root hash and the nullifier batch guard's settled roots move with both the base's repeated-key schema billing and the DashPay v3 address descriptions. Co-Authored-By: Claude Opus 5.5 --- .../src/execution/check_tx/v0/mod.rs | 14 ++++++------- .../token/shielded/nullifier_batch_guard.rs | 20 +++++++++---------- .../rs-drive/tests/deterministic_root_hash.rs | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index 04e29431c2b..6a153311fe0 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -738,7 +738,7 @@ mod tests { // charged per compiled byte // and a document type's schema size is billed at least the values the // repeated-key walk read; the DashPay v3 schema adds its declarations - 24004512230 + 24004855740 ); let check_result = platform @@ -1095,7 +1095,7 @@ mod tests { // From protocol version 15 a document type's schema size is billed at least the // values the repeated-key walk read // and DashPay v3's declarations add to the schema the validation is billed for - assert_eq!(processing_result.aggregated_fees().processing_fee, 346810); + assert_eq!(processing_result.aggregated_fees().processing_fee, 346830); let check_result = platform .check_tx( @@ -1413,7 +1413,7 @@ mod tests { // charged per compiled byte // and a document type's schema size is billed at least the values the // repeated-key walk read; the DashPay v3 schema adds its declarations - 24009024460 + 24009711480 ); let check_result = platform @@ -1899,7 +1899,7 @@ mod tests { // charged per compiled byte // and a document type's schema size is billed at least the values the // repeated-key walk read; the DashPay v3 schema adds its declarations - 24004512230 + 24004855740 ); platform @@ -1998,7 +1998,7 @@ mod tests { // is charged per compiled byte // and a document type's schema size is billed at least the values the // repeated-key walk read; the DashPay v3 schema adds its declarations - 29005254750 + 29005647760 ); let check_result = platform @@ -2594,7 +2594,7 @@ mod tests { // charged per compiled byte // and a document type's schema size is billed at least the values the // repeated-key walk read; the DashPay v3 schema adds its declarations - 24004512230 + 24004855740 ); platform @@ -2720,7 +2720,7 @@ mod tests { // From protocol version 15 a document type's schema size is billed at least the // values the repeated-key walk read // and DashPay v3's declarations add to the schema the validation is billed for - assert_eq!(processing_result.aggregated_fees().processing_fee, 448790); + assert_eq!(processing_result.aggregated_fees().processing_fee, 448810); let check_result = platform .check_tx( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs index 8578857ae32..2c17e8f66a4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs @@ -734,7 +734,7 @@ async fn should_preserve_single_entry_full_processor_fees_and_result() { .value .unwrap() ), - "40d65df3163982165992d950fe8480907ec4348d921aa881016ff4b8113bbf72" + "fea6a8c54f7952ce5da787fe36c6c430be2f9f65b35e79bf5ba89ab81caf1c20" ); assert_eq!( pool.platform @@ -2183,39 +2183,39 @@ async fn should_preserve_single_entry_actions_errors_operations_and_settled_stat let (expected_validation, expected_settled) = match case { "shield" => ( "6a823c2db94612652926e6b9c1ae7feec9c2547dfd4cfc4e474318da1c16d59c", - "ebe68a4398dda43472c06c11a8a9f9508e117da94975c79f2b19374f01b5d8ac", + "2f26137497e6e1cb150543cfc281b4ba7f1c3d9b6277df2dd0a8953877217619", ), "valid-payment" => ( "9388d5cf63c4a571b9fb009679e2e34d0b681acc88f0f5195e7287c7efbd92f7", - "959ccac8b8e2300b8435ab8ffa8ca8ec5076262a072e62701b3d2c7532fcc098", + "f46c1ba97ceeaeda527b45c2b33d80b13369ba59673fd9e0c30b10ca8e0894aa", ), "failed-payment" => ( "d9b5fe191841bea952494f2a45e3833f925f4d367a1937b2ef1235a1aa01f51e", - "6ecafed30959c5c18872547858a7b2c6f5194b89ca94de5028b8131e80f6d093", + "b6577f3554916056d494e2fb8940e3c83a3f4563c8afa3250ded06ee63cafb66", ), "within-bundle-repeat" => ( "8023e03a697a20f6a78396a05503a4bc583ca8f69e19bd7ee07a6ae0ce7be478", - "32636790c508f5f1f6e20e90f2aec27c3063ace09cf977397e725e91beb785f2", + "9e3e23b04e8d2ce7852bc7cd12a03f5a068d11a90d1bc61f2e97b4b94a1b0a51", ), "stored-repeat" => ( "05652ac85c972902e8c09d20a7d8586e17e9434a478b335da136ad85f73def01", - "333b1c1cae8ba67e345be84be74339e316b7b06475604787cc54a7724fc62487", + "2a8ad984e1bbf73f3efc1fd5fb503b162833f3f22be0ed79e48a3c5ccc1d4fdc", ), "open-mint-group" => ( "5c68381297c9bf68c5e85ebe6bb9e0f1289e2c07718eecfb5e6773d6009455bb", - "fff8c4edec55b194dec5bf05544cd3916cf072bb72beec1e4cd2f03d46b1e38c", + "5c689d3fc2ac12f62113125f83db26a3327d9a443a90f890e9e01a20131938ec", ), "closing-mint-group" => ( "2cedc16732c76e0605b5f46085db3d69c12936a5af55a2a82f52ec9e9ca67e38", - "38a9de7c5593318c29dcc221da2ecd0fbbb233f0bd91d2f0a0f59e99b535eb07", + "16d284339c0e90f3e7a6b459ce841415d4a8da6a05404f16b558f26da9b563c0", ), "open-burn-group" => ( "e664a045f020ff84de1ca98f364fbabccfc141ad46aa82c79491a340bb0db5f8", - "8823858db6d9b02226ab6150dfd0012ffc0e8854526d8407b1a114e073f358bf", + "582099c3e3696ae2eaae2d5044bd4a69406af07da6ae06f40741717d5308bd8f", ), "closing-burn-group" => ( "93e1732286f168c4a91d2765931e181c5721bfca1bfd5f93b8baacca03e00b2b", - "01d445fc62c77f1a5f2964239f92394cf3f51097a749c13c7626c88a505d03b9", + "c7a50c13b917c5f58a16d60e3d649fc1748d198d57a1503e22fadd8e459423af", ), _ => unreachable!("enumerated characterization case"), }; diff --git a/packages/rs-drive/tests/deterministic_root_hash.rs b/packages/rs-drive/tests/deterministic_root_hash.rs index ad10f4caee3..80e3c346ffe 100644 --- a/packages/rs-drive/tests/deterministic_root_hash.rs +++ b/packages/rs-drive/tests/deterministic_root_hash.rs @@ -314,7 +314,7 @@ mod tests { // From protocol version 15 the contract is stored compiled (format version 2), its // document types beside its schemas, and is DashPay v3, which declares the checks of // its data triggers. - _ => "d36453bbc0d13eebcabc74544f6b3a3e59cbaf8c2a261d6a0679b15f7870435b", + _ => "ba8d5ff45dad759997166e63772ff7c582faf9b28c0a5d4d51b970b90d4ca9bd", }; assert_eq!( From 025287286cbca979a54a2888904761cdbef3d6d6 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 1 Nov 2026 11:16:30 +0300 Subject: [PATCH 8/8] docs(platform): say the DashPay v3 ciphertext shape check is new, and which versions keep v2 The v15 note now says the encryptedFor shape check is new at protocol version 15 (14 accepts any 48 to 80 byte account label), that the triggers made the other checks, and that only protocol version 14 keeps DashPay v2 and bindings 2. The shielded failing-trigger test names a parent no domain holds instead of the genesis `dash`. Co-Authored-By: Claude Opus 5.5 --- .../tests/token/shielded/nullifier_batch_guard.rs | 4 ++-- packages/rs-platform-version/src/version/v15.rs | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs index 2c17e8f66a4..f2948b7b90c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/nullifier_batch_guard.rs @@ -1640,11 +1640,11 @@ async fn should_reject_a_duplicate_payment_before_its_failing_trigger_without_ch version, ) .unwrap(); - // a name under `dash`, which this contract does not hold + // a name under a parent no domain holds, so the lookup finds nothing document.set("normalizedLabel", Value::Text("alice".to_string())); document.set( "normalizedParentDomainName", - Value::Text("dash".to_string()), + Value::Text("nowhere".to_string()), ); document.set( "subdomainRules", diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 4aa7abc323a..8a64bea543e 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -288,7 +288,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// stored contract carries it (the protocol version 12 migration stripped unknown /// doctype-level keys from the contracts admitted under meta-schema v0). /// -/// 14. **DashPay v3 states what its data triggers checked**: +/// 14. **DashPay v3 states what its data triggers checked, and its ciphertexts' shape**: /// `SYSTEM_DATA_CONTRACT_VERSIONS_V4` selects DashPay contract v3, loaded at /// genesis and re-stored by `transition_to_version_15` (`apply_contract`, as the /// upgrade to v2 did). A `contactRequest`'s `toUserId` refers to an identity @@ -297,8 +297,9 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// 10419); `encryptedPublicKey` and `encryptedAccountLabel` declare /// `ecdh-secp256k1-aes256-cbc` between the `senderKeyIndex` and `recipientKeyIndex` /// keys (`encryptedFor`), so each must be an IV plus whole AES blocks -/// (`InvalidEncryptedPropertyShapeError`, 10420), and both key indexes gain the -/// u32 `maximum` that requires. A `profile`'s rules `corePaymentAddressType` and +/// (`InvalidEncryptedPropertyShapeError`, 10420), a check new at this version +/// (protocol v14 accepts any account label of 48 to 80 bytes), and both key indexes +/// gain the u32 `maximum` that requires. A `profile`'s rules `corePaymentAddressType` and /// `platformPaymentAddressType` hold the first byte of each DIP-33 payment /// address to 0x00 or 0x01 (`byteAt`, `DocumentPropertyConstraintViolatedError`, /// 10422), and the two address descriptions name the rules instead of the data @@ -306,9 +307,9 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// types off, so the key indexes stay i64), so documents written under v1 and v2 /// read back unchanged. `DRIVE_ABCI_VALIDATION_VERSIONS_V11` selects the data trigger /// `bindings` 3, v2 without the DashPay contact request create and profile create -/// and replace triggers, which made the same checks and refused with -/// `DataTriggerConditionError` (40500). Protocol v14 and earlier keep DashPay v2 -/// and bindings 2. +/// and replace triggers, which made the identity, writer and type byte checks and +/// refused with `DataTriggerConditionError` (40500). Protocol v14 keeps DashPay v2 +/// and bindings 2, and earlier versions the contract and bindings they had. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11)