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..433cd3756e5 --- /dev/null +++ b/packages/dashpay-contract/schema/v3/dashpay.schema.json @@ -0,0 +1,301 @@ +{ + "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, + "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": { + "type": "array", + "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": { + "type": "array", + "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 + } + }, + "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..628e3bac60e --- /dev/null +++ b/packages/dashpay-contract/src/v3/mod.rs @@ -0,0 +1,25 @@ +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. 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")) + .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 2360eb394b6..3c5a3d7ce94 100644 --- a/packages/rs-dpp/src/system_data_contracts.rs +++ b/packages/rs-dpp/src/system_data_contracts.rs @@ -1470,6 +1470,139 @@ 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" + ); + } + } +} + #[cfg(all(test, feature = "token-market-contract", feature = "validation"))] mod token_market_tests { use super::*; 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 717cd4fd64a..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 @@ -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, }; @@ -736,8 +737,8 @@ mod tests { // 1.8 times its bytes, which are byte-billed, and checking its compiled size is // charged per compiled byte // and a document type's schema size is billed at least the values the - // repeated-key walk read - 24004476710 + // repeated-key walk read; the DashPay v3 schema adds its declarations + 24004855740 ); let check_result = platform @@ -1093,7 +1094,8 @@ mod tests { // From protocol version 15 a document type's schema size is billed at least the // values the repeated-key walk read - assert_eq!(processing_result.aggregated_fees().processing_fee, 346680); + // and DashPay v3's declarations add to the schema the validation is billed for + assert_eq!(processing_result.aggregated_fees().processing_fee, 346830); let check_result = platform .check_tx( @@ -1410,8 +1412,8 @@ mod tests { // 1.8 times its bytes, which are byte-billed, and checking its compiled size is // charged per compiled byte // and a document type's schema size is billed at least the values the - // repeated-key walk read - 24008953420 + // repeated-key walk read; the DashPay v3 schema adds its declarations + 24009711480 ); let check_result = platform @@ -1495,8 +1497,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(); @@ -1896,8 +1898,8 @@ mod tests { // 1.8 times its bytes, which are byte-billed, and checking its compiled size is // charged per compiled byte // and a document type's schema size is billed at least the values the - // repeated-key walk read - 24004476710 + // repeated-key walk read; the DashPay v3 schema adds its declarations + 24004855740 ); platform @@ -1995,8 +1997,8 @@ mod tests { // about 1.8 times its bytes, which are byte-billed, and checking its compiled size // is charged per compiled byte // and a document type's schema size is billed at least the values the - // repeated-key walk read - 29005214590 + // repeated-key walk read; the DashPay v3 schema adds its declarations + 29005647760 ); let check_result = platform @@ -2591,8 +2593,8 @@ mod tests { // 1.8 times its bytes, which are byte-billed, and checking its compiled size is // charged per compiled byte // and a document type's schema size is billed at least the values the - // repeated-key walk read - 24004476710 + // repeated-key walk read; the DashPay v3 schema adds its declarations + 24004855740 ); platform @@ -2717,7 +2719,8 @@ mod tests { // From protocol version 15 a document type's schema size is billed at least the // values the repeated-key walk read - assert_eq!(processing_result.aggregated_fees().processing_fee, 448660); + // and DashPay v3's declarations add to the schema the validation is billed for + assert_eq!(processing_result.aggregated_fees().processing_fee, 448810); let check_result = platform .check_tx( @@ -2870,6 +2873,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 d9df3275bc0..d7dfd378053 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 @@ -914,9 +914,11 @@ 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, - /// insert the decision proposals contract whose proposals open those polls, and add - /// the token marketplace: its escrow trees and the token market contract. + /// 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, insert the decision proposals contract whose proposals open those + /// polls, and add the token marketplace: its escrow trees and the token market + /// contract. fn transition_to_version_15( &self, block_info: &BlockInfo, @@ -936,6 +938,22 @@ impl Platform { platform_version, )?; + // DashPay contract v3 (v15 note 24): 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 @@ -2397,6 +2415,302 @@ 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, DocumentTypeV2Getters, + }; + use dpp::data_contract::document_type::random_document::{ + CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, + }; + use dpp::data_contract::document_type::{ + DistinctFrom, 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; + use std::collections::BTreeMap; + + 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" + ); + + // The other declarations that replace the triggers survive the store too: the + // stored contract holds what DashPay v3 loads with, whose values the dpp + // `dashpay_v3_tests` pin. + let dashpay_v3_loaded = + load_system_data_contract(SystemDataContract::Dashpay, platform_version_15) + .expect("expected to load DashPay v3"); + let contact_request = dashpay_v3 + .document_type_for_name("contactRequest") + .expect("expected the contactRequest document type"); + assert_eq!( + contact_request + .flattened_properties() + .get("toUserId") + .expect("expected toUserId") + .distinct_from, + Some(DistinctFrom::OwnerId), + "the stored DashPay v3 keeps the contact request's distinctFrom" + ); + let encrypted = contact_request + .encrypted_properties() + .into_iter() + .collect::>(); + assert_eq!( + encrypted + .keys() + .map(|path| path.as_str()) + .collect::>(), + vec!["encryptedAccountLabel", "encryptedPublicKey"], + "the stored DashPay v3 keeps both encryptedFor declarations" + ); + assert_eq!( + encrypted, + dashpay_v3_loaded + .document_type_for_name("contactRequest") + .expect("expected the loaded contactRequest document type") + .encrypted_properties() + .into_iter() + .collect::>(), + "the stored encryptedFor declarations are the ones DashPay v3 loads with" + ); + let profile = dashpay_v3 + .document_type_for_name("profile") + .expect("expected the profile document type"); + let profile_rules = profile.property_constraints(); + assert_eq!( + profile_rules + .keys() + .map(|name| name.as_str()) + .collect::>(), + vec!["corePaymentAddressType", "platformPaymentAddressType"], + "the stored DashPay v3 keeps both byteAt rules" + ); + assert_eq!( + profile_rules, + dashpay_v3_loaded + .document_type_for_name("profile") + .expect("expected the loaded profile document type") + .property_constraints(), + "the stored byteAt rules are the ones DashPay v3 loads with" + ); + + 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/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/list/v3/mod.rs index d4d53e6885e..09b1d669382 100644 --- 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 @@ -3,20 +3,32 @@ use crate::execution::validation::state_transition::batch::data_triggers::bindin use crate::execution::validation::state_transition::batch::data_triggers::triggers::reject::reject_data_trigger; use dpp::errors::ProtocolError; +use dpp::system_data_contracts::dashpay_contract; use dpp::system_data_contracts::token_market_contract; use dpp::system_data_contracts::token_market_contract::v1::document_types::{ask, bid}; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionActionType; /// Retrieves a list of data triggers binding with matching params. /// -/// v3 (PROTOCOL_VERSION_15): v2 plus a reject of every replace and delete of a token market -/// contract order (`ask` and `bid`) sent as a document transition. Only the token marketplace -/// transitions write orders: creating one as a document is refused by -/// `creationRestrictionMode` 2, and the types stay mutable and deletable, so documents carry a -/// revision and storage flags for the partial fills and closes the platform writes. +/// v3 (PROTOCOL_VERSION_15) is v2 with two changes: +/// +/// - No 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. +/// - A reject of every replace and delete of a token market contract order (`ask` and `bid`) +/// sent as a document transition. Only the token marketplace transitions write orders: +/// creating one as a document is refused by `creationRestrictionMode` 2, and the types stay +/// mutable and deletable, so documents carry a revision and storage flags for the partial +/// fills and closes the platform writes. +/// +/// Every other binding is v2's. #[inline(always)] pub(super) fn data_trigger_bindings_list_v3() -> Result, ProtocolError> { - let mut data_triggers = data_trigger_bindings_list_v2()?; + let mut data_triggers: Vec = data_trigger_bindings_list_v2()? + .into_iter() + .filter(|binding| binding.data_contract_id != dashpay_contract::ID) + .collect(); for document_type in [ask::NAME, bid::NAME] { for transition_action_type in [ DocumentTransitionActionType::Replace, @@ -38,10 +50,19 @@ mod tests { use super::*; #[test] - fn should_reject_every_replace_and_delete_of_an_order_and_keep_the_v2_bindings() { + fn should_drop_the_dashpay_bindings_and_reject_every_replace_and_delete_of_an_order() { let v2 = data_trigger_bindings_list_v2().expect("expected the v2 bindings"); let v3 = data_trigger_bindings_list_v3().expect("expected the v3 bindings"); - assert_eq!(v3.len(), v2.len() + 4); + + let v2_dashpay_bindings = v2 + .iter() + .filter(|binding| binding.data_contract_id == dashpay_contract::ID) + .count(); + assert_eq!(v2_dashpay_bindings, 3); + assert!(v3 + .iter() + .all(|binding| binding.data_contract_id != dashpay_contract::ID)); + assert_eq!(v3.len(), v2.len() - v2_dashpay_bindings + 4); let token_market_bindings: Vec<(&str, DocumentTransitionActionType)> = v3 .iter() 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..94f42fc45f7 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dashpay_v3.rs @@ -0,0 +1,429 @@ +//! 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). +//! - A profile without either payment address or both, on create and on replace: +//! accepted by the trigger and by the rules alike. + +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)); + } + } + } + + /// Both payment addresses are optional. The trigger checked only an address that + /// is present; from protocol version 15 a missing one reads as 0 in its rule's + /// `byteAt`, which the rule allows. So a profile without either address or both + /// is created, and a replace can add them and remove them again. + #[tokio::test] + async fn should_accept_a_profile_without_payment_addresses_on_create_and_replace() { + let omissions: [&[&str]; 3] = [ + &["corePaymentAddress"], + &["platformPaymentAddress"], + &["corePaymentAddress", "platformPaymentAddress"], + ]; + for omitted in omissions { + for fixture in [Fixture::with_triggers(), Fixture::latest()] { + let protocol_version = fixture.platform_version.protocol_version; + let remove_omitted = |document: &mut Document| { + for field in omitted { + document + .remove(field) + .unwrap_or_else(|| panic!("expected the profile to have {field}")); + } + }; + + // one profile per identity (the unique `ownerId` index), so the + // replaces revise the one created without the addresses + let (mut document, entropy) = fixture.profile(2); + let with_addresses = document.clone(); + remove_omitted(&mut document); + assert_matches!( + fixture + .create("profile", document.clone(), entropy, 2) + .await, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "a profile created without {omitted:?} at protocol version {protocol_version}" + ); + + for field in omitted { + document.set( + field, + with_addresses + .get(field) + .cloned() + .expect("expected the address"), + ); + } + document.increment_revision().expect("expected a revision"); + assert_matches!( + fixture.replace("profile", document.clone(), 3).await, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "a replace adding {omitted:?} at protocol version {protocol_version}" + ); + + remove_omitted(&mut document); + document.increment_revision().expect("expected a revision"); + assert_matches!( + fixture.replace("profile", document, 4).await, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "a replace removing {omitted:?} at protocol version {protocol_version}" + ); + } + } + } +} 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 cceb7a0d9b5..15648ab7bf9 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 @@ -896,6 +896,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 a3750ecbcdb..1dc65ecf65c 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 6c0f9ef12a2..a19c1477808 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 @@ -1314,6 +1314,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(); @@ -1425,7 +1430,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 d9f0ae58a7c..7f8707cfeef 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() ), - "fb76e051fe936ae3e39265a9cddf3d3297e901ca2c6048c1dcf85b82a92d8778" + "01b1aa765b58c73b580de019d98fdb10106003b0941b738769a96dc0feba8741" ); 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 a parent no domain holds, so the lookup finds nothing + document.set("normalizedLabel", Value::Text("alice".to_string())); + document.set( + "normalizedParentDomainName", + Value::Text("nowhere".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()), @@ -2160,39 +2185,39 @@ async fn should_preserve_single_entry_actions_errors_operations_and_settled_stat let (expected_validation, expected_settled) = match case { "shield" => ( "6a823c2db94612652926e6b9c1ae7feec9c2547dfd4cfc4e474318da1c16d59c", - "eb7e34a02f156dbe1d9c9609b41bd67a033d1cd3fe81c5f2e6fcef575fc88459", + "761e92d28f8cef61e908c2ae3b24938b405868e468f44fd22a68f61da64b682c", ), "valid-payment" => ( "9388d5cf63c4a571b9fb009679e2e34d0b681acc88f0f5195e7287c7efbd92f7", - "44684cf51317f0e60bc836e7bd671798145bb69bc7605241ebbc659028d50070", + "5a8fa6e02357c351ba35b210eaaadb09cb9786d7b12d06b1483e75411c478d3f", ), "failed-payment" => ( "d9b5fe191841bea952494f2a45e3833f925f4d367a1937b2ef1235a1aa01f51e", - "365a97c4e20ae4ee695505a8d5dbb0dea5584c14eb4c6b223a3ae4f0b2f8b668", + "1dee6b00d4f6ab7be85238448840e8a124b75260a445a3ec38b0c2283633544b", ), "within-bundle-repeat" => ( "8023e03a697a20f6a78396a05503a4bc583ca8f69e19bd7ee07a6ae0ce7be478", - "b9cfc7cafbeafb436917f0b35bfa97df6be58b73a25e0159038558fdc9a2751c", + "5557745a60262645a038803fffb5c3e7f5a85b0822d0d81f515b30cfca8a5316", ), "stored-repeat" => ( "05652ac85c972902e8c09d20a7d8586e17e9434a478b335da136ad85f73def01", - "9e1d6b0bc9097d817f4ca0ac952611f69398d8f201b6eb36556059b166488393", + "a8fa01705072cdaddf01155bc21a707da71765cca36e829ed8689bc010721571", ), "open-mint-group" => ( "5c68381297c9bf68c5e85ebe6bb9e0f1289e2c07718eecfb5e6773d6009455bb", - "b96b1e9b643552413c2e5049f9f9b6ae92357a6ad462e2d3a673d239a355cfd9", + "2cc8ac005b08d6bdba5713b81653f43d8db6d62f1f0c64f2e11dc987dd6587be", ), "closing-mint-group" => ( "2cedc16732c76e0605b5f46085db3d69c12936a5af55a2a82f52ec9e9ca67e38", - "78fd3bdabed41d31082162dfc95158872108872c79f3041ef3d71c7fa48dd905", + "cc45783c058f65155e440957bde48a991854eda553fe868586c4688e7e608963", ), "open-burn-group" => ( "e664a045f020ff84de1ca98f364fbabccfc141ad46aa82c79491a340bb0db5f8", - "38b6b6d2d77fb41c4262adaa6bbbd581dbb2c5d3195e3d11cec16411464e2758", + "a15f8c988bf4aac48480c32842c9e0b1d04b6304fbfd427b289dc7dc9fb54b8a", ), "closing-burn-group" => ( "93e1732286f168c4a91d2765931e181c5721bfca1bfd5f93b8baacca03e00b2b", - "6a4f89129b2bd16b078b1165a46775e20deb1808bfd239fd49e5f4e89e45453c", + "d799aab48f5ce1eb60b3076e576b663c982e59ac003b42210565753f68848591", ), _ => 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..80e3c346ffe 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. + _ => "ba8d5ff45dad759997166e63772ff7c582faf9b28c0a5d4d51b970b90d4ca9bd", }; 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 6a029664820..c1190c7adcc 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 @@ -23,9 +23,11 @@ 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, which refuse a document transition's replace or delete of a -/// token market contract order (`ask` and `bid`): only the token marketplace transitions write -/// them. +/// - the data trigger `bindings` 3: v2 without the DashPay bindings, the contact request create +/// and the profile create and replace triggers, as the DashPay contract v3 +/// (`SYSTEM_DATA_CONTRACT_VERSIONS_V4`) declares their checks as schema keywords; and with a +/// refusal of a document transition's replace or delete of a token market contract order +/// (`ask` and `bid`), as only the token marketplace transitions write them. /// - 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. @@ -88,7 +90,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = 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 document_create_transition_state_validation: 3, // a non-contested create cannot take the values of a live contest its index values do not decide data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { - bindings: 3, // the token market contract's orders are written by the marketplace transitions only + bindings: 3, // no DashPay bindings (DashPay v3 declares their checks); the token market contract's orders are written by the marketplace transitions only 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 62e75215a69..d97bdccbae4 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 @@ -10,6 +10,14 @@ use crate::version::system_data_contract_versions::SystemDataContractVersions; // upgrade to 15 and registered at genesis from 15 on. v3 (dpns: 2, no token // market) 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 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: // creating a proposal opens a yes/no masternode vote poll on it. @@ -17,7 +25,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 812d90f1dd4..6a754bfd357 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -115,7 +115,8 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// the parent's subdomain rule. DPNS needs no new data trigger bindings: /// its `domain` Replace and Delete rejects are never reached, as /// `documentsMutable` and `canBeDeleted` false refuse both first (10404). -/// (The bindings move to 3 for the token market contract alone, note 17.) +/// (The bindings move to 3 for the token market contract, note 17, and +/// DashPay, note 24, not for DPNS.) /// /// 4. **`countOf` and `sumOf` match byte array keys**: `CONTRACT_VERSIONS_V7` /// selects `parse_property_constraints` 1, whose grammar binds a filter key @@ -447,6 +448,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// the chain locked height to one where V24 is active, withdrawals stay version 1, which /// Core always accepts. Protocol v14 and earlier keep /// `build_untied_withdrawal_transactions_from_documents` 0, which emits version 1. +/// /// 21. **A contest is keyed by top-level properties**: a contested create keys its vote poll /// by looking each index property up by name among the document's top-level properties /// (`Index::extract_values`), where a property path (`records.identity`) or a system @@ -480,13 +482,36 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// whose structure is invalid is still refused first, for its structure. Such a transition /// is refused unpaid either way. Protocol v14 and earlier keep `basic_structure` 0 and /// `advanced_structure` 0 or 1. +/// +/// 24. **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 +/// (`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), 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 +/// 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. The data trigger `bindings` 3 (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`, +/// which also add note 17's order rejects) drop the DashPay contact request create and +/// profile create 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) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17); fetch_core_fork_activation_height and store_core_fork_activation_height Some(0) keep the Core fork activations (note 18) 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; consensus_params_update 2 installs the full evidence policy (note 16); update_core_info 1 records the chain locked height at which Core's V24 is first seen active (note 18); build_untied_withdrawal_transactions_from_documents 1 emits asset unlock payload version 2 once V24 is recorded as active (note 20); update_voter_identity 1 re-enables the voter key of the address a masternode returns to (note 22) - 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); data trigger bindings 3 refuse a replace or a delete of a token market order (note 17); document create state_validation 3 keeps a create outside a contest off the values of a live one its index values do not decide (note 19); identity create basic_structure 1 checks the identity id before the asset lock proof and advanced_structure 2 no longer does (note 23) + 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); data trigger bindings 3 refuse a replace or a delete of a token market order (note 17) and drop the DashPay bindings (note 24); document create state_validation 3 keeps a create outside a contest off the values of a live one its index values do not decide (note 19); identity create basic_structure 1 checks the identity id before the asset lock proof and advanced_structure 2 no longer does (note 23) withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, @@ -507,7 +532,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); the token market contract is selected (note 17) + 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); the token market contract is selected (note 17); DashPay v3 states what its data triggers checked (note 24) 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, and the private document store limits size its store (note 13) consensus: ConsensusVersions {