Skip to main content

dpp/
system_data_contracts.rs

1use crate::data_contract::DataContractFactory;
2use crate::prelude::*;
3use crate::ProtocolError;
4use std::collections::{BTreeMap, BTreeSet};
5
6use crate::data_contract::accessors::v0::DataContractV0Setters;
7use crate::data_contract::config::v1::DataContractConfigSettersV1;
8use crate::data_contract::config::DataContractConfig;
9pub use data_contracts::*;
10use platform_version::version::PlatformVersion;
11
12pub trait ConfigurationForSystemContract {
13    fn configuration_in_platform_version(
14        &self,
15        version: &PlatformVersion,
16    ) -> Result<DataContractConfig, ProtocolError>;
17}
18
19impl ConfigurationForSystemContract for SystemDataContract {
20    fn configuration_in_platform_version(
21        &self,
22        platform_version: &PlatformVersion,
23    ) -> Result<DataContractConfig, ProtocolError> {
24        match self {
25            SystemDataContract::Withdrawals => {
26                let mut config = DataContractConfig::default_for_version(platform_version)?;
27                config.set_sized_integer_types_enabled(false);
28                Ok(config)
29            }
30            SystemDataContract::MasternodeRewards => {
31                let mut config = DataContractConfig::default_for_version(platform_version)?;
32                config.set_sized_integer_types_enabled(false);
33                Ok(config)
34            }
35            // Reserved slot with no implementation. Any caller that reaches here
36            // has a bug (they should have short-circuited on `source()` returning
37            // `ContractReserved`). Return a harmless default config rather than
38            // panicking so this failure mode stays non-fatal.
39            SystemDataContract::FeatureFlags => {
40                DataContractConfig::default_for_version(platform_version)
41            }
42            SystemDataContract::DPNS => {
43                let mut config = DataContractConfig::default_for_version(platform_version)?;
44                config.set_sized_integer_types_enabled(false);
45                Ok(config)
46            }
47            SystemDataContract::Dashpay => {
48                let mut config = DataContractConfig::default_for_version(platform_version)?;
49                config.set_sized_integer_types_enabled(false);
50                Ok(config)
51            }
52            SystemDataContract::WalletUtils => {
53                let mut config = DataContractConfig::default_for_version(platform_version)?;
54                config.set_sized_integer_types_enabled(false);
55                Ok(config)
56            }
57            SystemDataContract::TokenHistory => {
58                let mut config = DataContractConfig::default_for_version(platform_version)?;
59                config.set_sized_integer_types_enabled(true);
60                Ok(config)
61            }
62            SystemDataContract::KeywordSearch => {
63                let mut config = DataContractConfig::default_for_version(platform_version)?;
64                config.set_sized_integer_types_enabled(true);
65                Ok(config)
66            }
67            SystemDataContract::DocumentHistory => {
68                let mut config = DataContractConfig::default_for_version(platform_version)?;
69                config.set_sized_integer_types_enabled(true);
70                Ok(config)
71            }
72            SystemDataContract::AppConnect => {
73                let mut config = DataContractConfig::default_for_version(platform_version)?;
74                config.set_sized_integer_types_enabled(true);
75                Ok(config)
76            }
77            SystemDataContract::ModerationCharters => {
78                let mut config = DataContractConfig::default_for_version(platform_version)?;
79                config.set_sized_integer_types_enabled(true);
80                Ok(config)
81            }
82        }
83    }
84}
85
86/// Builds a system contract from its source, with full validation, under its published id.
87///
88/// The contract is parsed under the published id rather than one derived from the owner and
89/// a nonce and renamed afterwards: the document types remember the contract id they belong
90/// to, and the checks that compare against it read that one. The moderation charters
91/// contract's key references require keys bound to its own document types (`boundTo`), which
92/// would name the wrong contract under any other id.
93fn create_data_contract(
94    factory: &DataContractFactory,
95    system_contract: SystemDataContract,
96    platform_version: &PlatformVersion,
97) -> Result<DataContract, ProtocolError> {
98    let DataContractSource {
99        id_bytes,
100        owner_id_bytes,
101        version,
102        definitions,
103        document_schemas,
104    } = system_contract
105        .source(platform_version)
106        .map_err(|e| ProtocolError::Generic(e.to_string()))?;
107
108    let mut data_contract = factory.create_with_id(
109        Identifier::from(id_bytes),
110        Identifier::from(owner_id_bytes),
111        0,
112        document_schemas.into(),
113        Some(system_contract.configuration_in_platform_version(platform_version)?),
114        definitions.map(|def| def.into()),
115    )?;
116
117    data_contract.data_contract_mut().set_version(version);
118
119    Ok(data_contract.data_contract_owned())
120}
121
122pub fn load_system_data_contract(
123    system_contract: SystemDataContract,
124    platform_version: &PlatformVersion,
125) -> Result<DataContract, ProtocolError> {
126    let factory = DataContractFactory::new(platform_version.protocol_version)?;
127
128    create_data_contract(&factory, system_contract, platform_version)
129}
130
131pub fn load_system_data_contracts(
132    system_contracts: BTreeSet<SystemDataContract>,
133    platform_version: &PlatformVersion,
134) -> Result<BTreeMap<SystemDataContract, DataContract>, ProtocolError> {
135    let factory = DataContractFactory::new(platform_version.protocol_version)?;
136
137    system_contracts
138        .into_iter()
139        .map(|system_contract| {
140            let data_contract = create_data_contract(&factory, system_contract, platform_version)?;
141
142            Ok((system_contract, data_contract))
143        })
144        .collect()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::data_contract::serialized_version::DataContractInSerializationFormat;
151    use crate::serialization::PlatformSerializableWithPlatformVersion;
152    use platform_version::TryIntoPlatformVersioned;
153    /// A system contract is written by genesis and protocol upgrades, which nothing refuses
154    /// before Drive does, so each must fit the contract size caps at every protocol version
155    /// that has it: its bytes in the format transitions carry under `max_serialized_size`
156    /// and, where contracts are stored compiled, its compiled bytes under
157    /// `max_compiled_contract_size`.
158    #[cfg(feature = "validation")]
159    #[test]
160    fn should_fit_every_system_contract_under_the_contract_size_caps() {
161        use crate::serialization::PlatformSerializableWithPlatformVersion;
162        use platform_version::version::PLATFORM_VERSIONS;
163
164        for platform_version in PLATFORM_VERSIONS.iter() {
165            let contract_versions = &platform_version.system_data_contracts;
166            for system_contract in SystemDataContract::ALL {
167                // Only the contracts a version has; any of them failing to load fails the test
168                let absent = match system_contract {
169                    // A reserved slot, with no contract at any version
170                    SystemDataContract::FeatureFlags => true,
171                    // Inserted by the upgrade to protocol version 13; earlier meta-schemas
172                    // refuse its schema
173                    SystemDataContract::DocumentHistory => platform_version.protocol_version < 13,
174                    SystemDataContract::AppConnect => contract_versions.app_connect == 0,
175                    SystemDataContract::ModerationCharters => {
176                        contract_versions.moderation_charters == 0
177                    }
178                    SystemDataContract::Withdrawals
179                    | SystemDataContract::MasternodeRewards
180                    | SystemDataContract::DPNS
181                    | SystemDataContract::Dashpay
182                    | SystemDataContract::WalletUtils
183                    | SystemDataContract::TokenHistory
184                    | SystemDataContract::KeywordSearch => false,
185                };
186                if absent {
187                    continue;
188                }
189                let contract = load_system_data_contract(system_contract, platform_version)
190                    .unwrap_or_else(|error| {
191                        panic!(
192                            "{system_contract:?} does not load at protocol version {}: {error}",
193                            platform_version.protocol_version
194                        )
195                    });
196                let origin = format!(
197                    "{system_contract:?} at protocol version {}",
198                    platform_version.protocol_version
199                );
200                let carried_size = contract
201                    .serialize_to_bytes_with_platform_version(platform_version)
202                    .expect("expected the system contract to serialize")
203                    .len();
204                assert!(
205                    carried_size
206                        <= platform_version.dpp.contract_versions.max_serialized_size as usize,
207                    "{origin} serializes to {carried_size} bytes"
208                );
209                let stored_size = contract
210                    .serialize_to_storage_bytes(platform_version)
211                    .expect("expected the system contract to serialize for storage")
212                    .len();
213                if let Some(max_compiled_size) =
214                    platform_version.system_limits.max_compiled_contract_size
215                {
216                    assert!(
217                        stored_size <= max_compiled_size as usize,
218                        "{origin} is stored in {stored_size} bytes"
219                    );
220                }
221            }
222        }
223    }
224
225    #[test]
226    fn test_load_system_data_contract_v8_vs_v9() {
227        let contract_1 = load_system_data_contract(
228            SystemDataContract::TokenHistory,
229            PlatformVersion::get(8).unwrap(),
230        )
231        .expect("data_contract");
232        let contract_2 = load_system_data_contract(
233            SystemDataContract::TokenHistory,
234            PlatformVersion::get(9).unwrap(),
235        )
236        .expect("data_contract");
237        assert_ne!(contract_1, contract_2);
238    }
239
240    #[test]
241    fn serialize_withdrawal_contract_v1_vs_v9() {
242        let contract_1 = load_system_data_contract(
243            SystemDataContract::Withdrawals,
244            PlatformVersion::get(1).unwrap(),
245        )
246        .expect("data_contract");
247        let contract_2 = load_system_data_contract(
248            SystemDataContract::Withdrawals,
249            PlatformVersion::get(9).unwrap(),
250        )
251        .expect("data_contract");
252
253        assert_ne!(contract_1, contract_2);
254        let v1_ser: DataContractInSerializationFormat = contract_1
255            .clone()
256            .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
257            .expect("expected to serialize");
258        let v2_ser: DataContractInSerializationFormat = contract_2
259            .clone()
260            .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
261            .expect("expected to serialize");
262        assert_eq!(v1_ser, v2_ser);
263
264        let v1_bytes = contract_1
265            .serialize_to_bytes_with_platform_version(PlatformVersion::get(1).unwrap())
266            .expect("expected to serialize");
267        let v8_bytes = contract_1
268            .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
269            .expect("expected to serialize");
270        let v9_bytes = contract_1
271            .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
272            .expect("expected to serialize");
273        assert_eq!(v1_bytes.len(), 1747);
274        assert_eq!(v8_bytes.len(), 1747);
275        assert_eq!(v9_bytes.len(), 1757); // this will still use a config v0 without sized_integer_types
276
277        let v1_bytes = contract_2
278            .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
279            .expect("expected to serialize");
280        let v8_bytes = contract_2
281            .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
282            .expect("expected to serialize");
283        let v9_bytes = contract_2
284            .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
285            .expect("expected to serialize");
286        assert_eq!(v1_bytes.len(), 1747);
287        assert_eq!(v8_bytes.len(), 1747);
288        assert_eq!(v9_bytes.len(), 1758); // this will use a config v1 in serialization with sized_integer_types
289    }
290}
291
292#[cfg(all(test, feature = "app-connect-contract", feature = "validation"))]
293mod app_connect_tests {
294    use super::*;
295    use crate::data_contract::accessors::v0::DataContractV0Getters;
296    use crate::data_contract::document_type::random_document::CreateRandomDocument;
297    use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
298    use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
299    use platform_value::Value;
300
301    fn response(contract: &DataContract) -> Document {
302        let mut document = contract
303            .document_type_for_name("loginKeyResponse")
304            .expect("response type")
305            .random_document(Some(42), PlatformVersion::latest())
306            .expect("response document");
307        document.set_properties(BTreeMap::from([
308            (
309                "appEphemeralPubKeyHash".into(),
310                Value::Bytes(vec![0x11; 20]),
311            ),
312            ("walletEphemeralPubKey".into(), Value::Bytes(vec![0x22; 33])),
313            ("encryptedPayload".into(), Value::Bytes(vec![0x33; 60])),
314        ]));
315        document
316    }
317
318    #[test]
319    fn should_validate_app_connect_response_lengths() {
320        let platform_version = PlatformVersion::latest();
321        let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
322            .expect("system contract");
323        for (property, length, expected) in [
324            ("appEphemeralPubKeyHash", 19, false),
325            ("appEphemeralPubKeyHash", 20, true),
326            ("appEphemeralPubKeyHash", 21, false),
327            ("walletEphemeralPubKey", 32, false),
328            ("walletEphemeralPubKey", 33, true),
329            ("walletEphemeralPubKey", 34, false),
330            ("encryptedPayload", 59, false),
331            ("encryptedPayload", 60, true),
332            ("encryptedPayload", 572, true),
333            ("encryptedPayload", 573, false),
334        ] {
335            let mut document = response(&contract);
336            document.set(property, Value::Bytes(vec![0x44; length]));
337            let result = contract
338                .validate_document("loginKeyResponse", &document, platform_version)
339                .expect("validation executes");
340            assert_eq!(
341                result.is_valid(),
342                expected,
343                "{property}: {length} bytes: {result:?}"
344            );
345        }
346    }
347
348    #[test]
349    fn should_require_only_the_three_app_connect_response_properties() {
350        let platform_version = PlatformVersion::latest();
351        let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
352            .expect("system contract");
353        assert_eq!(contract.document_types().len(), 1);
354        for property in [
355            "appEphemeralPubKeyHash",
356            "walletEphemeralPubKey",
357            "encryptedPayload",
358        ] {
359            let mut document = response(&contract);
360            document.properties_mut().remove(property);
361            assert!(
362                !contract
363                    .validate_document("loginKeyResponse", &document, platform_version)
364                    .expect("validation executes")
365                    .is_valid(),
366                "{property} is required"
367            );
368        }
369        let mut document = response(&contract);
370        document.set("contractId", Value::Bytes(vec![0x55; 32]));
371        assert!(
372            !contract
373                .validate_document("loginKeyResponse", &document, platform_version)
374                .expect("validation executes")
375                .is_valid(),
376            "the former contractId field is not admitted"
377        );
378    }
379}
380
381#[cfg(all(test, feature = "moderation-charters-contract", feature = "validation"))]
382mod moderation_charters_tests {
383    use super::*;
384    use crate::consensus::basic::document::PropertyConstraintViolation;
385    use crate::consensus::basic::BasicError;
386    use crate::consensus::ConsensusError;
387    use crate::data_contract::accessors::v0::DataContractV0Getters;
388    use crate::data_contract::document_type::accessors::{
389        DocumentTypeV0Getters, DocumentTypeV2Getters,
390    };
391    use crate::data_contract::document_type::random_document::CreateRandomDocument;
392    use crate::data_contract::document_type::{
393        ContestedIndexResolution, ContractReferenceModeration, DistinctFrom,
394        DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentType, EncryptedForRecipient,
395        EncryptionScheme, KeyReferenceIdentityProperty, LookupKeySource, PropertyReference,
396        StringPropertySizes,
397    };
398    use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
399    use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
400    use crate::identity::Purpose;
401    use crate::moderation_charter::{
402        property_names, ElectedCharter, ModerationCharterRewardSplit, SubmittedCharter,
403        ADDED_MODERATOR_DOCUMENT_TYPE_NAME, ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
404        JOIN_REQUEST_DOCUMENT_TYPE_NAME, MODERATION_CHARTERS_CONTRACT_ID,
405        REASON_DOCUMENT_TYPE_NAME, REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
406        RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
407    };
408    use platform_value::{Identifier, Value};
409
410    fn contract() -> DataContract {
411        load_system_data_contract(
412            SystemDataContract::ModerationCharters,
413            PlatformVersion::latest(),
414        )
415        .expect("the moderation charters contract loads")
416    }
417
418    fn document_type<'a>(contract: &'a DataContract, name: &str) -> &'a DocumentType {
419        contract
420            .document_types()
421            .get(name)
422            .unwrap_or_else(|| panic!("the {name} type"))
423    }
424
425    fn reference<'a>(
426        contract: &'a DataContract,
427        type_name: &str,
428        property: &str,
429    ) -> PropertyReference<'a> {
430        document_type(contract, type_name)
431            .flattened_properties()
432            .get(property)
433            .unwrap_or_else(|| panic!("{type_name}.{property}"))
434            .property_type
435            .reference()
436            .unwrap_or_else(|| panic!("{type_name}.{property} declares a reference"))
437    }
438
439    fn proposal() -> SubmittedCharter {
440        SubmittedCharter {
441            target_contract_id: Identifier::from([9u8; 32]),
442            description: "We remove spam and doxing within a day.".to_string(),
443            reasons: vec![Identifier::from([3u8; 32]), Identifier::from([4u8; 32])],
444            moderators_share: None,
445            reward_split: ModerationCharterRewardSplit {
446                leader: 10,
447                equal: 40,
448                actions: 50,
449            },
450        }
451    }
452
453    fn document_with(
454        contract: &DataContract,
455        type_name: &str,
456        properties: std::collections::BTreeMap<String, Value>,
457    ) -> Document {
458        let mut document = document_type(contract, type_name)
459            .random_document(Some(42), PlatformVersion::latest())
460            .expect("a random document");
461        document.set_properties(properties);
462        document
463    }
464
465    fn schema_validation(
466        contract: &DataContract,
467        type_name: &str,
468        document: &Document,
469    ) -> Vec<ConsensusError> {
470        contract
471            .validate_document(type_name, document, PlatformVersion::latest())
472            .expect("validation executes")
473            .errors
474    }
475
476    /// The members a charter elects bound how many approvals a `deleteSettled` rule may ask
477    /// for, through `SystemLimits::max_moderation_charter_elected_members`: the limit and the
478    /// schema must agree.
479    #[test]
480    fn should_elect_as_many_members_as_the_system_limit_says() {
481        let contract = contract();
482        let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
483            .schema()
484            .get_optional_value("properties")
485            .ok()
486            .flatten()
487            .and_then(|properties| properties.get_optional_value("members").ok().flatten())
488            .and_then(|members| {
489                members
490                    .get_optional_integer::<u16>("maxItems")
491                    .ok()
492                    .flatten()
493            })
494            .expect("electedCharter.members declares maxItems");
495        assert_eq!(
496            members,
497            PlatformVersion::latest()
498                .system_limits
499                .max_moderation_charter_elected_members
500        );
501    }
502
503    #[test]
504    fn should_spell_the_same_id_in_the_crate_and_in_dpp() {
505        assert_eq!(
506            moderation_charters_contract::ID,
507            MODERATION_CHARTERS_CONTRACT_ID
508        );
509        assert_eq!(contract().id(), MODERATION_CHARTERS_CONTRACT_ID);
510    }
511
512    /// The document types remember the contract id they were parsed under, and the key bound
513    /// and lookup checks compare against it, so the contract has to be built under its
514    /// published id rather than renamed afterwards.
515    #[test]
516    fn should_parse_every_document_type_under_the_published_id() {
517        let contract = contract();
518        let mut names: Vec<&str> = contract
519            .document_types()
520            .keys()
521            .map(String::as_str)
522            .collect();
523        names.sort();
524        assert_eq!(
525            names,
526            vec![
527                ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
528                ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
529                JOIN_REQUEST_DOCUMENT_TYPE_NAME,
530                REASON_DOCUMENT_TYPE_NAME,
531                REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
532                RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
533                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
534            ]
535        );
536        for name in names {
537            let document_type = document_type(&contract, name);
538            assert_eq!(
539                document_type.data_contract_id(),
540                MODERATION_CHARTERS_CONTRACT_ID
541            );
542            assert!(!document_type.documents_mutable(), "{name} is immutable");
543            // A team change is undone by deleting it: an addition takes the member off, a
544            // removal puts them back, a resignation request is withdrawn. What makes the
545            // charter is final.
546            assert_eq!(
547                document_type.documents_can_be_deleted(),
548                [
549                    ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
550                    REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
551                    RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
552                ]
553                .contains(&name),
554                "{name}: only the team changes can be deleted"
555            );
556        }
557    }
558
559    #[test]
560    fn should_declare_only_the_elected_charter_as_a_no_locking_contest() {
561        let contract = contract();
562        for name in [
563            REASON_DOCUMENT_TYPE_NAME,
564            SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
565            JOIN_REQUEST_DOCUMENT_TYPE_NAME,
566            ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
567            REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
568            RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
569        ] {
570            assert!(
571                document_type(&contract, name)
572                    .find_contested_index()
573                    .is_none(),
574                "{name} is not contested"
575            );
576        }
577        let index = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
578            .find_contested_index()
579            .expect("the elected charter type has a contested index");
580        assert_eq!(index.name, "byTargetContract");
581        assert!(index.unique);
582        assert_eq!(
583            index
584                .properties
585                .iter()
586                .map(|p| p.name.as_str())
587                .collect::<Vec<_>>(),
588            vec![property_names::TARGET_CONTRACT_ID]
589        );
590        assert_eq!(
591            index
592                .contested_index
593                .as_ref()
594                .expect("contested")
595                .resolution,
596            ContestedIndexResolution::MasternodeVoteNoLocking
597        );
598    }
599
600    #[test]
601    fn should_let_teams_form_before_the_election_opens() {
602        let contract = contract();
603        let moderation =
604            |type_name| match reference(&contract, type_name, property_names::TARGET_CONTRACT_ID) {
605                PropertyReference::Value(DocumentPropertyReferenceTarget::Contract {
606                    contract_requirements,
607                }) => contract_requirements.moderation,
608                other => panic!("{type_name}.targetContractId: {other:?}"),
609            };
610        assert_eq!(
611            moderation(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME),
612            Some(ContractReferenceModeration::Elected)
613        );
614        assert_eq!(
615            moderation(ELECTED_CHARTER_DOCUMENT_TYPE_NAME),
616            Some(ContractReferenceModeration::ElectionOpen)
617        );
618    }
619
620    #[test]
621    fn should_list_reasons_as_references_to_reason_documents() {
622        let contract = contract();
623        match reference(
624            &contract,
625            SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
626            property_names::REASONS,
627        ) {
628            PropertyReference::Elements {
629                target:
630                    DocumentPropertyReferenceTarget::PermanentDocument {
631                        contract_id: None,
632                        document_type_name,
633                        ..
634                    },
635                max_items,
636            } => {
637                assert_eq!(document_type_name, REASON_DOCUMENT_TYPE_NAME);
638                assert_eq!(max_items, 64);
639            }
640            other => panic!("reasons: {other:?}"),
641        }
642    }
643
644    #[test]
645    fn should_address_a_join_request_to_the_leader_under_bound_keys() {
646        let contract = contract();
647        match reference(
648            &contract,
649            JOIN_REQUEST_DOCUMENT_TYPE_NAME,
650            "submittedCharterId",
651        ) {
652            PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
653                document_type_name,
654                property_agreement,
655                ..
656            }) => {
657                assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
658                assert_eq!(
659                    property_agreement.get("recipientId").map(String::as_str),
660                    Some("$ownerId"),
661                    "the recipient is the proposal's owner"
662                );
663            }
664            other => panic!("submittedCharterId: {other:?}"),
665        }
666        match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "recipientId") {
667            PropertyReference::Value(DocumentPropertyReferenceTarget::IdentityPublicKey {
668                key_id_property,
669                key_requirements,
670            }) => {
671                assert_eq!(key_id_property, "recipientKeyId");
672                assert_eq!(key_requirements.purpose, Some(Purpose::DECRYPTION));
673                assert_eq!(
674                    key_requirements.bound_to.as_deref(),
675                    Some(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
676                );
677            }
678            other => panic!("recipientId: {other:?}"),
679        }
680        match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "senderKeyId") {
681            PropertyReference::KeyId(key_reference) => {
682                assert_eq!(
683                    key_reference.identity_property,
684                    KeyReferenceIdentityProperty::OwnerId
685                );
686                assert_eq!(
687                    key_reference.key_requirements.purpose,
688                    Some(Purpose::ENCRYPTION)
689                );
690                assert_eq!(
691                    key_reference.key_requirements.bound_to.as_deref(),
692                    Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME)
693                );
694            }
695            other => panic!("senderKeyId: {other:?}"),
696        }
697        let encrypted_for = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME)
698            .flattened_properties()
699            .get("encryptedMessage")
700            .and_then(|property| property.encrypted_for.clone())
701            .expect("the message declares its envelope");
702        assert_eq!(
703            encrypted_for.recipient,
704            EncryptedForRecipient::Property("recipientId".to_string())
705        );
706        assert_eq!(encrypted_for.recipient_key, "recipientKeyId");
707        assert_eq!(encrypted_for.sender_key, "senderKeyId");
708        assert_eq!(
709            encrypted_for.scheme,
710            EncryptionScheme::EcdhSecp256k1Aes256Cbc
711        );
712    }
713
714    #[test]
715    fn should_open_the_contest_only_from_the_leaders_own_proposal() {
716        let contract = contract();
717        match reference(
718            &contract,
719            ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
720            property_names::SUBMITTED_CHARTER_ID,
721        ) {
722            PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
723                document_type_name,
724                property_agreement,
725                ..
726            }) => {
727                assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
728                assert_eq!(
729                    property_agreement.get("$ownerId").map(String::as_str),
730                    Some("$ownerId")
731                );
732                assert_eq!(
733                    property_agreement
734                        .get(property_names::TARGET_CONTRACT_ID)
735                        .map(String::as_str),
736                    Some(property_names::TARGET_CONTRACT_ID)
737                );
738            }
739            other => panic!("submittedCharterId: {other:?}"),
740        }
741    }
742
743    #[test]
744    fn should_choose_members_only_from_the_proposals_join_requests() {
745        let contract = contract();
746        let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
747            .flattened_properties()
748            .get(property_names::MEMBERS)
749            .expect("members")
750            .clone();
751        match members.property_type.reference() {
752            Some(PropertyReference::Elements {
753                target:
754                    DocumentPropertyReferenceTarget::PermanentDocumentLookup {
755                        document_type_name,
756                        lookup,
757                        ..
758                    },
759                max_items,
760            }) => {
761                assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
762                assert_eq!(max_items, 15);
763                assert_eq!(
764                    lookup.resolve_index(
765                        document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
766                    ),
767                    Ok("bySubmittedCharter".to_string())
768                );
769                assert_eq!(
770                    lookup.keys.get(property_names::SUBMITTED_CHARTER_ID),
771                    Some(&LookupKeySource::Property(
772                        property_names::SUBMITTED_CHARTER_ID.to_string()
773                    ))
774                );
775                assert_eq!(
776                    lookup.keys.get("$ownerId"),
777                    Some(&LookupKeySource::ReferenceValue)
778                );
779            }
780            other => panic!("members: {other:?}"),
781        }
782        let DocumentPropertyType::TypedArray(typed_array) = &members.property_type else {
783            panic!("members is a typed array");
784        };
785        assert!(typed_array.unique_items);
786        assert_eq!(typed_array.min_items.unwrap_or_default(), 0);
787        assert_eq!(
788            members.distinct_from,
789            Some(DistinctFrom::OwnerId),
790            "the leader cannot list themself"
791        );
792        let join_request = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
793        let index = join_request
794            .indexes()
795            .get("bySubmittedCharter")
796            .expect("the join request's unique index");
797        assert!(index.unique);
798        assert!(
799            !join_request.documents_transferable().is_transferable(),
800            "a lookup may key on $ownerId only on a type that cannot change hands"
801        );
802    }
803
804    #[test]
805    fn should_round_trip_a_proposal_through_the_system_contract() {
806        let contract = contract();
807        let proposal = proposal();
808        let document = document_with(
809            &contract,
810            SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
811            proposal.to_document_properties(),
812        );
813        assert_eq!(
814            schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
815            vec![],
816            "the encoded proposal passes the schema"
817        );
818        let read = SubmittedCharter::from_document_properties(document.properties())
819            .into_data()
820            .expect("the proposal reads");
821        assert_eq!(read, proposal);
822    }
823
824    /// A proposal's reward split is held to 100 by a `propertyConstraints` rule, which
825    /// the contract's document validation applies, so consensus checks it on every create
826    /// and a client validating the document sees the same answer.
827    #[test]
828    fn should_hold_a_proposal_reward_split_to_one_hundred() {
829        let contract = contract();
830        let submitted_charter = document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
831        assert_eq!(
832            submitted_charter
833                .property_constraints()
834                .keys()
835                .collect::<Vec<_>>(),
836            ["rewardSplitIsWhole"]
837        );
838        let judge = |leader: u8, equal: u8, actions: u8| {
839            let mut proposal = proposal();
840            proposal.reward_split = ModerationCharterRewardSplit {
841                leader,
842                equal,
843                actions,
844            };
845            let document = document_with(
846                &contract,
847                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
848                proposal.to_document_properties(),
849            );
850            schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
851        };
852        assert_eq!(judge(10, 40, 50), vec![]);
853        assert_eq!(judge(100, 0, 0), vec![]);
854        for (leader, equal, actions) in [(10, 40, 40), (40, 40, 40), (0, 0, 0)] {
855            let errors = judge(leader, equal, actions);
856            assert!(
857                matches!(
858                    errors.as_slice(),
859                    [ConsensusError::BasicError(
860                        BasicError::DocumentPropertyConstraintViolatedError(e)
861                    )] if e.constraint() == "rewardSplitIsWhole"
862                        && e.violation() == PropertyConstraintViolation::NotMet
863                ),
864                "{leader} + {equal} + {actions}: {errors:?}"
865            );
866        }
867    }
868
869    #[test]
870    fn should_round_trip_an_elected_charter_through_the_system_contract() {
871        let contract = contract();
872        let charter = ElectedCharter {
873            target_contract_id: Identifier::from([9u8; 32]),
874            submitted_charter_id: Identifier::from([7u8; 32]),
875            members: vec![Identifier::from([2u8; 32]), Identifier::from([5u8; 32])],
876        };
877        let document = document_with(
878            &contract,
879            ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
880            charter.to_document_properties(),
881        );
882        assert_eq!(
883            schema_validation(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
884            vec![],
885            "the encoded elected charter passes the schema"
886        );
887        let read = ElectedCharter::from_document_properties(document.properties())
888            .into_data()
889            .expect("the elected charter reads back");
890        assert_eq!(read, charter);
891    }
892
893    #[test]
894    fn should_refuse_through_the_schema_what_it_can_express() {
895        let contract = contract();
896        let too_many =
897            |count: u8| Value::Array((0..count).map(|i| Value::Identifier([i; 32])).collect());
898        for (type_name, property, value, expected_keyword) in [
899            (
900                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
901                property_names::DESCRIPTION,
902                Value::Text("a".repeat(4097)),
903                "maxLength",
904            ),
905            (
906                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
907                property_names::DESCRIPTION,
908                Value::Text(String::new()),
909                "minLength",
910            ),
911            (
912                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
913                property_names::REASONS,
914                too_many(65),
915                "maxItems",
916            ),
917            (
918                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
919                property_names::REASONS,
920                Value::Array(vec![Value::Identifier([3; 32]), Value::Identifier([3; 32])]),
921                "uniqueItems",
922            ),
923            (
924                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
925                property_names::MODERATORS_SHARE,
926                Value::U8(101),
927                "maximum",
928            ),
929            (
930                ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
931                property_names::MEMBERS,
932                too_many(16),
933                "maxItems",
934            ),
935        ] {
936            let properties = if type_name == SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME {
937                proposal().to_document_properties()
938            } else {
939                ElectedCharter {
940                    target_contract_id: Identifier::from([9u8; 32]),
941                    submitted_charter_id: Identifier::from([7u8; 32]),
942                    members: vec![],
943                }
944                .to_document_properties()
945            };
946            let mut document = document_with(&contract, type_name, properties);
947            document.set(property, value);
948            let errors = schema_validation(&contract, type_name, &document);
949            let message = format!("{errors:?}");
950            assert!(
951                !errors.is_empty() && message.contains(expected_keyword),
952                "{type_name}.{property}: expected a {expected_keyword} error, got {message}"
953            );
954        }
955        for property in [
956            property_names::TARGET_CONTRACT_ID,
957            property_names::DESCRIPTION,
958            property_names::REASONS,
959            property_names::REWARD_SPLIT,
960        ] {
961            let mut document = document_with(
962                &contract,
963                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
964                proposal().to_document_properties(),
965            );
966            document.properties_mut().remove(property);
967            assert!(
968                !schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
969                    .is_empty(),
970                "{property} is required"
971            );
972        }
973    }
974
975    /// The description's cap is 4096 bytes, not just 4096 characters: the schema's `maxBytes`,
976    /// which document validation checks after the JSON schema, so clients refuse an oversized
977    /// description before they broadcast it.
978    #[test]
979    fn should_refuse_a_description_over_4096_bytes_within_4096_characters() {
980        let contract = contract();
981        assert_eq!(
982            document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
983                .flattened_properties()
984                .get(property_names::DESCRIPTION)
985                .expect("the description")
986                .property_type,
987            DocumentPropertyType::String(StringPropertySizes {
988                min_length: Some(1),
989                max_length: Some(4096),
990                max_bytes: Some(4096),
991            })
992        );
993        let with_description = |description: String| {
994            document_with(
995                &contract,
996                SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
997                SubmittedCharter {
998                    description,
999                    ..proposal()
1000                }
1001                .to_document_properties(),
1002            )
1003        };
1004
1005        // At the cap, in one-byte and in two-byte characters
1006        for description in ["a".repeat(4096), "é".repeat(2048)] {
1007            let document = with_description(description);
1008            assert_eq!(
1009                schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
1010                vec![]
1011            );
1012        }
1013
1014        // 2049 characters are within maxLength, but their 4098 bytes are over maxBytes
1015        let document = with_description("é".repeat(2049));
1016        assert!(matches!(
1017            schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
1018                .as_slice(),
1019            [ConsensusError::BasicError(BasicError::DocumentPropertyMaxBytesExceededError(e))]
1020                if e.property() == property_names::DESCRIPTION
1021                    && e.byte_length() == 4098
1022                    && e.max_bytes() == 4096
1023        ));
1024    }
1025
1026    /// After the election the leader adds members from the same join requests and removes
1027    /// elected members, and a member asks to leave: each change is written once per member,
1028    /// and only by the one entitled to it.
1029    #[test]
1030    fn should_let_only_the_leader_change_the_team_and_only_a_member_resign() {
1031        let contract = contract();
1032        let charter_agreement =
1033            |type_name| match reference(&contract, type_name, property_names::ELECTED_CHARTER_ID) {
1034                PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
1035                    document_type_name,
1036                    property_agreement,
1037                    ..
1038                }) => {
1039                    assert_eq!(document_type_name, ELECTED_CHARTER_DOCUMENT_TYPE_NAME);
1040                    property_agreement.clone()
1041                }
1042                other => panic!("{type_name}.electedCharterId: {other:?}"),
1043            };
1044
1045        let added = charter_agreement(ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
1046        assert_eq!(added.get("$ownerId").map(String::as_str), Some("$ownerId"));
1047        assert_eq!(
1048            added
1049                .get(property_names::SUBMITTED_CHARTER_ID)
1050                .map(String::as_str),
1051            Some(property_names::SUBMITTED_CHARTER_ID)
1052        );
1053        let removed = charter_agreement(REMOVED_MODERATOR_DOCUMENT_TYPE_NAME);
1054        assert_eq!(
1055            removed.get("$ownerId").map(String::as_str),
1056            Some("$ownerId")
1057        );
1058        let resignation = charter_agreement(RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
1059        assert_eq!(
1060            resignation.get("recipientId").map(String::as_str),
1061            Some("$ownerId"),
1062            "a resignation is addressed to the charter's leader"
1063        );
1064
1065        match reference(
1066            &contract,
1067            ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1068            property_names::MEMBER_ID,
1069        ) {
1070            PropertyReference::Value(
1071                DocumentPropertyReferenceTarget::PermanentDocumentLookup {
1072                    document_type_name,
1073                    lookup,
1074                    ..
1075                },
1076            ) => {
1077                assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
1078                assert_eq!(
1079                    lookup.resolve_index(
1080                        document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
1081                    ),
1082                    Ok("bySubmittedCharter".to_string())
1083                );
1084                assert_eq!(
1085                    lookup.keys.get("$ownerId"),
1086                    Some(&LookupKeySource::ReferenceValue)
1087                );
1088            }
1089            other => panic!("addedModerator.memberId: {other:?}"),
1090        }
1091        // Only an elected member can be removed; an added one is taken off by deleting the
1092        // addition
1093        match reference(
1094            &contract,
1095            REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1096            property_names::MEMBER_ID,
1097        ) {
1098            PropertyReference::Value(DocumentPropertyReferenceTarget::ListElement(listed)) => {
1099                assert_eq!(
1100                    listed.document_type_name,
1101                    ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1102                );
1103                assert_eq!(listed.in_list, property_names::MEMBERS);
1104                assert_eq!(
1105                    listed.document_id_property(),
1106                    Some(property_names::ELECTED_CHARTER_ID)
1107                );
1108            }
1109            other => panic!("removedModerator.memberId: {other:?}"),
1110        }
1111        for type_name in [
1112            ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1113            REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1114        ] {
1115            let member = document_type(&contract, type_name)
1116                .flattened_properties()
1117                .get(property_names::MEMBER_ID)
1118                .expect("memberId");
1119            assert_eq!(
1120                member.distinct_from,
1121                Some(DistinctFrom::OwnerId),
1122                "{type_name}: the leader is not a member"
1123            );
1124        }
1125
1126        for (type_name, index_name, member_property) in [
1127            (
1128                ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1129                "byElectedCharterMember",
1130                property_names::MEMBER_ID,
1131            ),
1132            (
1133                REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1134                "byElectedCharterMember",
1135                property_names::MEMBER_ID,
1136            ),
1137            (
1138                RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1139                "byElectedCharterOwner",
1140                "$ownerId",
1141            ),
1142        ] {
1143            let index = document_type(&contract, type_name)
1144                .indexes()
1145                .get(index_name)
1146                .unwrap_or_else(|| panic!("{type_name}.{index_name}"));
1147            assert!(index.unique, "{type_name}: once per member and charter");
1148            assert_eq!(
1149                index
1150                    .properties
1151                    .iter()
1152                    .map(|p| p.name.as_str())
1153                    .collect::<Vec<_>>(),
1154                vec![property_names::ELECTED_CHARTER_ID, member_property]
1155            );
1156        }
1157    }
1158
1159    /// Only a member of the seated team may ask to leave: the writer is listed in the elected
1160    /// charter's `members`, or the leader added it after the election and has not deleted the
1161    /// addition. The request is
1162    /// deletable, which withdraws it, and carries a message only the leader can read.
1163    #[test]
1164    fn should_let_only_a_team_member_ask_to_leave() {
1165        let contract = contract();
1166        let resignation = document_type(&contract, RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
1167        assert!(resignation.documents_can_be_deleted());
1168
1169        let Some(DocumentPropertyReferenceTarget::AnyOf(operands)) = resignation.owner_reference()
1170        else {
1171            panic!(
1172                "the writer must meet any of the membership targets: {:?}",
1173                resignation.owner_reference()
1174            );
1175        };
1176        match operands.operands() {
1177            [DocumentPropertyReferenceTarget::ListElement(listed), DocumentPropertyReferenceTarget::DeletableDocumentLookup {
1178                document_type_name,
1179                lookup,
1180                ..
1181            }] => {
1182                assert_eq!(
1183                    listed.document_type_name,
1184                    ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1185                );
1186                assert_eq!(listed.in_list, property_names::MEMBERS);
1187                assert_eq!(
1188                    listed.document_id_property(),
1189                    Some(property_names::ELECTED_CHARTER_ID)
1190                );
1191                assert_eq!(document_type_name, ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
1192                assert_eq!(
1193                    lookup.resolve_index(
1194                        document_type(&contract, ADDED_MODERATOR_DOCUMENT_TYPE_NAME).as_ref()
1195                    ),
1196                    Ok("byElectedCharterMember".to_string())
1197                );
1198                assert_eq!(
1199                    lookup.keys.get(property_names::MEMBER_ID),
1200                    Some(&LookupKeySource::ReferenceValue)
1201                );
1202            }
1203            other => panic!("resignation membership operands: {other:?}"),
1204        }
1205
1206        let encrypted_for = resignation
1207            .flattened_properties()
1208            .get("encryptedMessage")
1209            .and_then(|property| property.encrypted_for.clone())
1210            .expect("the message declares its envelope");
1211        assert_eq!(
1212            encrypted_for.recipient,
1213            EncryptedForRecipient::Property("recipientId".to_string())
1214        );
1215        match reference(
1216            &contract,
1217            RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1218            "senderKeyId",
1219        ) {
1220            PropertyReference::KeyId(key_reference) => {
1221                assert_eq!(
1222                    key_reference.key_requirements.bound_to.as_deref(),
1223                    Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME),
1224                    "the member's encryption key is the one its join request used"
1225                );
1226            }
1227            other => panic!("senderKeyId: {other:?}"),
1228        }
1229    }
1230}
1231
1232#[cfg(test)]
1233mod dpns_v3_tests {
1234    use super::*;
1235    use crate::data_contract::accessors::v0::DataContractV0Getters;
1236    use crate::data_contract::document_type::accessors::{
1237        DocumentTypeV0Getters, DocumentTypeV2Getters,
1238    };
1239    use crate::data_contract::document_type::property_constraints::{
1240        AggregateBinding, AggregateKind, AggregateRead, ConstraintComparison, ConstraintExpression,
1241        EqualityKind, PropertyConstraint,
1242    };
1243    use crate::data_contract::document_type::{
1244        ChooseFirstOrder, DocumentReferenceKind, DocumentTypeRef, HashFunction, LookupChooseFirst,
1245        LookupHashKey, LookupKeyParam, LookupKeySource,
1246    };
1247    use crate::util::hash::hash_double;
1248    use platform_value::{Identifier, Value};
1249    use std::collections::BTreeMap;
1250
1251    /// DPNS v3, registered as the chain registers it, under full validation.
1252    fn dpns() -> DataContract {
1253        load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest())
1254            .expect("DPNS v3 registers under full validation")
1255    }
1256
1257    fn document_type<'a>(contract: &'a DataContract, name: &str) -> DocumentTypeRef<'a> {
1258        contract
1259            .document_type_for_name(name)
1260            .unwrap_or_else(|_| panic!("the {name} type"))
1261    }
1262
1263    fn name_params() -> Vec<LookupKeyParam> {
1264        vec![
1265            LookupKeyParam::Property("normalizedLabel".to_string()),
1266            LookupKeyParam::Const(".".to_string()),
1267            LookupKeyParam::Property("normalizedParentDomainName".to_string()),
1268        ]
1269    }
1270
1271    /// A domain finds its creator's own preorder by the creator's id and the hash of the
1272    /// normalized name, made in an earlier block, chosen first by creation height among the
1273    /// preorders of the name that hold the hash of their own owner's id and the name, and
1274    /// deletes it. The salt stays declared, unused, so domains written under v1 and v2 decode
1275    #[test]
1276    fn should_find_the_creators_own_preorder_chosen_first_among_those_of_the_name() {
1277        let contract = dpns();
1278        let domain = document_type(&contract, "domain");
1279        let declaration = domain
1280            .creator_reference()
1281            .and_then(|target| target.as_any_document_reference())
1282            .expect("the domain's creatorRefersTo is a document reference");
1283        assert_eq!(declaration.document_type_name, "preorder");
1284        assert_eq!(declaration.kind, DocumentReferenceKind::Deletable);
1285        let lookup = declaration.lookup.expect("found by findBy");
1286        assert_eq!(lookup.keys.get("$ownerId"), Some(&LookupKeySource::OwnerId));
1287        assert_eq!(
1288            lookup.keys.get("nameHash"),
1289            Some(&LookupKeySource::Hash(LookupHashKey {
1290                function: HashFunction::Sha256d,
1291                params: name_params(),
1292            }))
1293        );
1294        assert_eq!(lookup.minimum_age_blocks, Some(1));
1295        assert_eq!(
1296            lookup.choose_first,
1297            Some(LookupChooseFirst {
1298                among: "nameHash".to_string(),
1299                valid_when_property: "ownerNameHash".to_string(),
1300                valid_when: LookupHashKey {
1301                    function: HashFunction::Sha256d,
1302                    params: [vec![LookupKeyParam::Writer], name_params()].concat(),
1303                },
1304                by: vec![ChooseFirstOrder {
1305                    property: "$createdAtBlockHeight".to_string(),
1306                    ascending: true,
1307                }],
1308            })
1309        );
1310        assert!(lookup.consume);
1311
1312        let salt = domain
1313            .flattened_properties()
1314            .get("preorderSalt")
1315            .expect("preorderSalt stays declared");
1316        assert!(salt.transient);
1317        assert!(salt.property_type.reference().is_none());
1318        assert!(salt.revealed_reference.is_none());
1319        assert!(!domain.required_fields().contains("preorderSalt"));
1320    }
1321
1322    /// The preorder of `b0b.dash` by an identity holds the double SHA-256 of the normalized
1323    /// label, "." and the normalized parent, and, to count, the same with that identity's id
1324    /// first
1325    #[test]
1326    fn should_hash_the_normalized_name_and_the_creator_with_it() {
1327        let contract = dpns();
1328        let domain = document_type(&contract, "domain");
1329        let lookup = domain
1330            .creator_reference()
1331            .and_then(|target| target.as_any_document_reference())
1332            .and_then(|declaration| declaration.lookup)
1333            .expect("the creator reference is found by findBy");
1334        let name_key = domain
1335            .creator_reference()
1336            .and_then(|target| target.lookup_hash_key())
1337            .expect("the creator reference holds a findBy function");
1338        let owner_name_key = &lookup
1339            .choose_first
1340            .as_ref()
1341            .expect("the creator reference chooses the first preorder")
1342            .valid_when;
1343        let creator = Identifier::from([3; 32]);
1344        let data = BTreeMap::from([
1345            (
1346                "normalizedLabel".to_string(),
1347                Value::Text("b0b".to_string()),
1348            ),
1349            (
1350                "normalizedParentDomainName".to_string(),
1351                Value::Text("dash".to_string()),
1352            ),
1353        ]);
1354
1355        let (name_hash, _) = name_key
1356            .key_value(domain, None, creator, &data)
1357            .expect("the name preimage assembles");
1358        let (owner_name_hash, _) = owner_name_key
1359            .key_value(domain, None, creator, &data)
1360            .expect("the owner and name preimage assembles");
1361
1362        assert_eq!(name_hash, hash_double(b"b0b.dash"));
1363        let mut preimage = creator.to_buffer().to_vec();
1364        preimage.extend(b"b0b.dash");
1365        assert_eq!(owner_name_hash, hash_double(preimage));
1366    }
1367
1368    /// A preorder lives an hour, is unique per owner and name hash, and is indexed by its
1369    /// name hash and creation height
1370    #[test]
1371    fn should_expire_preorders_after_an_hour_and_index_them_by_name_hash() {
1372        let contract = dpns();
1373        let preorder = document_type(&contract, "preorder");
1374        assert_eq!(preorder.documents_ttl_seconds(), Some(3600));
1375
1376        let index_properties = |name: &str| {
1377            let index = preorder
1378                .indexes()
1379                .get(name)
1380                .unwrap_or_else(|| panic!("the {name} index"));
1381            (
1382                index
1383                    .properties
1384                    .iter()
1385                    .map(|property| property.name.clone())
1386                    .collect::<Vec<_>>(),
1387                index.unique,
1388            )
1389        };
1390        assert_eq!(
1391            index_properties("ownerAndNameHash"),
1392            (vec!["$ownerId".to_string(), "nameHash".to_string()], true)
1393        );
1394        assert_eq!(
1395            index_properties("nameHashAndHeight"),
1396            (
1397                vec!["nameHash".to_string(), "$createdAtBlockHeight".to_string()],
1398                false
1399            )
1400        );
1401    }
1402
1403    /// A name holds at most `max_choose_first_candidates` preorders, counted by its name hash
1404    /// through the countable `nameHash` index: the competitors a reveal reads are then always
1405    /// all of those before the revealer's own, so none can be pushed past the read
1406    #[test]
1407    fn should_hold_a_name_to_the_preorders_a_reveal_reads() {
1408        let contract = dpns();
1409        let preorder = document_type(&contract, "preorder");
1410        let index = preorder
1411            .indexes()
1412            .get("nameHash")
1413            .expect("the nameHash index");
1414        assert!(index.countable.is_countable());
1415        assert!(!index.unique);
1416
1417        let cap = preorder
1418            .property_constraints()
1419            .get("atMostAThousandPerName")
1420            .expect("the per-name preorder cap");
1421        let by_name_hash = AggregateRead {
1422            kind: AggregateKind::Count,
1423            document_type: "preorder".to_string(),
1424            filter: [(
1425                "nameHash".to_string(),
1426                AggregateBinding::Property {
1427                    path: "nameHash".to_string(),
1428                    kind: Some(EqualityKind::Bytes),
1429                },
1430            )]
1431            .into(),
1432            of_own_type: true,
1433        };
1434        assert_eq!(
1435            cap,
1436            &PropertyConstraint::Compare {
1437                comparison: ConstraintComparison::LessThanOrEqual,
1438                left: ConstraintExpression::Aggregate(by_name_hash.clone()),
1439                right: ConstraintExpression::Value(i128::from(
1440                    PlatformVersion::latest()
1441                        .system_limits
1442                        .max_choose_first_candidates
1443                )),
1444            }
1445        );
1446        assert_eq!(
1447            by_name_hash
1448                .answering_index(&preorder)
1449                .map(|index| index.name.as_str()),
1450            Some("nameHash")
1451        );
1452    }
1453}