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..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 @@ -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(); @@ -1252,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; } @@ -1263,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; } 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.