Skip to main content

drive_abci/execution/validation/state_transition/state_transitions/
mod.rs

1/// Module containing functionality related to batch processing of documents.
2pub mod batch;
3
4/// Module for creating an identity entity.
5pub mod identity_create;
6
7/// Module for managing transfers of credit between identity entities.
8pub mod identity_credit_transfer;
9
10/// Module for managing withdrawals of credit from an identity entity.
11pub mod identity_credit_withdrawal;
12
13/// Module for topping up credit in an identity entity.
14pub mod identity_top_up;
15
16/// Module for updating an existing identity entity.
17pub mod identity_update;
18
19/// Module for raising the limits of an identity key.
20pub mod identity_key_limits_update;
21
22/// Validation shared by the data contract create and update transitions.
23pub mod data_contract_common;
24
25/// Module for creating a data contract entity.
26pub mod data_contract_create;
27
28/// Module for updating an existing data contract entity.
29pub mod data_contract_update;
30
31/// Module for voting from a masternode.
32pub mod masternode_vote;
33
34/// Identity create from addresses
35pub mod identity_create_from_addresses;
36
37/// Module for validation of address funding from asset lock transitions
38pub mod address_funding_from_asset_lock;
39
40/// Module for validation of credit transfer from an identity to addresses
41pub mod identity_credit_transfer_to_addresses;
42
43/// Module for validation of address credit withdrawal transitions
44pub mod address_credit_withdrawal;
45
46/// Module for validation of address funds transfer transitions
47pub mod address_funds_transfer;
48mod identity_top_up_from_addresses;
49
50/// Module for identity-create-from-shielded-pool transition validation
51pub mod identity_create_from_shielded_pool;
52/// Identity top up from shielded pool (pool to an existing identity)
53pub mod identity_top_up_from_shielded_pool;
54/// Module for shield transition validation
55pub mod shield;
56/// Module for shield from asset lock transition validation
57pub mod shield_from_asset_lock;
58/// Shield from identity (identity balance to shielded pool)
59pub mod shield_from_identity;
60/// Common validation logic shared by shielded transitions (proof verification)
61pub mod shielded_common;
62/// Module for shielded transfer transition validation
63pub mod shielded_transfer;
64/// Module for shielded withdrawal transition validation
65pub mod shielded_withdrawal;
66/// Module for unshield transition validation
67pub mod unshield;
68
69use dpp::document::Document;
70use dpp::version::PlatformVersion;
71
72pub(crate) fn stamp_withdrawal_document(
73    document: &mut Document,
74    platform_version: &PlatformVersion,
75) {
76    match document {
77        Document::V0(document) => {
78            document.contract_version =
79                Some(platform_version.system_data_contracts.withdrawals as u32);
80        }
81    }
82}
83
84/// The validation mode we are using
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum ValidationMode {
87    /// The basic checktx before the state transition is put into mempool
88    CheckTx,
89    /// Rechecking a state transition every block
90    RecheckTx,
91    /// The validation during block execution by a proposer or validator
92    Validator,
93    /// A validation mode used to get the action with no validation
94    NoValidation,
95}
96
97impl ValidationMode {
98    /// Can this validation mode alter cache on drive?
99    pub fn can_alter_cache(&self) -> bool {
100        match self {
101            ValidationMode::CheckTx => false,
102            ValidationMode::RecheckTx => false,
103            ValidationMode::Validator => true,
104            ValidationMode::NoValidation => false,
105        }
106    }
107}
108
109#[cfg(test)]
110pub(crate) mod test_helpers;
111
112#[cfg(test)]
113pub(in crate::execution) mod tests {
114    use crate::rpc::core::MockCoreRPCLike;
115    use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder};
116    use dpp::block::block_info::BlockInfo;
117    use dpp::data_contracts::SystemDataContract;
118    use dpp::fee::Credits;
119    use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
120    use dpp::identity::{Identity, IdentityPublicKey, IdentityV0, KeyID, KeyType, Purpose, SecurityLevel, TimestampMillis};
121    use dpp::prelude::{BlockHeight, Identifier, IdentityNonce};
122    use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0;
123    use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition;
124    use dpp::system_data_contracts::load_system_data_contract;
125    use dpp::tests::json_document::json_document_to_contract_with_ids;
126    use drive::drive::document::query::QueryDocumentsOutcomeV0Methods;
127    use drive::query::DriveDocumentQuery;
128    use platform_version::version::PlatformVersion;
129    use rand::prelude::StdRng;
130    use rand::{Rng, SeedableRng};
131    use simple_signer::signer::SimpleSigner;
132    use std::borrow::Cow;
133    use std::collections::BTreeMap;
134    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
135    use std::ops::Deref;
136    use std::sync::Arc;
137    use arc_swap::Guard;
138    use assert_matches::assert_matches;
139    use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType};
140    use dapi_grpc::platform::v0::{get_contested_resource_vote_state_request, get_contested_resource_vote_state_response, GetContestedResourceVoteStateRequest, GetContestedResourceVoteStateResponse};
141    use dapi_grpc::platform::v0::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::ResultType;
142    use dapi_grpc::platform::v0::get_contested_resource_vote_state_request::{get_contested_resource_vote_state_request_v0, GetContestedResourceVoteStateRequestV0};
143    use dapi_grpc::platform::v0::get_contested_resource_vote_state_response::{get_contested_resource_vote_state_response_v0, GetContestedResourceVoteStateResponseV0};
144    use dapi_grpc::platform::v0::get_contested_resource_vote_state_response::get_contested_resource_vote_state_response_v0::FinishedVoteInfo;
145    use dpp::balances::credits::TokenAmount;
146    use dpp::dash_to_credits;
147    use dpp::dashcore::{ProTxHash, Txid};
148    use dpp::dashcore::hashes::Hash;
149    use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters};
150    use dpp::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters};
151    use dpp::data_contract::{DataContract, GroupContractPosition, TokenContractPosition};
152    use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV1Setters};
153    use dpp::data_contract::document_type::random_document::{CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType};
154    use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters};
155    use dpp::document::serialization_traits::DocumentPlatformConversionMethodsV0;
156    use dpp::fee::fee_result::FeeResult;
157    use dpp::identifier::MasternodeIdentifiers;
158    use dpp::identity::accessors::IdentityGettersV0;
159    use dpp::identity::contract_bounds::ContractBounds;
160    use dpp::identity::hash::IdentityPublicKeyHashMethodsV0;
161    use dpp::platform_value::{Bytes32, Value};
162    use dpp::serialization::PlatformSerializable;
163    use dpp::state_transition::batch_transition::BatchTransition;
164    use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0;
165    use dpp::state_transition::masternode_vote_transition::MasternodeVoteTransition;
166    use dpp::state_transition::masternode_vote_transition::methods::MasternodeVoteTransitionMethodsV0;
167    use dpp::state_transition::StateTransition;
168    use dpp::block::epoch::EpochIndex;
169    use dpp::tokens::calculate_token_id;
170    use dpp::util::hash::hash_double;
171    use dpp::util::strings::convert_to_homograph_safe_chars;
172    use dpp::voting::contender_structs::{Contender, ContenderV0};
173    use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice;
174    use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo;
175    use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
176    use dpp::voting::vote_polls::VotePoll;
177    use dpp::voting::votes::resource_vote::ResourceVote;
178    use dpp::voting::votes::resource_vote::v0::ResourceVoteV0;
179    use dpp::voting::votes::Vote;
180    use drive::util::object_size_info::DataContractResolvedInfo;
181    use drive::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed;
182    use drive::query::vote_poll_vote_state_query::ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally;
183    use drive::query::vote_poll_vote_state_query::{ContestedDocumentVotePollDriveQueryResultType, ResolvedContestedDocumentVotePollDriveQuery};
184    use drive::util::test_helpers::setup_contract;
185    use crate::execution::types::block_execution_context::BlockExecutionContext;
186    use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0;
187    use crate::expect_match;
188    use crate::platform_types::platform_state::PlatformState;
189    use crate::platform_types::platform_state::PlatformStateV0Methods;
190    use crate::platform_types::state_transitions_processing_result::{StateTransitionExecutionResult, StateTransitionsProcessingResult};
191    use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::{SuccessfulExecution, UnpaidConsensusError};
192    use crate::execution::types::block_state_info::BlockStateInfo;
193    use crate::execution::types::block_state_info::v0::BlockStateInfoV0;
194    use crate::platform_types::epoch_info::EpochInfo;
195    use crate::platform_types::epoch_info::v0::EpochInfoV0;
196    use crate::execution::types::block_fees::v0::BlockFeesV0;
197    use crate::execution::types::processed_block_fees_outcome::v0::ProcessedBlockFeesOutcome;
198    use dpp::data_contract::associated_token::token_configuration::TokenConfiguration;
199    use dpp::data_contract::group::Group;
200    use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy;
201    use dpp::tokens::token_amount_on_contract_token::{DocumentActionTokenCost, DocumentActionTokenEffect};
202    use dpp::data_contract::document_type::accessors::DocumentTypeV0MutGetters;
203    use dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructureUntrusted;
204
205    /// We add an identity, but we also add the same amount to system credits
206    pub(in crate::execution) fn setup_identity_with_system_credits(
207        platform: &mut TempPlatform<MockCoreRPCLike>,
208        seed: u64,
209        credits: Credits,
210    ) -> (Identity, SimpleSigner, IdentityPublicKey) {
211        let platform_version = PlatformVersion::latest();
212        platform
213            .drive
214            .add_to_system_credits(credits, None, platform_version)
215            .expect("expected to add to system credits");
216        setup_identity(platform, seed, credits)
217    }
218
219    pub(in crate::execution) fn setup_identity(
220        platform: &mut TempPlatform<MockCoreRPCLike>,
221        seed: u64,
222        credits: Credits,
223    ) -> (Identity, SimpleSigner, IdentityPublicKey) {
224        let platform_version = PlatformVersion::latest();
225        let mut signer = SimpleSigner::default();
226
227        let mut rng = StdRng::seed_from_u64(seed);
228
229        let (master_key, master_private_key) =
230            IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng(
231                0,
232                &mut rng,
233                platform_version,
234            )
235            .expect("expected to get key pair");
236
237        signer.add_identity_public_key(master_key.clone(), master_private_key);
238
239        let (critical_public_key, private_key) =
240            IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng(
241                1,
242                &mut rng,
243                platform_version,
244            )
245            .expect("expected to get key pair");
246
247        signer.add_identity_public_key(critical_public_key.clone(), private_key);
248
249        let identity: Identity = IdentityV0 {
250            id: Identifier::random_with_rng(&mut rng),
251            public_keys: BTreeMap::from([
252                (0, master_key.clone()),
253                (1, critical_public_key.clone()),
254            ]),
255            balance: credits,
256            revision: 0,
257        }
258        .into();
259
260        // We just add this identity to the system first
261
262        platform
263            .drive
264            .add_new_identity(
265                identity.clone(),
266                false,
267                &BlockInfo::default(),
268                true,
269                None,
270                platform_version,
271            )
272            .expect("expected to add a new identity");
273
274        (identity, signer, critical_public_key)
275    }
276
277    pub(in crate::execution) fn setup_identity_without_adding_it(
278        seed: u64,
279        credits: Credits,
280    ) -> (Identity, SimpleSigner, IdentityPublicKey) {
281        let platform_version = PlatformVersion::latest();
282        let mut signer = SimpleSigner::default();
283
284        let mut rng = StdRng::seed_from_u64(seed);
285
286        let (master_key, master_private_key) =
287            IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng(
288                0,
289                &mut rng,
290                platform_version,
291            )
292            .expect("expected to get key pair");
293
294        signer.add_identity_public_key(master_key.clone(), master_private_key);
295
296        let (critical_public_key, private_key) =
297            IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng(
298                1,
299                &mut rng,
300                platform_version,
301            )
302            .expect("expected to get key pair");
303
304        signer.add_identity_public_key(critical_public_key.clone(), private_key);
305
306        let identity: Identity = IdentityV0 {
307            id: Identifier::random_with_rng(&mut rng),
308            public_keys: BTreeMap::from([
309                (0, master_key.clone()),
310                (1, critical_public_key.clone()),
311            ]),
312            balance: credits,
313            revision: 0,
314        }
315        .into();
316
317        (identity, signer, critical_public_key)
318    }
319
320    pub(in crate::execution) fn setup_identity_return_master_key(
321        platform: &mut TempPlatform<MockCoreRPCLike>,
322        seed: u64,
323        credits: Credits,
324    ) -> (Identity, SimpleSigner, IdentityPublicKey, IdentityPublicKey) {
325        let platform_version = PlatformVersion::latest();
326        let mut signer = SimpleSigner::default();
327
328        let mut rng = StdRng::seed_from_u64(seed);
329
330        let (master_key, master_private_key) =
331            IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng(
332                0,
333                &mut rng,
334                platform_version,
335            )
336            .expect("expected to get key pair");
337
338        signer.add_identity_public_key(master_key.clone(), master_private_key);
339
340        let (critical_public_key, private_key) =
341            IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng(
342                1,
343                &mut rng,
344                platform_version,
345            )
346            .expect("expected to get key pair");
347
348        signer.add_identity_public_key(critical_public_key.clone(), private_key);
349
350        let identity: Identity = IdentityV0 {
351            id: Identifier::random_with_rng(&mut rng),
352            public_keys: BTreeMap::from([
353                (0, master_key.clone()),
354                (1, critical_public_key.clone()),
355            ]),
356            balance: credits,
357            revision: 0,
358        }
359        .into();
360
361        // We just add this identity to the system first
362
363        platform
364            .drive
365            .add_new_identity(
366                identity.clone(),
367                false,
368                &BlockInfo::default(),
369                true,
370                None,
371                platform_version,
372            )
373            .expect("expected to add a new identity");
374
375        (identity, signer, critical_public_key, master_key)
376    }
377
378    #[allow(clippy::too_many_arguments)]
379    pub(crate) fn setup_add_key_to_identity(
380        platform: &mut TempPlatform<MockCoreRPCLike>,
381        identity: &mut Identity,
382        signer: &mut SimpleSigner,
383        seed: u64,
384        key_id: KeyID,
385        purpose: Purpose,
386        security_level: SecurityLevel,
387        key_type: KeyType,
388        contract_bounds: Option<ContractBounds>,
389    ) -> IdentityPublicKey {
390        let platform_version = PlatformVersion::latest();
391
392        let mut rng = StdRng::seed_from_u64(seed);
393
394        let (key, private_key) = IdentityPublicKey::random_key_with_known_attributes(
395            key_id,
396            &mut rng,
397            purpose,
398            security_level,
399            key_type,
400            contract_bounds,
401            platform_version,
402        )
403        .expect("expected to get key pair");
404
405        signer.add_identity_public_key(key.clone(), private_key);
406
407        identity.add_public_key(key.clone());
408
409        platform
410            .drive
411            .add_new_unique_keys_to_identity(
412                identity.id().to_buffer(),
413                vec![key.clone()],
414                &BlockInfo::default(),
415                true,
416                None,
417                platform_version,
418            )
419            .expect("expected to add a new key");
420
421        key
422    }
423
424    pub(in crate::execution) fn setup_identity_with_withdrawal_key_and_system_credits(
425        platform: &mut TempPlatform<MockCoreRPCLike>,
426        seed: u64,
427        withdrawal_key_type: KeyType,
428        credits: Credits,
429    ) -> (Identity, SimpleSigner, IdentityPublicKey, IdentityPublicKey) {
430        let platform_version = PlatformVersion::latest();
431        platform
432            .drive
433            .add_to_system_credits(credits, None, platform_version)
434            .expect("expected to add to system credits");
435        let mut signer = SimpleSigner::default();
436
437        let mut rng = StdRng::seed_from_u64(seed);
438
439        let (master_key, master_private_key) =
440            IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng(
441                0,
442                &mut rng,
443                platform_version,
444            )
445            .expect("expected to get key pair");
446
447        signer.add_identity_public_key(master_key.clone(), master_private_key);
448
449        let (critical_public_key, private_key) =
450            IdentityPublicKey::random_ecdsa_critical_level_authentication_key_with_rng(
451                1,
452                &mut rng,
453                platform_version,
454            )
455            .expect("expected to get key pair");
456
457        signer.add_identity_public_key(critical_public_key.clone(), private_key);
458
459        let (withdrawal_public_key, withdrawal_private_key) =
460            IdentityPublicKey::random_key_with_known_attributes(
461                2,
462                &mut rng,
463                Purpose::TRANSFER,
464                SecurityLevel::CRITICAL,
465                withdrawal_key_type,
466                None,
467                platform_version,
468            )
469            .expect("expected to get key pair");
470
471        signer.add_identity_public_key(withdrawal_public_key.clone(), withdrawal_private_key);
472
473        let identity: Identity = IdentityV0 {
474            id: Identifier::random_with_rng(&mut rng),
475            public_keys: BTreeMap::from([
476                (0, master_key.clone()),
477                (1, critical_public_key.clone()),
478                (2, withdrawal_public_key.clone()),
479            ]),
480            balance: credits,
481            revision: 0,
482        }
483        .into();
484
485        // We just add this identity to the system first
486
487        platform
488            .drive
489            .add_new_identity(
490                identity.clone(),
491                false,
492                &BlockInfo::default(),
493                true,
494                None,
495                platform_version,
496            )
497            .expect("expected to add a new identity");
498
499        (identity, signer, critical_public_key, withdrawal_public_key)
500    }
501
502    pub(in crate::execution) fn add_tokens_to_identity(
503        platform: &TempPlatform<MockCoreRPCLike>,
504        token_id: Identifier,
505        identity_id: Identifier,
506        balance_to_add: Credits,
507    ) {
508        let platform_version = PlatformVersion::latest();
509        platform
510            .drive
511            .add_to_identity_token_balance(
512                token_id.to_buffer(),
513                identity_id.to_buffer(),
514                balance_to_add,
515                &BlockInfo::default(),
516                true,
517                None,
518                platform_version,
519                None,
520            )
521            .expect("expected to add token balance to identity");
522        platform
523            .drive
524            .add_to_token_total_supply(
525                token_id.to_buffer(),
526                balance_to_add,
527                true,
528                false,
529                true,
530                &BlockInfo::default(),
531                None,
532                platform_version,
533            )
534            .expect("expected to add to total supply");
535    }
536
537    pub(in crate::execution) fn process_state_transitions(
538        platform: &TempPlatform<MockCoreRPCLike>,
539        state_transitions: &[StateTransition],
540        block_info: BlockInfo,
541        platform_state: &PlatformState,
542    ) -> (Vec<FeeResult>, ProcessedBlockFeesOutcome) {
543        let platform_version = PlatformVersion::latest();
544
545        let raw_state_transitions = state_transitions
546            .iter()
547            .map(|a| a.serialize_to_bytes().expect("expected to serialize"))
548            .collect::<Vec<_>>();
549
550        let transaction = platform.drive.grove.start_transaction();
551
552        let processing_result = platform
553            .platform
554            .process_raw_state_transitions(
555                &raw_state_transitions,
556                platform_state,
557                &block_info,
558                &transaction,
559                platform_version,
560                false,
561                None,
562            )
563            .expect("expected to process state transition");
564
565        let fee_results = processing_result.execution_results().iter().map(|result| {
566            let fee_result = expect_match!(result, StateTransitionExecutionResult::SuccessfulExecution{ fee_result, .. } => fee_result);
567            fee_result.clone()
568        }).collect();
569
570        // while we have the state transitions executed, we now need to process the block fees
571        let block_fees_v0: BlockFeesV0 = processing_result.aggregated_fees().clone().into();
572
573        let block_execution_context = BlockExecutionContext::V0(BlockExecutionContextV0 {
574            block_state_info: BlockStateInfo::V0(BlockStateInfoV0 {
575                height: block_info.height,
576                round: 0,
577                block_time_ms: block_info.time_ms,
578                previous_block_time_ms: platform_state.last_committed_block_time_ms(),
579                proposer_pro_tx_hash: Default::default(),
580                core_chain_locked_height: 0,
581                block_hash: None,
582                app_hash: None,
583            }),
584            epoch_info: EpochInfo::V0(EpochInfoV0::default()),
585            unsigned_withdrawal_transactions: Default::default(),
586            block_address_balance_changes: Default::default(),
587            block_platform_state: platform_state.clone(),
588            proposer_results: None,
589        });
590
591        // Process fees
592        let processed_block_fees = platform
593            .process_block_fees_and_validate_sum_trees(
594                &block_execution_context,
595                block_fees_v0.into(),
596                &transaction,
597                platform_version,
598            )
599            .expect("expected to process block fees");
600
601        platform
602            .drive
603            .grove
604            .commit_transaction(transaction)
605            .unwrap()
606            .expect("expected to commit");
607
608        (fee_results, processed_block_fees)
609    }
610
611    pub(in crate::execution) fn fetch_expected_identity_balance(
612        platform: &TempPlatform<MockCoreRPCLike>,
613        identity_id: Identifier,
614        platform_version: &PlatformVersion,
615        expected_balance: Credits,
616    ) {
617        assert_eq!(
618            expected_balance,
619            platform
620                .drive
621                .fetch_identity_balance(identity_id.to_buffer(), None, platform_version)
622                .expect("expected to be able to fetch balance")
623                .expect("expected a balance")
624        );
625    }
626
627    pub(in crate::execution) fn setup_masternode_owner_identity(
628        platform: &mut TempPlatform<MockCoreRPCLike>,
629        seed: u64,
630        credits: Credits,
631        platform_version: &PlatformVersion,
632    ) -> (Identity, SimpleSigner, IdentityPublicKey, IdentityPublicKey) {
633        let mut signer = SimpleSigner::default();
634
635        platform
636            .drive
637            .add_to_system_credits(credits, None, platform_version)
638            .expect("expected to add to system credits");
639
640        let mut rng = StdRng::seed_from_u64(seed);
641
642        let (transfer_key, transfer_private_key) =
643            IdentityPublicKey::random_masternode_transfer_key_with_rng(
644                0,
645                &mut rng,
646                platform_version,
647            )
648            .expect("expected to get key pair");
649
650        let (owner_key, owner_private_key) =
651            IdentityPublicKey::random_masternode_owner_key_with_rng(1, &mut rng, platform_version)
652                .expect("expected to get key pair");
653
654        let owner_address = owner_key
655            .public_key_hash()
656            .expect("expected a public key hash");
657
658        let payout_address = transfer_key
659            .public_key_hash()
660            .expect("expected a public key hash");
661
662        signer.add_identity_public_key(transfer_key.clone(), transfer_private_key);
663        signer.add_identity_public_key(owner_key.clone(), owner_private_key);
664
665        let pro_tx_hash_bytes: [u8; 32] = rng.gen();
666
667        let identity: Identity = IdentityV0 {
668            id: pro_tx_hash_bytes.into(),
669            public_keys: BTreeMap::from([(0, transfer_key.clone()), (1, owner_key.clone())]),
670            balance: credits,
671            revision: 0,
672        }
673        .into();
674
675        // We just add this identity to the system first
676
677        platform
678            .drive
679            .add_new_identity(
680                identity.clone(),
681                true,
682                &BlockInfo::default(),
683                true,
684                None,
685                platform_version,
686            )
687            .expect("expected to add a new identity");
688
689        let mut platform_state = platform.state.load().clone().deref().clone();
690
691        let pro_tx_hash = ProTxHash::from_byte_array(pro_tx_hash_bytes);
692
693        let random_ip = Ipv4Addr::new(
694            rng.gen_range(0..255),
695            rng.gen_range(0..255),
696            rng.gen_range(0..255),
697            rng.gen_range(0..255),
698        );
699
700        platform_state.full_masternode_list_mut().insert(
701            pro_tx_hash,
702            MasternodeListItem {
703                node_type: MasternodeType::Regular,
704                pro_tx_hash,
705                collateral_hash: Txid::from_byte_array(rng.gen()),
706                collateral_index: 0,
707                collateral_address: rng.gen(),
708                operator_reward: 0.0,
709                state: DMNState {
710                    service: SocketAddr::new(IpAddr::V4(random_ip), 19999),
711                    registered_height: 0,
712                    pose_revived_height: None,
713                    pose_ban_height: None,
714                    revocation_reason: 0,
715                    owner_address,
716                    voting_address: rng.gen(),
717                    payout_address,
718                    pub_key_operator: vec![],
719                    operator_payout_address: None,
720                    platform_node_id: None,
721                    platform_p2p_port: None,
722                    platform_http_port: None,
723                },
724            },
725        );
726
727        platform.state.store(Arc::new(platform_state));
728
729        (identity, signer, owner_key, transfer_key)
730    }
731
732    pub(in crate::execution) fn setup_masternode_voting_identity(
733        platform: &mut TempPlatform<MockCoreRPCLike>,
734        seed: u64,
735        platform_version: &PlatformVersion,
736    ) -> (Identifier, Identity, SimpleSigner, IdentityPublicKey) {
737        let mut signer = SimpleSigner::default();
738
739        let mut rng = StdRng::seed_from_u64(seed);
740
741        let (voting_key, voting_private_key) =
742            IdentityPublicKey::random_voting_key_with_rng(0, &mut rng, platform_version)
743                .expect("expected to get key pair");
744
745        signer.add_identity_public_key(voting_key.clone(), voting_private_key);
746
747        let pro_tx_hash_bytes: [u8; 32] = rng.gen();
748
749        let voting_address = voting_key
750            .public_key_hash()
751            .expect("expected a public key hash");
752
753        let voter_identifier =
754            Identifier::create_voter_identifier(&pro_tx_hash_bytes, &voting_address);
755
756        let identity: Identity = IdentityV0 {
757            id: voter_identifier,
758            public_keys: BTreeMap::from([(0, voting_key.clone())]),
759            balance: 0,
760            revision: 0,
761        }
762        .into();
763
764        // We just add this identity to the system first
765
766        platform
767            .drive
768            .add_new_identity(
769                identity.clone(),
770                true,
771                &BlockInfo::default(),
772                true,
773                None,
774                platform_version,
775            )
776            .expect("expected to add a new identity");
777
778        let mut platform_state = platform.state.load().clone().deref().clone();
779
780        let pro_tx_hash = ProTxHash::from_byte_array(pro_tx_hash_bytes);
781
782        let random_ip = Ipv4Addr::new(
783            rng.gen_range(0..255),
784            rng.gen_range(0..255),
785            rng.gen_range(0..255),
786            rng.gen_range(0..255),
787        );
788
789        platform_state.full_masternode_list_mut().insert(
790            pro_tx_hash,
791            MasternodeListItem {
792                node_type: MasternodeType::Regular,
793                pro_tx_hash,
794                collateral_hash: Txid::from_byte_array(rng.gen()),
795                collateral_index: 0,
796                collateral_address: rng.gen(),
797                operator_reward: 0.0,
798                state: DMNState {
799                    service: SocketAddr::new(IpAddr::V4(random_ip), 19999),
800                    registered_height: 0,
801                    pose_revived_height: None,
802                    pose_ban_height: None,
803                    revocation_reason: 0,
804                    owner_address: rng.gen(),
805                    voting_address,
806                    payout_address: rng.gen(),
807                    pub_key_operator: vec![],
808                    operator_payout_address: None,
809                    platform_node_id: None,
810                    platform_p2p_port: None,
811                    platform_http_port: None,
812                },
813            },
814        );
815
816        platform.state.store(Arc::new(platform_state));
817
818        (pro_tx_hash_bytes.into(), identity, signer, voting_key)
819    }
820
821    pub(in crate::execution) fn take_down_masternode_identities(
822        platform: &mut TempPlatform<MockCoreRPCLike>,
823        masternode_identities: &Vec<Identifier>,
824    ) {
825        let mut platform_state = platform.state.load().clone().deref().clone();
826
827        let list = platform_state.full_masternode_list_mut();
828
829        for masternode_identifiers in masternode_identities {
830            let pro_tx_hash = ProTxHash::from_byte_array(masternode_identifiers.to_buffer());
831
832            list.remove(&pro_tx_hash);
833        }
834
835        platform.state.store(Arc::new(platform_state));
836    }
837
838    #[allow(dead_code)]
839    pub(in crate::execution) enum IdentityTestInfo<'a> {
840        Given {
841            identity: &'a Identity,
842            signer: &'a SimpleSigner,
843            public_key: &'a IdentityPublicKey,
844            identity_nonce: IdentityNonce,
845        },
846        UseSeed(u64),
847        UseRng(&'a mut StdRng),
848    }
849
850    pub(in crate::execution) async fn register_contract_from_bytes(
851        platform: &mut TempPlatform<MockCoreRPCLike>,
852        platform_state: &PlatformState,
853        contract_bytes: Vec<u8>,
854        identity_info: IdentityTestInfo<'_>,
855        platform_version: &PlatformVersion,
856    ) -> DataContract {
857        // Deserialize the data contract from bytes
858        let mut data_contract =
859            DataContract::versioned_deserialize_untrusted(&contract_bytes, false, platform_version)
860                .expect("expected to deserialize data contract");
861
862        // Get identity info based on the enum variant
863        let (identity_cow, signer_cow, key_cow, nonce) = match identity_info {
864            IdentityTestInfo::Given {
865                identity,
866                signer,
867                public_key,
868                identity_nonce,
869            } => (
870                Cow::Borrowed(identity),
871                Cow::Borrowed(signer),
872                Cow::Borrowed(public_key),
873                identity_nonce,
874            ),
875            IdentityTestInfo::UseSeed(seed) => {
876                let (identity, signer, key) = setup_identity(platform, seed, dash_to_credits!(1));
877                (Cow::Owned(identity), Cow::Owned(signer), Cow::Owned(key), 1)
878            }
879            IdentityTestInfo::UseRng(rng) => {
880                let seed = rng.gen();
881                let (identity, signer, key) = setup_identity(platform, seed, dash_to_credits!(1));
882                (Cow::Owned(identity), Cow::Owned(signer), Cow::Owned(key), 1)
883            }
884        };
885
886        let contract_id = DataContract::generate_data_contract_id_v0(identity_cow.id(), nonce);
887
888        data_contract.set_id(contract_id);
889
890        // Create and sign the data contract create transition
891        let state_transition = DataContractCreateTransition::new_from_data_contract(
892            data_contract.clone(),
893            nonce,
894            &identity_cow.as_ref().clone().into_partial_identity_info(),
895            key_cow.id(),
896            signer_cow.as_ref(),
897            platform_version,
898            None,
899        )
900        .await
901        .expect("expected to create and sign data contract create transition");
902
903        // Serialize the state transition
904        let state_transition_bytes = state_transition
905            .serialize_to_bytes()
906            .expect("expected to serialize state transition");
907
908        let transaction = platform.drive.grove.start_transaction();
909
910        let processing_result = platform
911            .platform
912            .process_raw_state_transitions(
913                &[state_transition_bytes],
914                platform_state,
915                &BlockInfo::default(),
916                &transaction,
917                platform_version,
918                false,
919                None,
920            )
921            .expect("expected to process state transition");
922
923        platform
924            .drive
925            .grove
926            .commit_transaction(transaction)
927            .unwrap()
928            .expect("expected to commit transaction");
929
930        let execution_result = processing_result.into_execution_results().remove(0);
931        assert_matches!(execution_result, SuccessfulExecution { .. });
932
933        data_contract
934    }
935
936    pub(in crate::execution) async fn create_dpns_name_contest_give_key_info(
937        platform: &mut TempPlatform<MockCoreRPCLike>,
938        platform_state: &PlatformState,
939        seed: u64,
940        name: &str,
941        platform_version: &PlatformVersion,
942    ) -> (
943        (
944            Identity,
945            SimpleSigner,
946            IdentityPublicKey,
947            (Document, Bytes32),
948            (Document, Bytes32),
949        ),
950        (
951            Identity,
952            SimpleSigner,
953            IdentityPublicKey,
954            (Document, Bytes32),
955            (Document, Bytes32),
956        ),
957        Arc<DataContract>,
958    ) {
959        let mut rng = StdRng::seed_from_u64(seed);
960
961        let identity_1_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
962
963        let identity_2_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
964
965        // Flip them if needed so identity 1 id is always smaller than identity 2 id
966        let (identity_1_info, identity_2_info) = if identity_1_info.0.id() < identity_2_info.0.id()
967        {
968            (identity_1_info, identity_2_info)
969        } else {
970            (identity_2_info, identity_1_info)
971        };
972
973        let ((preorder_document_1, document_1), (preorder_document_2, document_2), dpns_contract) =
974            create_dpns_name_contest_on_identities(
975                platform,
976                &identity_1_info,
977                &identity_2_info,
978                platform_state,
979                rng,
980                name,
981                None,
982                false,
983                platform_version,
984            )
985            .await;
986
987        let (identity_1, signer_1, identity_key_1) = identity_1_info;
988
989        let (identity_2, signer_2, identity_key_2) = identity_2_info;
990
991        (
992            (
993                identity_1,
994                signer_1,
995                identity_key_1,
996                preorder_document_1,
997                document_1,
998            ),
999            (
1000                identity_2,
1001                signer_2,
1002                identity_key_2,
1003                preorder_document_2,
1004                document_2,
1005            ),
1006            dpns_contract,
1007        )
1008    }
1009
1010    pub(in crate::execution) async fn create_dpns_identity_name_contest(
1011        platform: &mut TempPlatform<MockCoreRPCLike>,
1012        platform_state: &PlatformState,
1013        seed: u64,
1014        name: &str,
1015        platform_version: &PlatformVersion,
1016    ) -> (Identity, Identity, Arc<DataContract>) {
1017        let mut rng = StdRng::seed_from_u64(seed);
1018
1019        let identity_1_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
1020
1021        let identity_2_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
1022
1023        // Flip them if needed so identity 1 id is always smaller than identity 2 id
1024        let (identity_1_info, identity_2_info) = if identity_1_info.0.id() < identity_2_info.0.id()
1025        {
1026            (identity_1_info, identity_2_info)
1027        } else {
1028            (identity_2_info, identity_1_info)
1029        };
1030
1031        let (_, _, dpns_contract) = create_dpns_name_contest_on_identities(
1032            platform,
1033            &identity_1_info,
1034            &identity_2_info,
1035            platform_state,
1036            rng,
1037            name,
1038            None,
1039            false,
1040            platform_version,
1041        )
1042        .await;
1043        (identity_1_info.0, identity_2_info.0, dpns_contract)
1044    }
1045
1046    /// This can be useful if we already created the identities and we reuse the seed
1047    pub(in crate::execution) async fn create_dpns_identity_name_contest_skip_creating_identities(
1048        platform: &mut TempPlatform<MockCoreRPCLike>,
1049        platform_state: &PlatformState,
1050        seed: u64,
1051        name: &str,
1052        nonce_offset: Option<IdentityNonce>,
1053        platform_version: &PlatformVersion,
1054    ) -> (Identity, Identity, Arc<DataContract>) {
1055        let mut rng = StdRng::seed_from_u64(seed);
1056
1057        let identity_1_info = setup_identity_without_adding_it(rng.gen(), dash_to_credits!(0.5));
1058
1059        let identity_2_info = setup_identity_without_adding_it(rng.gen(), dash_to_credits!(0.5));
1060
1061        // Flip them if needed so identity 1 id is always smaller than identity 2 id
1062        let (identity_1_info, identity_2_info) = if identity_1_info.0.id() < identity_2_info.0.id()
1063        {
1064            (identity_1_info, identity_2_info)
1065        } else {
1066            (identity_2_info, identity_1_info)
1067        };
1068
1069        let (_, _, dpns_contract) = create_dpns_name_contest_on_identities(
1070            platform,
1071            &identity_1_info,
1072            &identity_2_info,
1073            platform_state,
1074            rng,
1075            name,
1076            nonce_offset,
1077            true, //we should also skip preorder
1078            platform_version,
1079        )
1080        .await;
1081        (identity_1_info.0, identity_2_info.0, dpns_contract)
1082    }
1083
1084    pub(in crate::execution) async fn create_dpns_contract_name_contest(
1085        platform: &mut TempPlatform<MockCoreRPCLike>,
1086        platform_state: &PlatformState,
1087        seed: u64,
1088        name: &str,
1089        platform_version: &PlatformVersion,
1090    ) -> (Identity, Identity, DataContract) {
1091        let mut rng = StdRng::seed_from_u64(seed);
1092
1093        let identity_1_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
1094
1095        let identity_2_info = setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
1096
1097        // Flip them if needed so identity 1 id is always smaller than identity 2 id
1098        let (identity_1_info, identity_2_info) = if identity_1_info.0.id() < identity_2_info.0.id()
1099        {
1100            (identity_1_info, identity_2_info)
1101        } else {
1102            (identity_2_info, identity_1_info)
1103        };
1104
1105        let dashpay_contract = setup_contract(
1106            &platform.drive,
1107            "tests/supporting_files/contract/dashpay/dashpay-contract-all-mutable.json",
1108            None,
1109            None,
1110            None::<fn(&mut DataContract)>,
1111            None,
1112            None,
1113        );
1114
1115        let card_game = setup_contract(
1116            &platform.drive,
1117            "tests/supporting_files/contract/crypto-card-game/crypto-card-game-direct-purchase.json",
1118            None,
1119            None,
1120            None::<fn(&mut DataContract)>,
1121            None,
1122            None,
1123        );
1124
1125        let (_, _, dpns_contract) = create_dpns_name_contest_on_identities_for_contract_records(
1126            platform,
1127            &identity_1_info,
1128            &identity_2_info,
1129            &dashpay_contract,
1130            &card_game,
1131            platform_state,
1132            rng,
1133            name,
1134            platform_version,
1135        )
1136        .await;
1137        (identity_1_info.0, identity_2_info.0, dpns_contract)
1138    }
1139
1140    #[allow(clippy::too_many_arguments)]
1141    async fn create_dpns_name_contest_on_identities(
1142        platform: &mut TempPlatform<MockCoreRPCLike>,
1143        identity_1: &(Identity, SimpleSigner, IdentityPublicKey),
1144        identity_2: &(Identity, SimpleSigner, IdentityPublicKey),
1145        platform_state: &PlatformState,
1146        mut rng: StdRng,
1147        name: &str,
1148        nonce_offset: Option<IdentityNonce>,
1149        skip_preorder: bool,
1150        platform_version: &PlatformVersion,
1151    ) -> (
1152        ((Document, Bytes32), (Document, Bytes32)),
1153        ((Document, Bytes32), (Document, Bytes32)),
1154        Arc<DataContract>,
1155    ) {
1156        let (identity_1, signer_1, key_1) = identity_1;
1157
1158        let (identity_2, signer_2, key_2) = identity_2;
1159
1160        let dpns = platform
1161            .drive
1162            .cache
1163            .system_data_contracts
1164            .load_dpns(platform_version)
1165            .expect("expected the dpns system contract");
1166        let dpns_contract = dpns.clone();
1167
1168        let preorder = dpns_contract
1169            .document_type_for_name("preorder")
1170            .expect("expected a profile document type");
1171
1172        assert!(!preorder.documents_mutable());
1173        assert!(preorder.documents_can_be_deleted());
1174        assert!(!preorder.documents_transferable().is_transferable());
1175
1176        let domain = dpns_contract
1177            .document_type_for_name("domain")
1178            .expect("expected a profile document type");
1179
1180        assert!(!domain.documents_mutable());
1181        // Deletion is disabled with data trigger
1182        assert!(domain.documents_can_be_deleted());
1183        assert!(domain.documents_transferable().is_transferable());
1184
1185        let entropy = Bytes32::random_with_rng(&mut rng);
1186
1187        let mut preorder_document_1 = preorder
1188            .random_document_with_identifier_and_entropy(
1189                &mut rng,
1190                identity_1.id(),
1191                entropy,
1192                DocumentFieldFillType::FillIfNotRequired,
1193                DocumentFieldFillSize::AnyDocumentFillSize,
1194                platform_version,
1195            )
1196            .expect("expected a random document");
1197
1198        let mut preorder_document_2 = preorder
1199            .random_document_with_identifier_and_entropy(
1200                &mut rng,
1201                identity_2.id(),
1202                entropy,
1203                DocumentFieldFillType::FillIfNotRequired,
1204                DocumentFieldFillSize::AnyDocumentFillSize,
1205                platform_version,
1206            )
1207            .expect("expected a random document");
1208
1209        let mut document_1 = domain
1210            .random_document_with_identifier_and_entropy(
1211                &mut rng,
1212                identity_1.id(),
1213                entropy,
1214                DocumentFieldFillType::FillIfNotRequired,
1215                DocumentFieldFillSize::AnyDocumentFillSize,
1216                platform_version,
1217            )
1218            .expect("expected a random document");
1219
1220        let mut document_2 = domain
1221            .random_document_with_identifier_and_entropy(
1222                &mut rng,
1223                identity_2.id(),
1224                entropy,
1225                DocumentFieldFillType::FillIfNotRequired,
1226                DocumentFieldFillSize::AnyDocumentFillSize,
1227                platform_version,
1228            )
1229            .expect("expected a random document");
1230
1231        document_1.set("parentDomainName", "dash".into());
1232        document_1.set("normalizedParentDomainName", "dash".into());
1233        document_1.set("label", name.into());
1234        document_1.set(
1235            "normalizedLabel",
1236            convert_to_homograph_safe_chars(name).into(),
1237        );
1238        document_1.set("records.identity", document_1.owner_id().into());
1239        document_1.set("subdomainRules.allowSubdomains", false.into());
1240
1241        document_2.set("parentDomainName", "dash".into());
1242        document_2.set("normalizedParentDomainName", "dash".into());
1243        document_2.set("label", name.into());
1244        document_2.set(
1245            "normalizedLabel",
1246            convert_to_homograph_safe_chars(name).into(),
1247        );
1248        document_2.set("records.identity", document_2.owner_id().into());
1249        document_2.set("subdomainRules.allowSubdomains", false.into());
1250
1251        let salt_1: [u8; 32] = rng.gen();
1252        let salt_2: [u8; 32] = rng.gen();
1253
1254        let mut salted_domain_buffer_1: Vec<u8> = vec![];
1255        salted_domain_buffer_1.extend(salt_1);
1256        salted_domain_buffer_1.extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
1257
1258        let salted_domain_hash_1 = hash_double(salted_domain_buffer_1);
1259
1260        let mut salted_domain_buffer_2: Vec<u8> = vec![];
1261        salted_domain_buffer_2.extend(salt_2);
1262        salted_domain_buffer_2.extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
1263
1264        let salted_domain_hash_2 = hash_double(salted_domain_buffer_2);
1265
1266        preorder_document_1.set("saltedDomainHash", salted_domain_hash_1.into());
1267        preorder_document_2.set("saltedDomainHash", salted_domain_hash_2.into());
1268
1269        document_1.set("preorderSalt", salt_1.into());
1270        document_2.set("preorderSalt", salt_2.into());
1271
1272        let documents_batch_create_preorder_transition_1 =
1273            BatchTransition::new_document_creation_transition_from_document(
1274                preorder_document_1.clone(),
1275                preorder,
1276                entropy.0,
1277                key_1,
1278                2 + nonce_offset.unwrap_or_default(),
1279                0,
1280                None,
1281                signer_1,
1282                platform_version,
1283                None,
1284            )
1285            .await
1286            .expect("expect to create documents batch transition");
1287
1288        let documents_batch_create_serialized_preorder_transition_1 =
1289            documents_batch_create_preorder_transition_1
1290                .serialize_to_bytes()
1291                .expect("expected documents batch serialized state transition");
1292
1293        let documents_batch_create_preorder_transition_2 =
1294            BatchTransition::new_document_creation_transition_from_document(
1295                preorder_document_2.clone(),
1296                preorder,
1297                entropy.0,
1298                key_2,
1299                2 + nonce_offset.unwrap_or_default(),
1300                0,
1301                None,
1302                signer_2,
1303                platform_version,
1304                None,
1305            )
1306            .await
1307            .expect("expect to create documents batch transition");
1308
1309        let documents_batch_create_serialized_preorder_transition_2 =
1310            documents_batch_create_preorder_transition_2
1311                .serialize_to_bytes()
1312                .expect("expected documents batch serialized state transition");
1313
1314        let documents_batch_create_transition_1 =
1315            BatchTransition::new_document_creation_transition_from_document(
1316                document_1.clone(),
1317                domain,
1318                entropy.0,
1319                key_1,
1320                3 + nonce_offset.unwrap_or_default(),
1321                0,
1322                None,
1323                signer_1,
1324                platform_version,
1325                None,
1326            )
1327            .await
1328            .expect("expect to create documents batch transition");
1329
1330        let documents_batch_create_serialized_transition_1 = documents_batch_create_transition_1
1331            .serialize_to_bytes()
1332            .expect("expected documents batch serialized state transition");
1333
1334        let documents_batch_create_transition_2 =
1335            BatchTransition::new_document_creation_transition_from_document(
1336                document_2.clone(),
1337                domain,
1338                entropy.0,
1339                key_2,
1340                3 + nonce_offset.unwrap_or_default(),
1341                0,
1342                None,
1343                signer_2,
1344                platform_version,
1345                None,
1346            )
1347            .await
1348            .expect("expect to create documents batch transition");
1349
1350        let documents_batch_create_serialized_transition_2 = documents_batch_create_transition_2
1351            .serialize_to_bytes()
1352            .expect("expected documents batch serialized state transition");
1353
1354        if !skip_preorder {
1355            let transaction = platform.drive.grove.start_transaction();
1356
1357            let processing_result = platform
1358                .platform
1359                .process_raw_state_transitions(
1360                    &[
1361                        documents_batch_create_serialized_preorder_transition_1.clone(),
1362                        documents_batch_create_serialized_preorder_transition_2.clone(),
1363                    ],
1364                    platform_state,
1365                    &BlockInfo::default_with_time(
1366                        platform_state
1367                            .last_committed_block_time_ms()
1368                            .unwrap_or_default()
1369                            + 3000,
1370                    ),
1371                    &transaction,
1372                    platform_version,
1373                    false,
1374                    None,
1375                )
1376                .expect("expected to process state transition");
1377
1378            platform
1379                .drive
1380                .grove
1381                .commit_transaction(transaction)
1382                .unwrap()
1383                .expect("expected to commit transaction");
1384
1385            let successful_count = processing_result
1386                .execution_results()
1387                .iter()
1388                .filter(|result| {
1389                    assert_matches!(
1390                        result,
1391                        StateTransitionExecutionResult::SuccessfulExecution { .. }
1392                    );
1393                    true
1394                })
1395                .count();
1396
1397            assert_eq!(successful_count, 2);
1398        }
1399
1400        let transaction = platform.drive.grove.start_transaction();
1401
1402        let processing_result = platform
1403            .platform
1404            .process_raw_state_transitions(
1405                &[
1406                    documents_batch_create_serialized_transition_1.clone(),
1407                    documents_batch_create_serialized_transition_2.clone(),
1408                ],
1409                platform_state,
1410                &BlockInfo::default_with_time(
1411                    platform_state
1412                        .last_committed_block_time_ms()
1413                        .unwrap_or_default()
1414                        + 3000,
1415                ),
1416                &transaction,
1417                platform_version,
1418                false,
1419                None,
1420            )
1421            .expect("expected to process state transition");
1422
1423        platform
1424            .drive
1425            .grove
1426            .commit_transaction(transaction)
1427            .unwrap()
1428            .expect("expected to commit transaction");
1429
1430        let successful_count = processing_result
1431            .execution_results()
1432            .iter()
1433            .filter(|result| {
1434                assert_matches!(
1435                    result,
1436                    StateTransitionExecutionResult::SuccessfulExecution { .. }
1437                );
1438                true
1439            })
1440            .count();
1441
1442        assert_eq!(successful_count, 2);
1443        (
1444            ((preorder_document_1, entropy), (document_1, entropy)),
1445            ((preorder_document_2, entropy), (document_2, entropy)),
1446            dpns_contract,
1447        )
1448    }
1449
1450    #[allow(clippy::too_many_arguments)]
1451    async fn create_dpns_name_contest_on_identities_for_contract_records(
1452        platform: &mut TempPlatform<MockCoreRPCLike>,
1453        identity_1: &(Identity, SimpleSigner, IdentityPublicKey),
1454        identity_2: &(Identity, SimpleSigner, IdentityPublicKey),
1455        contract_1: &DataContract,
1456        contract_2: &DataContract,
1457        platform_state: &PlatformState,
1458        mut rng: StdRng,
1459        name: &str,
1460        platform_version: &PlatformVersion,
1461    ) -> (
1462        ((Document, Bytes32), (Document, Bytes32)),
1463        ((Document, Bytes32), (Document, Bytes32)),
1464        DataContract,
1465    ) {
1466        let (identity_1, signer_1, key_1) = identity_1;
1467
1468        let (identity_2, signer_2, key_2) = identity_2;
1469
1470        let dpns_contract = setup_contract(
1471            &platform.drive,
1472            "tests/supporting_files/contract/dpns/dpns-contract-contested-unique-index-with-contract-id.json",
1473            None,
1474            None,
1475            None::<fn(&mut DataContract)>,
1476            None,
1477            None,
1478        );
1479
1480        let preorder = dpns_contract
1481            .document_type_for_name("preorder")
1482            .expect("expected a profile document type");
1483
1484        assert!(!preorder.documents_mutable());
1485        assert!(preorder.documents_can_be_deleted());
1486        assert!(!preorder.documents_transferable().is_transferable());
1487
1488        let domain = dpns_contract
1489            .document_type_for_name("domain")
1490            .expect("expected a profile document type");
1491
1492        assert!(!domain.documents_mutable());
1493        // Deletion is disabled with data trigger
1494        assert!(domain.documents_can_be_deleted());
1495        assert!(domain.documents_transferable().is_transferable());
1496
1497        let entropy = Bytes32::random_with_rng(&mut rng);
1498
1499        let mut preorder_document_1 = preorder
1500            .random_document_with_identifier_and_entropy(
1501                &mut rng,
1502                identity_1.id(),
1503                entropy,
1504                DocumentFieldFillType::FillIfNotRequired,
1505                DocumentFieldFillSize::AnyDocumentFillSize,
1506                platform_version,
1507            )
1508            .expect("expected a random document");
1509
1510        let mut preorder_document_2 = preorder
1511            .random_document_with_identifier_and_entropy(
1512                &mut rng,
1513                identity_2.id(),
1514                entropy,
1515                DocumentFieldFillType::FillIfNotRequired,
1516                DocumentFieldFillSize::AnyDocumentFillSize,
1517                platform_version,
1518            )
1519            .expect("expected a random document");
1520
1521        let mut document_1 = domain
1522            .random_document_with_identifier_and_entropy(
1523                &mut rng,
1524                identity_1.id(),
1525                entropy,
1526                DocumentFieldFillType::FillIfNotRequired,
1527                DocumentFieldFillSize::AnyDocumentFillSize,
1528                platform_version,
1529            )
1530            .expect("expected a random document");
1531
1532        let mut document_2 = domain
1533            .random_document_with_identifier_and_entropy(
1534                &mut rng,
1535                identity_2.id(),
1536                entropy,
1537                DocumentFieldFillType::FillIfNotRequired,
1538                DocumentFieldFillSize::AnyDocumentFillSize,
1539                platform_version,
1540            )
1541            .expect("expected a random document");
1542
1543        document_1.set("parentDomainName", "dash".into());
1544        document_1.set("normalizedParentDomainName", "dash".into());
1545        document_1.set("label", name.into());
1546        document_1.set(
1547            "normalizedLabel",
1548            convert_to_homograph_safe_chars(name).into(),
1549        );
1550        document_1.remove("records.identity");
1551        document_1.set("records.contract", contract_1.id().into());
1552        document_1.set("subdomainRules.allowSubdomains", false.into());
1553
1554        document_2.set("parentDomainName", "dash".into());
1555        document_2.set("normalizedParentDomainName", "dash".into());
1556        document_2.set("label", name.into());
1557        document_2.set(
1558            "normalizedLabel",
1559            convert_to_homograph_safe_chars(name).into(),
1560        );
1561        document_2.remove("records.identity");
1562        document_2.set("records.contract", contract_2.id().into());
1563        document_2.set("subdomainRules.allowSubdomains", false.into());
1564
1565        let salt_1: [u8; 32] = rng.gen();
1566        let salt_2: [u8; 32] = rng.gen();
1567
1568        let mut salted_domain_buffer_1: Vec<u8> = vec![];
1569        salted_domain_buffer_1.extend(salt_1);
1570        salted_domain_buffer_1.extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
1571
1572        let salted_domain_hash_1 = hash_double(salted_domain_buffer_1);
1573
1574        let mut salted_domain_buffer_2: Vec<u8> = vec![];
1575        salted_domain_buffer_2.extend(salt_2);
1576        salted_domain_buffer_2.extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
1577
1578        let salted_domain_hash_2 = hash_double(salted_domain_buffer_2);
1579
1580        preorder_document_1.set("saltedDomainHash", salted_domain_hash_1.into());
1581        preorder_document_2.set("saltedDomainHash", salted_domain_hash_2.into());
1582
1583        document_1.set("preorderSalt", salt_1.into());
1584        document_2.set("preorderSalt", salt_2.into());
1585
1586        let documents_batch_create_preorder_transition_1 =
1587            BatchTransition::new_document_creation_transition_from_document(
1588                preorder_document_1.clone(),
1589                preorder,
1590                entropy.0,
1591                key_1,
1592                2,
1593                0,
1594                None,
1595                signer_1,
1596                platform_version,
1597                None,
1598            )
1599            .await
1600            .expect("expect to create documents batch transition");
1601
1602        let documents_batch_create_serialized_preorder_transition_1 =
1603            documents_batch_create_preorder_transition_1
1604                .serialize_to_bytes()
1605                .expect("expected documents batch serialized state transition");
1606
1607        let documents_batch_create_preorder_transition_2 =
1608            BatchTransition::new_document_creation_transition_from_document(
1609                preorder_document_2.clone(),
1610                preorder,
1611                entropy.0,
1612                key_2,
1613                2,
1614                0,
1615                None,
1616                signer_2,
1617                platform_version,
1618                None,
1619            )
1620            .await
1621            .expect("expect to create documents batch transition");
1622
1623        let documents_batch_create_serialized_preorder_transition_2 =
1624            documents_batch_create_preorder_transition_2
1625                .serialize_to_bytes()
1626                .expect("expected documents batch serialized state transition");
1627
1628        let documents_batch_create_transition_1 =
1629            BatchTransition::new_document_creation_transition_from_document(
1630                document_1.clone(),
1631                domain,
1632                entropy.0,
1633                key_1,
1634                3,
1635                0,
1636                None,
1637                signer_1,
1638                platform_version,
1639                None,
1640            )
1641            .await
1642            .expect("expect to create documents batch transition");
1643
1644        let documents_batch_create_serialized_transition_1 = documents_batch_create_transition_1
1645            .serialize_to_bytes()
1646            .expect("expected documents batch serialized state transition");
1647
1648        let documents_batch_create_transition_2 =
1649            BatchTransition::new_document_creation_transition_from_document(
1650                document_2.clone(),
1651                domain,
1652                entropy.0,
1653                key_2,
1654                3,
1655                0,
1656                None,
1657                signer_2,
1658                platform_version,
1659                None,
1660            )
1661            .await
1662            .expect("expect to create documents batch transition");
1663
1664        let documents_batch_create_serialized_transition_2 = documents_batch_create_transition_2
1665            .serialize_to_bytes()
1666            .expect("expected documents batch serialized state transition");
1667
1668        let transaction = platform.drive.grove.start_transaction();
1669
1670        let processing_result = platform
1671            .platform
1672            .process_raw_state_transitions(
1673                &[
1674                    documents_batch_create_serialized_preorder_transition_1.clone(),
1675                    documents_batch_create_serialized_preorder_transition_2.clone(),
1676                ],
1677                platform_state,
1678                &BlockInfo::default_with_time(
1679                    platform_state
1680                        .last_committed_block_time_ms()
1681                        .unwrap_or_default()
1682                        + 3000,
1683                ),
1684                &transaction,
1685                platform_version,
1686                false,
1687                None,
1688            )
1689            .expect("expected to process state transition");
1690
1691        platform
1692            .drive
1693            .grove
1694            .commit_transaction(transaction)
1695            .unwrap()
1696            .expect("expected to commit transaction");
1697
1698        assert_eq!(processing_result.valid_count(), 2);
1699
1700        let transaction = platform.drive.grove.start_transaction();
1701
1702        let processing_result = platform
1703            .platform
1704            .process_raw_state_transitions(
1705                &[
1706                    documents_batch_create_serialized_transition_1.clone(),
1707                    documents_batch_create_serialized_transition_2.clone(),
1708                ],
1709                platform_state,
1710                &BlockInfo::default_with_time(
1711                    platform_state
1712                        .last_committed_block_time_ms()
1713                        .unwrap_or_default()
1714                        + 3000,
1715                ),
1716                &transaction,
1717                platform_version,
1718                false,
1719                None,
1720            )
1721            .expect("expected to process state transition");
1722
1723        platform
1724            .drive
1725            .grove
1726            .commit_transaction(transaction)
1727            .unwrap()
1728            .expect("expected to commit transaction");
1729
1730        assert_eq!(processing_result.valid_count(), 2);
1731        (
1732            ((preorder_document_1, entropy), (document_1, entropy)),
1733            ((preorder_document_2, entropy), (document_2, entropy)),
1734            dpns_contract,
1735        )
1736    }
1737
1738    pub(in crate::execution) async fn add_contender_to_dpns_name_contest(
1739        platform: &mut TempPlatform<MockCoreRPCLike>,
1740        platform_state: &PlatformState,
1741        seed: u64,
1742        name: &str,
1743        expect_err: Option<&str>,
1744        platform_version: &PlatformVersion,
1745    ) -> Identity {
1746        let mut rng = StdRng::seed_from_u64(seed);
1747
1748        let (identity_1, signer_1, key_1) =
1749            setup_identity(platform, rng.gen(), dash_to_credits!(0.5));
1750
1751        let dpns = platform
1752            .drive
1753            .cache
1754            .system_data_contracts
1755            .load_dpns(platform_version)
1756            .expect("expected the dpns system contract");
1757        let dpns_contract = dpns.clone();
1758
1759        let preorder = dpns_contract
1760            .document_type_for_name("preorder")
1761            .expect("expected a profile document type");
1762
1763        let domain = dpns_contract
1764            .document_type_for_name("domain")
1765            .expect("expected a profile document type");
1766
1767        let entropy = Bytes32::random_with_rng(&mut rng);
1768
1769        let mut preorder_document_1 = preorder
1770            .random_document_with_identifier_and_entropy(
1771                &mut rng,
1772                identity_1.id(),
1773                entropy,
1774                DocumentFieldFillType::FillIfNotRequired,
1775                DocumentFieldFillSize::AnyDocumentFillSize,
1776                platform_version,
1777            )
1778            .expect("expected a random document");
1779
1780        let mut document_1 = domain
1781            .random_document_with_identifier_and_entropy(
1782                &mut rng,
1783                identity_1.id(),
1784                entropy,
1785                DocumentFieldFillType::FillIfNotRequired,
1786                DocumentFieldFillSize::AnyDocumentFillSize,
1787                platform_version,
1788            )
1789            .expect("expected a random document");
1790
1791        document_1.set("parentDomainName", "dash".into());
1792        document_1.set("normalizedParentDomainName", "dash".into());
1793        document_1.set("label", name.into());
1794        document_1.set(
1795            "normalizedLabel",
1796            convert_to_homograph_safe_chars(name).into(),
1797        );
1798        document_1.set("records.identity", document_1.owner_id().into());
1799        document_1.set("subdomainRules.allowSubdomains", false.into());
1800
1801        let salt_1: [u8; 32] = rng.gen();
1802
1803        let mut salted_domain_buffer_1: Vec<u8> = vec![];
1804        salted_domain_buffer_1.extend(salt_1);
1805        salted_domain_buffer_1.extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
1806
1807        let salted_domain_hash_1 = hash_double(salted_domain_buffer_1);
1808
1809        preorder_document_1.set("saltedDomainHash", salted_domain_hash_1.into());
1810
1811        document_1.set("preorderSalt", salt_1.into());
1812
1813        let documents_batch_create_preorder_transition_1 =
1814            BatchTransition::new_document_creation_transition_from_document(
1815                preorder_document_1,
1816                preorder,
1817                entropy.0,
1818                &key_1,
1819                2,
1820                0,
1821                None,
1822                &signer_1,
1823                platform_version,
1824                None,
1825            )
1826            .await
1827            .expect("expect to create documents batch transition");
1828
1829        let documents_batch_create_serialized_preorder_transition_1 =
1830            documents_batch_create_preorder_transition_1
1831                .serialize_to_bytes()
1832                .expect("expected documents batch serialized state transition");
1833
1834        let documents_batch_create_transition_1 =
1835            BatchTransition::new_document_creation_transition_from_document(
1836                document_1,
1837                domain,
1838                entropy.0,
1839                &key_1,
1840                3,
1841                0,
1842                None,
1843                &signer_1,
1844                platform_version,
1845                None,
1846            )
1847            .await
1848            .expect("expect to create documents batch transition");
1849
1850        let documents_batch_create_serialized_transition_1 = documents_batch_create_transition_1
1851            .serialize_to_bytes()
1852            .expect("expected documents batch serialized state transition");
1853
1854        let transaction = platform.drive.grove.start_transaction();
1855
1856        let processing_result = platform
1857            .platform
1858            .process_raw_state_transitions(
1859                &[documents_batch_create_serialized_preorder_transition_1.clone()],
1860                platform_state,
1861                &BlockInfo::default_with_time(
1862                    platform_state
1863                        .last_committed_block_time_ms()
1864                        .unwrap_or_default()
1865                        + 3000,
1866                ),
1867                &transaction,
1868                platform_version,
1869                false,
1870                None,
1871            )
1872            .expect("expected to process state transition");
1873
1874        platform
1875            .drive
1876            .grove
1877            .commit_transaction(transaction)
1878            .unwrap()
1879            .expect("expected to commit transaction");
1880
1881        assert_eq!(processing_result.valid_count(), 1);
1882
1883        let transaction = platform.drive.grove.start_transaction();
1884
1885        let processing_result = platform
1886            .platform
1887            .process_raw_state_transitions(
1888                &[documents_batch_create_serialized_transition_1.clone()],
1889                platform_state,
1890                &BlockInfo::default_with_time(
1891                    platform_state
1892                        .last_committed_block_time_ms()
1893                        .unwrap_or_default()
1894                        + 3000,
1895                ),
1896                &transaction,
1897                platform_version,
1898                false,
1899                None,
1900            )
1901            .expect("expected to process state transition");
1902
1903        platform
1904            .drive
1905            .grove
1906            .commit_transaction(transaction)
1907            .unwrap()
1908            .expect("expected to commit transaction");
1909
1910        if let Some(expected_err) = expect_err {
1911            let result = processing_result.into_execution_results().remove(0);
1912
1913            let StateTransitionExecutionResult::PaidConsensusError {
1914                error: consensus_error,
1915                ..
1916            } = result
1917            else {
1918                panic!("expected a paid consensus error");
1919            };
1920            assert_eq!(consensus_error.to_string(), expected_err);
1921        } else {
1922            assert_eq!(processing_result.valid_count(), 1);
1923        }
1924        identity_1
1925    }
1926
1927    pub(in crate::execution) fn verify_dpns_name_contest(
1928        platform: &mut TempPlatform<MockCoreRPCLike>,
1929        platform_state: &Guard<Arc<PlatformState>>,
1930        dpns_contract: &DataContract,
1931        identity_1: &Identity,
1932        identity_2: &Identity,
1933        name: &str,
1934        platform_version: &PlatformVersion,
1935    ) {
1936        // Now let's run a query for the vote totals
1937
1938        let domain = dpns_contract
1939            .document_type_for_name("domain")
1940            .expect("expected a profile document type");
1941
1942        let config = bincode::config::standard()
1943            .with_big_endian()
1944            .with_no_limit();
1945
1946        let dash_encoded = bincode::encode_to_vec(Value::Text("dash".to_string()), config)
1947            .expect("expected to encode the word dash");
1948
1949        let quantum_encoded =
1950            bincode::encode_to_vec(Value::Text(convert_to_homograph_safe_chars(name)), config)
1951                .expect("expected to encode the word quantum");
1952
1953        let index_name = "parentNameAndLabel".to_string();
1954
1955        let query_validation_result = platform
1956            .query_contested_resource_vote_state(
1957                GetContestedResourceVoteStateRequest {
1958                    version: Some(get_contested_resource_vote_state_request::Version::V0(
1959                        GetContestedResourceVoteStateRequestV0 {
1960                            contract_id: dpns_contract.id().to_vec(),
1961                            document_type_name: domain.name().clone(),
1962                            index_name: index_name.clone(),
1963                            index_values: vec![dash_encoded.clone(), quantum_encoded.clone()],
1964                            result_type: ResultType::DocumentsAndVoteTally as i32,
1965                            allow_include_locked_and_abstaining_vote_tally: true,
1966                            start_at_identifier_info: None,
1967                            count: None,
1968                            prove: false,
1969                        },
1970                    )),
1971                },
1972                platform_state,
1973                platform_version,
1974            )
1975            .expect("expected to execute query")
1976            .into_data()
1977            .expect("expected query to be valid");
1978
1979        let get_contested_resource_vote_state_response::Version::V0(
1980            GetContestedResourceVoteStateResponseV0 {
1981                metadata: _,
1982                result,
1983            },
1984        ) = query_validation_result.version.expect("expected a version");
1985
1986        let Some(
1987            get_contested_resource_vote_state_response_v0::Result::ContestedResourceContenders(
1988                get_contested_resource_vote_state_response_v0::ContestedResourceContenders {
1989                    contenders,
1990                    ..
1991                },
1992            ),
1993        ) = result
1994        else {
1995            panic!("expected contenders")
1996        };
1997
1998        assert_eq!(contenders.len(), 2);
1999
2000        let first_contender = contenders.first().unwrap();
2001
2002        let second_contender = contenders.last().unwrap();
2003
2004        let first_contender_document = Document::from_bytes(
2005            first_contender
2006                .document
2007                .as_ref()
2008                .expect("expected a document")
2009                .as_slice(),
2010            domain,
2011            platform_version,
2012        )
2013        .expect("expected to get document");
2014
2015        let second_contender_document = Document::from_bytes(
2016            second_contender
2017                .document
2018                .as_ref()
2019                .expect("expected a document")
2020                .as_slice(),
2021            domain,
2022            platform_version,
2023        )
2024        .expect("expected to get document");
2025
2026        assert_ne!(first_contender_document, second_contender_document);
2027
2028        assert_eq!(first_contender.identifier, identity_1.id().to_vec());
2029
2030        assert_eq!(second_contender.identifier, identity_2.id().to_vec());
2031
2032        assert_eq!(first_contender.vote_count, Some(0));
2033
2034        assert_eq!(second_contender.vote_count, Some(0));
2035
2036        let GetContestedResourceVoteStateResponse { version } = platform
2037            .query_contested_resource_vote_state(
2038                GetContestedResourceVoteStateRequest {
2039                    version: Some(get_contested_resource_vote_state_request::Version::V0(
2040                        GetContestedResourceVoteStateRequestV0 {
2041                            contract_id: dpns_contract.id().to_vec(),
2042                            document_type_name: domain.name().clone(),
2043                            index_name: "parentNameAndLabel".to_string(),
2044                            index_values: vec![dash_encoded, quantum_encoded],
2045                            result_type: ResultType::DocumentsAndVoteTally as i32,
2046                            allow_include_locked_and_abstaining_vote_tally: true,
2047                            start_at_identifier_info: None,
2048                            count: None,
2049                            prove: true,
2050                        },
2051                    )),
2052                },
2053                platform_state,
2054                platform_version,
2055            )
2056            .expect("expected to execute query")
2057            .into_data()
2058            .expect("expected query to be valid");
2059
2060        let get_contested_resource_vote_state_response::Version::V0(
2061            GetContestedResourceVoteStateResponseV0 {
2062                metadata: _,
2063                result,
2064            },
2065        ) = version.expect("expected a version");
2066
2067        let Some(get_contested_resource_vote_state_response_v0::Result::Proof(proof)) = result
2068        else {
2069            panic!("expected contenders")
2070        };
2071
2072        let resolved_contested_document_vote_poll_drive_query =
2073            ResolvedContestedDocumentVotePollDriveQuery {
2074                vote_poll: ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed {
2075                    contract: DataContractResolvedInfo::BorrowedDataContract(dpns_contract),
2076                    document_type_name: domain.name().clone(),
2077                    index_name: index_name.clone(),
2078                    index_values: vec![
2079                        Value::Text("dash".to_string()),
2080                        Value::Text(convert_to_homograph_safe_chars(name)),
2081                    ],
2082                },
2083                result_type: DocumentsAndVoteTally,
2084                offset: None,
2085                limit: None,
2086                start_at: None,
2087                allow_include_locked_and_abstaining_vote_tally: true,
2088            };
2089
2090        let (_, result) = resolved_contested_document_vote_poll_drive_query
2091            .verify_vote_poll_vote_state_proof(proof.grovedb_proof.as_ref(), platform_version)
2092            .expect("expected to verify proof");
2093
2094        let contenders = result.contenders;
2095
2096        assert_eq!(contenders.len(), 2);
2097
2098        let first_contender = contenders.first().unwrap();
2099
2100        let second_contender = contenders.last().unwrap();
2101
2102        let first_contender_document = Document::from_bytes(
2103            first_contender
2104                .serialized_document()
2105                .as_ref()
2106                .expect("expected a document")
2107                .as_slice(),
2108            domain,
2109            platform_version,
2110        )
2111        .expect("expected to get document");
2112
2113        let second_contender_document = Document::from_bytes(
2114            second_contender
2115                .serialized_document()
2116                .as_ref()
2117                .expect("expected a document")
2118                .as_slice(),
2119            domain,
2120            platform_version,
2121        )
2122        .expect("expected to get document");
2123
2124        assert_ne!(first_contender_document, second_contender_document);
2125
2126        assert_eq!(first_contender.identity_id(), identity_1.id());
2127
2128        assert_eq!(second_contender.identity_id(), identity_2.id());
2129
2130        assert_eq!(first_contender.vote_tally(), Some(0));
2131
2132        assert_eq!(second_contender.vote_tally(), Some(0));
2133    }
2134
2135    #[allow(clippy::too_many_arguments)]
2136    pub(in crate::execution) async fn perform_vote(
2137        platform: &mut TempPlatform<MockCoreRPCLike>,
2138        platform_state: &Guard<Arc<PlatformState>>,
2139        dpns_contract: &DataContract,
2140        resource_vote_choice: ResourceVoteChoice,
2141        name: &str,
2142        signer: &SimpleSigner,
2143        pro_tx_hash: Identifier,
2144        voting_key: &IdentityPublicKey,
2145        nonce: IdentityNonce,
2146        expect_error: Option<&str>,
2147        platform_version: &PlatformVersion,
2148    ) {
2149        // Let's vote for contender 1
2150
2151        let vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 {
2152            vote_poll: VotePoll::ContestedDocumentResourceVotePoll(
2153                ContestedDocumentResourceVotePoll {
2154                    contract_id: dpns_contract.id(),
2155                    document_type_name: "domain".to_string(),
2156                    index_name: "parentNameAndLabel".to_string(),
2157                    index_values: vec![
2158                        Value::Text("dash".to_string()),
2159                        Value::Text(convert_to_homograph_safe_chars(name)),
2160                    ],
2161                },
2162            ),
2163            resource_vote_choice,
2164        }));
2165
2166        let masternode_vote_transition = MasternodeVoteTransition::try_from_vote_with_signer(
2167            vote,
2168            signer,
2169            pro_tx_hash,
2170            voting_key,
2171            nonce,
2172            platform_version,
2173            None,
2174        )
2175        .await
2176        .expect("expected to make transition vote");
2177
2178        let masternode_vote_serialized_transition = masternode_vote_transition
2179            .serialize_to_bytes()
2180            .expect("expected documents batch serialized state transition");
2181
2182        // CheckTx root-invariance guard (devnet paloma h788): `check_tx` asserts under
2183        // cfg(test) that it never mutates committed grovedb state, so every valid vote
2184        // fixture going through this shared helper pins the invariant for masternode votes.
2185        if expect_error.is_none() {
2186            crate::test::helpers::state_mutation_guard::assert_check_tx_valid_at_all_levels(
2187                platform,
2188                &masternode_vote_serialized_transition,
2189                "masternode vote",
2190            );
2191        }
2192
2193        let transaction = platform.drive.grove.start_transaction();
2194
2195        let processing_result = platform
2196            .platform
2197            .process_raw_state_transitions(
2198                &[masternode_vote_serialized_transition.clone()],
2199                platform_state,
2200                &BlockInfo::default(),
2201                &transaction,
2202                platform_version,
2203                false,
2204                None,
2205            )
2206            .expect("expected to process state transition");
2207
2208        platform
2209            .drive
2210            .grove
2211            .commit_transaction(transaction)
2212            .unwrap()
2213            .expect("expected to commit transaction");
2214
2215        let execution_result = processing_result.into_execution_results().remove(0);
2216        if let Some(error_msg) = expect_error {
2217            assert_matches!(execution_result, UnpaidConsensusError(..));
2218            let UnpaidConsensusError(consensus_error) = execution_result else {
2219                panic!()
2220            };
2221            assert_eq!(consensus_error.to_string(), error_msg)
2222        } else {
2223            assert_matches!(execution_result, SuccessfulExecution { .. });
2224        }
2225    }
2226
2227    #[allow(clippy::too_many_arguments)]
2228    pub(in crate::execution) async fn perform_votes(
2229        platform: &mut TempPlatform<MockCoreRPCLike>,
2230        dpns_contract: &DataContract,
2231        resource_vote_choice: ResourceVoteChoice,
2232        name: &str,
2233        count: u64,
2234        start_seed: u64,
2235        nonce_offset: Option<IdentityNonce>,
2236        platform_version: &PlatformVersion,
2237    ) -> Vec<(Identifier, Identity, SimpleSigner, IdentityPublicKey)> {
2238        let mut masternode_infos = vec![];
2239        for i in 0..count {
2240            let (pro_tx_hash_bytes, voting_identity, signer, voting_key) =
2241                setup_masternode_voting_identity(platform, start_seed + i, platform_version);
2242
2243            let platform_state = platform.state.load();
2244
2245            perform_vote(
2246                platform,
2247                &platform_state,
2248                dpns_contract,
2249                resource_vote_choice,
2250                name,
2251                &signer,
2252                pro_tx_hash_bytes,
2253                &voting_key,
2254                1 + nonce_offset.unwrap_or_default(),
2255                None,
2256                platform_version,
2257            )
2258            .await;
2259
2260            masternode_infos.push((pro_tx_hash_bytes, voting_identity, signer, voting_key));
2261        }
2262        masternode_infos
2263    }
2264
2265    pub(in crate::execution) async fn perform_votes_multi(
2266        platform: &mut TempPlatform<MockCoreRPCLike>,
2267        dpns_contract: &DataContract,
2268        resource_vote_choices: Vec<(ResourceVoteChoice, u64)>,
2269        name: &str,
2270        start_seed: u64,
2271        nonce_offset: Option<IdentityNonce>,
2272        platform_version: &PlatformVersion,
2273    ) -> BTreeMap<ResourceVoteChoice, Vec<(Identifier, Identity, SimpleSigner, IdentityPublicKey)>>
2274    {
2275        let mut count_aggregate = start_seed;
2276        let mut masternodes_by_vote_choice = BTreeMap::new();
2277        for (resource_vote_choice, count) in resource_vote_choices.into_iter() {
2278            let masternode_infos = perform_votes(
2279                platform,
2280                dpns_contract,
2281                resource_vote_choice,
2282                name,
2283                count,
2284                count_aggregate,
2285                nonce_offset,
2286                platform_version,
2287            )
2288            .await;
2289            masternodes_by_vote_choice.insert(resource_vote_choice, masternode_infos);
2290            count_aggregate += count;
2291        }
2292        masternodes_by_vote_choice
2293    }
2294
2295    #[allow(clippy::too_many_arguments)]
2296    pub(in crate::execution) fn get_vote_states(
2297        platform: &TempPlatform<MockCoreRPCLike>,
2298        platform_state: &PlatformState,
2299        dpns_contract: &DataContract,
2300        name: &str,
2301        count: Option<u32>,
2302        allow_include_locked_and_abstaining_vote_tally: bool,
2303        start_at_identifier_info: Option<
2304            get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo,
2305        >,
2306        result_type: ResultType,
2307        platform_version: &PlatformVersion,
2308    ) -> (
2309        Vec<Contender>,
2310        Option<u32>,
2311        Option<u32>,
2312        Option<FinishedVoteInfo>,
2313    ) {
2314        // Now let's run a query for the vote totals
2315
2316        let domain = dpns_contract
2317            .document_type_for_name("domain")
2318            .expect("expected a profile document type");
2319
2320        let config = bincode::config::standard()
2321            .with_big_endian()
2322            .with_no_limit();
2323
2324        let dash_encoded = bincode::encode_to_vec(Value::Text("dash".to_string()), config)
2325            .expect("expected to encode the word dash");
2326
2327        let name_encoded =
2328            bincode::encode_to_vec(Value::Text(convert_to_homograph_safe_chars(name)), config)
2329                .expect("expected to encode the word quantum");
2330
2331        let index_name = "parentNameAndLabel".to_string();
2332
2333        let query_validation_result = platform
2334            .query_contested_resource_vote_state(
2335                GetContestedResourceVoteStateRequest {
2336                    version: Some(get_contested_resource_vote_state_request::Version::V0(
2337                        GetContestedResourceVoteStateRequestV0 {
2338                            contract_id: dpns_contract.id().to_vec(),
2339                            document_type_name: domain.name().clone(),
2340                            index_name: index_name.clone(),
2341                            index_values: vec![dash_encoded.clone(), name_encoded.clone()],
2342                            result_type: result_type as i32,
2343                            allow_include_locked_and_abstaining_vote_tally,
2344                            start_at_identifier_info,
2345                            count,
2346                            prove: false,
2347                        },
2348                    )),
2349                },
2350                platform_state,
2351                platform_version,
2352            )
2353            .expect("expected to execute query")
2354            .into_data()
2355            .expect("expected query to be valid");
2356
2357        let get_contested_resource_vote_state_response::Version::V0(
2358            GetContestedResourceVoteStateResponseV0 {
2359                metadata: _,
2360                result,
2361            },
2362        ) = query_validation_result.version.expect("expected a version");
2363
2364        let Some(
2365            get_contested_resource_vote_state_response_v0::Result::ContestedResourceContenders(
2366                get_contested_resource_vote_state_response_v0::ContestedResourceContenders {
2367                    contenders,
2368                    abstain_vote_tally,
2369                    lock_vote_tally,
2370                    finished_vote_info,
2371                },
2372            ),
2373        ) = result
2374        else {
2375            panic!("expected contenders")
2376        };
2377        (
2378            contenders
2379                .into_iter()
2380                .map(|contender| {
2381                    ContenderV0 {
2382                        identity_id: contender.identifier.try_into().expect("expected 32 bytes"),
2383                        document: contender.document.map(|document_bytes| {
2384                            Document::from_bytes(
2385                                document_bytes.as_slice(),
2386                                domain,
2387                                platform_version,
2388                            )
2389                            .expect("expected to deserialize document")
2390                        }),
2391                        vote_tally: contender.vote_count,
2392                    }
2393                    .into()
2394                })
2395                .collect(),
2396            abstain_vote_tally,
2397            lock_vote_tally,
2398            finished_vote_info,
2399        )
2400    }
2401
2402    #[allow(clippy::too_many_arguments)]
2403    pub(in crate::execution) fn get_proved_vote_states(
2404        platform: &TempPlatform<MockCoreRPCLike>,
2405        platform_state: &PlatformState,
2406        dpns_contract: &DataContract,
2407        name: &str,
2408        count: Option<u32>,
2409        allow_include_locked_and_abstaining_vote_tally: bool,
2410        start_at_identifier_info: Option<
2411            get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo,
2412        >,
2413        result_type: ResultType,
2414        platform_version: &PlatformVersion,
2415    ) -> (
2416        Vec<Contender>,
2417        Option<u32>,
2418        Option<u32>,
2419        Option<(ContestedDocumentVotePollWinnerInfo, BlockInfo)>,
2420    ) {
2421        // Now let's run a query for the vote totals
2422
2423        let domain = dpns_contract
2424            .document_type_for_name("domain")
2425            .expect("expected a profile document type");
2426
2427        let config = bincode::config::standard()
2428            .with_big_endian()
2429            .with_no_limit();
2430
2431        let dash_encoded = bincode::encode_to_vec(Value::Text("dash".to_string()), config)
2432            .expect("expected to encode the word dash");
2433
2434        let name_encoded =
2435            bincode::encode_to_vec(Value::Text(convert_to_homograph_safe_chars(name)), config)
2436                .expect("expected to encode the word quantum");
2437
2438        let index_name = "parentNameAndLabel".to_string();
2439
2440        let query_validation_result = platform
2441            .query_contested_resource_vote_state(
2442                GetContestedResourceVoteStateRequest {
2443                    version: Some(get_contested_resource_vote_state_request::Version::V0(
2444                        GetContestedResourceVoteStateRequestV0 {
2445                            contract_id: dpns_contract.id().to_vec(),
2446                            document_type_name: domain.name().clone(),
2447                            index_name: index_name.clone(),
2448                            index_values: vec![dash_encoded.clone(), name_encoded.clone()],
2449                            result_type: result_type as i32,
2450                            allow_include_locked_and_abstaining_vote_tally,
2451                            start_at_identifier_info,
2452                            count,
2453                            prove: true,
2454                        },
2455                    )),
2456                },
2457                platform_state,
2458                platform_version,
2459            )
2460            .expect("expected to execute query")
2461            .into_data()
2462            .expect("expected query to be valid");
2463
2464        let get_contested_resource_vote_state_response::Version::V0(
2465            GetContestedResourceVoteStateResponseV0 {
2466                metadata: _,
2467                result,
2468            },
2469        ) = query_validation_result.version.expect("expected a version");
2470
2471        let Some(get_contested_resource_vote_state_response_v0::Result::Proof(proof)) = result
2472        else {
2473            panic!("expected contenders")
2474        };
2475
2476        let resolved_contested_document_vote_poll_drive_query =
2477            ResolvedContestedDocumentVotePollDriveQuery {
2478                vote_poll: ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed {
2479                    contract: DataContractResolvedInfo::BorrowedDataContract(dpns_contract),
2480                    document_type_name: domain.name().clone(),
2481                    index_name: index_name.clone(),
2482                    index_values: vec![
2483                        Value::Text("dash".to_string()),
2484                        Value::Text(convert_to_homograph_safe_chars(name)),
2485                    ],
2486                },
2487                result_type: ContestedDocumentVotePollDriveQueryResultType::try_from(
2488                    result_type as i32,
2489                )
2490                .expect("expected valid result type"),
2491                offset: None,
2492                limit: count.map(|a| a as u16),
2493                start_at: None,
2494                allow_include_locked_and_abstaining_vote_tally,
2495            };
2496
2497        let (_, result) = resolved_contested_document_vote_poll_drive_query
2498            .verify_vote_poll_vote_state_proof(proof.grovedb_proof.as_ref(), platform_version)
2499            .expect("expected to verify proof");
2500
2501        let abstaining_vote_tally = result.abstaining_vote_tally;
2502        let lock_vote_tally = result.locked_vote_tally;
2503
2504        let contenders = result.contenders;
2505        let finished_vote_info = result.winner;
2506        (
2507            contenders
2508                .into_iter()
2509                .map(|contender| {
2510                    ContenderV0 {
2511                        identity_id: contender.identity_id(),
2512                        document: contender
2513                            .serialized_document()
2514                            .as_ref()
2515                            .map(|document_bytes| {
2516                                Document::from_bytes(
2517                                    document_bytes.as_slice(),
2518                                    domain,
2519                                    platform_version,
2520                                )
2521                                .expect("expected to deserialize document")
2522                            }),
2523                        vote_tally: contender.vote_tally(),
2524                    }
2525                    .into()
2526                })
2527                .collect(),
2528            abstaining_vote_tally,
2529            lock_vote_tally,
2530            finished_vote_info,
2531        )
2532    }
2533
2534    pub(in crate::execution) fn create_token_contract_with_owner_identity(
2535        platform: &mut TempPlatform<MockCoreRPCLike>,
2536        identity_id: Identifier,
2537        token_configuration_modification: Option<impl FnOnce(&mut TokenConfiguration)>,
2538        contract_start_time: Option<TimestampMillis>,
2539        add_groups: Option<BTreeMap<GroupContractPosition, Group>>,
2540        contract_start_block: Option<BlockHeight>,
2541        platform_version: &PlatformVersion,
2542    ) -> (DataContract, Identifier) {
2543        create_token_contract_with_owner_identity_with_start_epoch(
2544            platform,
2545            identity_id,
2546            token_configuration_modification,
2547            contract_start_time,
2548            add_groups,
2549            contract_start_block,
2550            None,
2551            platform_version,
2552        )
2553    }
2554
2555    /// Like [`create_token_contract_with_owner_identity`], with the contract's creation epoch
2556    /// settable as well; a perpetual distribution counted in epochs starts from it.
2557    pub(in crate::execution) fn create_token_contract_with_owner_identity_with_start_epoch(
2558        platform: &mut TempPlatform<MockCoreRPCLike>,
2559        identity_id: Identifier,
2560        token_configuration_modification: Option<impl FnOnce(&mut TokenConfiguration)>,
2561        contract_start_time: Option<TimestampMillis>,
2562        add_groups: Option<BTreeMap<GroupContractPosition, Group>>,
2563        contract_start_block: Option<BlockHeight>,
2564        contract_start_epoch: Option<EpochIndex>,
2565        platform_version: &PlatformVersion,
2566    ) -> (DataContract, Identifier) {
2567        let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1);
2568
2569        let basic_token_contract = setup_contract(
2570            &platform.drive,
2571            "tests/supporting_files/contract/basic-token/basic-token.json",
2572            Some(data_contract_id.to_buffer()),
2573            Some(identity_id.to_buffer()),
2574            Some(|data_contract: &mut DataContract| {
2575                data_contract.set_created_at_epoch(Some(contract_start_epoch.unwrap_or_default()));
2576                data_contract.set_created_at(Some(contract_start_time.unwrap_or_default()));
2577                data_contract
2578                    .set_created_at_block_height(Some(contract_start_block.unwrap_or_default()));
2579                if let Some(token_configuration_modification) = token_configuration_modification {
2580                    let token_configuration = data_contract
2581                        .token_configuration_mut(0)
2582                        .expect("expected token configuration");
2583                    token_configuration_modification(token_configuration);
2584                }
2585                if let Some(add_groups) = add_groups {
2586                    data_contract.set_groups(add_groups);
2587                }
2588            }),
2589            None,
2590            Some(platform_version),
2591        );
2592
2593        let token_id = calculate_token_id(data_contract_id.as_bytes(), 0);
2594
2595        (basic_token_contract, token_id.into())
2596    }
2597
2598    pub(in crate::execution) fn create_card_game_internal_token_contract_with_owner_identity_burn_tokens(
2599        platform: &mut TempPlatform<MockCoreRPCLike>,
2600        identity_id: Identifier,
2601        platform_version: &PlatformVersion,
2602    ) -> (DataContract, Identifier, Identifier) {
2603        let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1);
2604
2605        let basic_token_contract = setup_contract(
2606            &platform.drive,
2607            "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency-burn-tokens.json",
2608            Some(data_contract_id.to_buffer()),
2609            Some(identity_id.to_buffer()),
2610            Some(|data_contract: &mut DataContract| {
2611                data_contract.set_created_at_epoch(Some(0));
2612                data_contract.set_created_at(Some(0));
2613                data_contract.set_created_at_block_height(Some(0));
2614            }),
2615            None,
2616            Some(platform_version),
2617        );
2618
2619        let token_id = calculate_token_id(data_contract_id.as_bytes(), 0);
2620        let token_id_2 = calculate_token_id(data_contract_id.as_bytes(), 1);
2621
2622        (basic_token_contract, token_id.into(), token_id_2.into())
2623    }
2624
2625    pub(in crate::execution) fn create_card_game_internal_token_contract_with_owner_identity_transfer_tokens(
2626        platform: &mut TempPlatform<MockCoreRPCLike>,
2627        identity_id: Identifier,
2628        platform_version: &PlatformVersion,
2629    ) -> (DataContract, Identifier, Identifier) {
2630        let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1);
2631
2632        let basic_token_contract = setup_contract(
2633            &platform.drive,
2634            "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency.json",
2635            Some(data_contract_id.to_buffer()),
2636            Some(identity_id.to_buffer()),
2637            Some(|data_contract: &mut DataContract| {
2638                data_contract.set_created_at_epoch(Some(0));
2639                data_contract.set_created_at(Some(0));
2640                data_contract.set_created_at_block_height(Some(0));
2641            }),
2642            None,
2643            Some(platform_version),
2644        );
2645
2646        let token_id = calculate_token_id(data_contract_id.as_bytes(), 0);
2647        let token_id_2 = calculate_token_id(data_contract_id.as_bytes(), 1);
2648
2649        (basic_token_contract, token_id.into(), token_id_2.into())
2650    }
2651
2652    pub(in crate::execution) fn create_card_game_external_token_contract_with_owner_identity(
2653        platform: &mut TempPlatform<MockCoreRPCLike>,
2654        token_contract_id: Identifier,
2655        token_contract_position: TokenContractPosition,
2656        token_cost_amount: TokenAmount,
2657        gas_fees_paid_by: GasFeesPaidBy,
2658        identity_id: Identifier,
2659        platform_version: &PlatformVersion,
2660    ) -> DataContract {
2661        let data_contract_id = DataContract::generate_data_contract_id_v0(identity_id, 1);
2662
2663        let basic_token_contract = setup_contract(
2664            &platform.drive,
2665            "tests/supporting_files/contract/crypto-card-game/crypto-card-game-use-external-currency.json",
2666            Some(data_contract_id.to_buffer()),
2667            Some(identity_id.to_buffer()),
2668            Some(|data_contract: &mut DataContract| {
2669                data_contract.set_created_at_epoch(Some(0));
2670                data_contract.set_created_at(Some(0));
2671                data_contract.set_created_at_block_height(Some(0));
2672                let document_type = data_contract.document_types_mut().get_mut("card").expect("expected a document type with name card");
2673                document_type.set_document_creation_token_cost(Some(DocumentActionTokenCost {
2674                    contract_id: Some(token_contract_id),
2675                    token_contract_position,
2676                    token_amount: token_cost_amount,
2677                    effect: DocumentActionTokenEffect::TransferTokenToContractOwner,
2678                    gas_fees_paid_by,
2679                }));
2680                let gas_fees_paid_by_int: u8 = gas_fees_paid_by.into();
2681                let schema = document_type.schema_mut();
2682                let token_cost = schema.get_mut("tokenCost").expect("expected to get token cost").expect("expected token cost to be set");
2683                let creation_token_cost = token_cost.get_mut("create").expect("expected to get creation token cost").expect("expected creation token cost to be set");
2684                creation_token_cost.set_value("contractId", token_contract_id.into()).expect("expected to set token contract id");
2685                creation_token_cost.set_value("tokenPosition", token_contract_position.into()).expect("expected to set token position");
2686                creation_token_cost.set_value("amount", token_cost_amount.into()).expect("expected to set token amount");
2687                creation_token_cost.set_value("gasFeesPaidBy", gas_fees_paid_by_int.into()).expect("expected to set token amount");
2688            }),
2689            None,
2690            Some(platform_version),
2691        );
2692
2693        basic_token_contract
2694    }
2695
2696    pub(in crate::execution) fn process_test_state_transition<S: PlatformSerializable>(
2697        platform: &mut TempPlatform<MockCoreRPCLike>,
2698        state_transition: S,
2699        platform_state: &PlatformState,
2700        platform_version: &PlatformVersion,
2701    ) -> StateTransitionsProcessingResult {
2702        let Ok(serialized_state_transition) = state_transition.serialize_to_bytes() else {
2703            panic!("expected documents batch serialized state transition")
2704        };
2705
2706        let transaction = platform.drive.grove.start_transaction();
2707
2708        let processing_result = platform
2709            .platform
2710            .process_raw_state_transitions(
2711                &[serialized_state_transition],
2712                platform_state,
2713                &BlockInfo::default(),
2714                &transaction,
2715                platform_version,
2716                false,
2717                None,
2718            )
2719            .expect("expected to process state transition");
2720
2721        platform
2722            .drive
2723            .grove
2724            .commit_transaction(transaction)
2725            .unwrap()
2726            .expect("expected to commit transaction");
2727
2728        processing_result
2729    }
2730
2731    mod keyword_search_contract {
2732        use dpp::consensus::basic::BasicError;
2733        use dpp::consensus::ConsensusError;
2734        use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult::PaidConsensusError;
2735        use super::*;
2736        //
2737        // ──────────────────────────────────────────────────────────────────────────
2738        //  Keyword‑Search contract – creation is forbidden
2739        // ──────────────────────────────────────────────────────────────────────────
2740        //
2741
2742        /// Return `(document, entropy)` so we can reuse the exact entropy when
2743        /// we build the transition.  **No rng.clone()** (that caused the ID mismatch).
2744        fn build_random_doc_of_type(
2745            rng: &mut StdRng,
2746            doc_type_name: &str,
2747            identity_id: Identifier,
2748            contract: &DataContract,
2749            platform_version: &PlatformVersion,
2750        ) -> (Document, Bytes32) {
2751            let doc_type = contract
2752                .document_type_for_name(doc_type_name)
2753                .expect("doc type exists");
2754
2755            let entropy = Bytes32::random_with_rng(rng);
2756
2757            let doc = doc_type
2758                .random_document_with_identifier_and_entropy(
2759                    rng,
2760                    identity_id,
2761                    entropy,
2762                    DocumentFieldFillType::FillIfNotRequired,
2763                    DocumentFieldFillSize::AnyDocumentFillSize,
2764                    platform_version,
2765                )
2766                .expect("random doc");
2767
2768            (doc, entropy)
2769        }
2770
2771        #[tokio::test]
2772        async fn should_err_when_creating_contract_keywords_document() {
2773            let platform_version = PlatformVersion::latest();
2774
2775            let mut platform = TestPlatformBuilder::new()
2776                .build_with_mock_rpc()
2777                .set_genesis_state();
2778
2779            let (identity, signer, key) = setup_identity(&mut platform, 42, dash_to_credits!(1.0));
2780
2781            let contract = load_system_data_contract(
2782                SystemDataContract::KeywordSearch,
2783                PlatformVersion::latest(),
2784            )
2785            .expect("expected to load search contract");
2786
2787            let mut rng = StdRng::seed_from_u64(1);
2788            let (doc, entropy) = build_random_doc_of_type(
2789                &mut rng,
2790                "contractKeywords",
2791                identity.id(),
2792                &contract,
2793                platform_version,
2794            );
2795
2796            let transition = BatchTransition::new_document_creation_transition_from_document(
2797                doc,
2798                contract.document_type_for_name("contractKeywords").unwrap(),
2799                entropy.0, // same entropy → no ID mismatch
2800                &key,
2801                1,
2802                0,
2803                None,
2804                &signer,
2805                platform_version,
2806                None,
2807            )
2808            .await
2809            .expect("batch transition");
2810
2811            let serialized = transition.serialize_to_bytes().unwrap();
2812
2813            let platform_state = platform.state.load();
2814            let processing_result = platform
2815                .platform
2816                .process_raw_state_transitions(
2817                    &[serialized],
2818                    &platform_state,
2819                    &BlockInfo::default(),
2820                    &platform.drive.grove.start_transaction(),
2821                    platform_version,
2822                    false,
2823                    None,
2824                )
2825                .expect("processing failed");
2826
2827            let execution_result = processing_result.into_execution_results().remove(0);
2828            assert_matches!(
2829                execution_result,
2830                StateTransitionExecutionResult::PaidConsensusError{ error, .. }
2831                    if error.to_string().contains("not allowed because of the document type's creation restriction mode")
2832            );
2833        }
2834
2835        #[tokio::test]
2836        async fn should_err_when_creating_short_description_document() {
2837            let platform_version = PlatformVersion::latest();
2838
2839            let mut platform = TestPlatformBuilder::new()
2840                .build_with_mock_rpc()
2841                .set_genesis_state();
2842
2843            let (identity, signer, key) = setup_identity(&mut platform, 43, dash_to_credits!(1.0));
2844
2845            let contract = load_system_data_contract(
2846                SystemDataContract::KeywordSearch,
2847                PlatformVersion::latest(),
2848            )
2849            .expect("expected to load search contract");
2850
2851            let mut rng = StdRng::seed_from_u64(2);
2852            let (doc, entropy) = build_random_doc_of_type(
2853                &mut rng,
2854                "shortDescription",
2855                identity.id(),
2856                &contract,
2857                platform_version,
2858            );
2859
2860            let transition = BatchTransition::new_document_creation_transition_from_document(
2861                doc,
2862                contract.document_type_for_name("shortDescription").unwrap(),
2863                entropy.0,
2864                &key,
2865                1,
2866                0,
2867                None,
2868                &signer,
2869                platform_version,
2870                None,
2871            )
2872            .await
2873            .expect("batch transition");
2874
2875            let serialized = transition.serialize_to_bytes().unwrap();
2876
2877            let platform_state = platform.state.load();
2878            let processing_result = platform
2879                .platform
2880                .process_raw_state_transitions(
2881                    &[serialized],
2882                    &platform_state,
2883                    &BlockInfo::default(),
2884                    &platform.drive.grove.start_transaction(),
2885                    platform_version,
2886                    false,
2887                    None,
2888                )
2889                .expect("processing failed");
2890
2891            let execution_result = processing_result.into_execution_results().remove(0);
2892            assert_matches!(
2893                execution_result,
2894                StateTransitionExecutionResult::PaidConsensusError{ error, .. }
2895                    if error.to_string().contains("not allowed because of the document type's creation restriction mode")
2896            );
2897        }
2898
2899        #[tokio::test]
2900        async fn should_err_when_creating_full_description_document() {
2901            let platform_version = PlatformVersion::latest();
2902
2903            let mut platform = TestPlatformBuilder::new()
2904                .build_with_mock_rpc()
2905                .set_genesis_state();
2906
2907            let (identity, signer, key) = setup_identity(&mut platform, 44, dash_to_credits!(1.0));
2908
2909            let contract = load_system_data_contract(
2910                SystemDataContract::KeywordSearch,
2911                PlatformVersion::latest(),
2912            )
2913            .expect("expected to load search contract");
2914
2915            let mut rng = StdRng::seed_from_u64(3);
2916            let (doc, entropy) = build_random_doc_of_type(
2917                &mut rng,
2918                "fullDescription",
2919                identity.id(),
2920                &contract,
2921                platform_version,
2922            );
2923
2924            let transition = BatchTransition::new_document_creation_transition_from_document(
2925                doc,
2926                contract.document_type_for_name("fullDescription").unwrap(),
2927                entropy.0,
2928                &key,
2929                1,
2930                0,
2931                None,
2932                &signer,
2933                platform_version,
2934                None,
2935            )
2936            .await
2937            .expect("batch transition");
2938
2939            let serialized = transition.serialize_to_bytes().unwrap();
2940
2941            let platform_state = platform.state.load();
2942            let processing_result = platform
2943                .platform
2944                .process_raw_state_transitions(
2945                    &[serialized],
2946                    &platform_state,
2947                    &BlockInfo::default(),
2948                    &platform.drive.grove.start_transaction(),
2949                    platform_version,
2950                    false,
2951                    None,
2952                )
2953                .expect("processing failed");
2954
2955            let execution_result = processing_result.into_execution_results().remove(0);
2956            assert_matches!(
2957                execution_result,
2958                StateTransitionExecutionResult::PaidConsensusError{ error, .. }
2959                    if error.to_string().contains("not allowed because of the document type's creation restriction mode")
2960            );
2961        }
2962
2963        //
2964        // ──────────────────────────────────────────────────────────────────────────
2965        //  Keyword‑Search contract – owner can update / delete
2966        // ──────────────────────────────────────────────────────────────────────────
2967        //
2968
2969        async fn create_contract_with_keywords_and_description(
2970            platform: &mut TempPlatform<MockCoreRPCLike>,
2971        ) -> (Identity, SimpleSigner, IdentityPublicKey) {
2972            let platform_version = PlatformVersion::latest();
2973
2974            // Owner identity
2975            let (owner_identity, signer, key) =
2976                setup_identity(platform, 777, dash_to_credits!(1.0));
2977
2978            // Load the keyword‑test fixture
2979            let mut contract = json_document_to_contract_with_ids(
2980                "tests/supporting_files/contract/keyword_test/keyword_base_contract.json",
2981                Some(owner_identity.id()),
2982                None,
2983                false,
2984                platform_version,
2985            )
2986            .expect("load contract");
2987
2988            // Inject description + keywords
2989            contract.set_description(Some("A short description".to_string()));
2990            contract.set_keywords(vec!["graph".into(), "indexing".into()]);
2991
2992            // Create transition inside GroveDB tx
2993            let create_transition = DataContractCreateTransition::new_from_data_contract(
2994                contract,
2995                1,
2996                &owner_identity.clone().into_partial_identity_info(),
2997                key.id(),
2998                &signer,
2999                platform_version,
3000                None,
3001            )
3002            .await
3003            .expect("build transition");
3004
3005            let serialized = create_transition.serialize_to_bytes().unwrap();
3006            let platform_state = platform.state.load();
3007            let tx = platform.drive.grove.start_transaction();
3008
3009            let processing_result = platform
3010                .platform
3011                .process_raw_state_transitions(
3012                    &[serialized],
3013                    &platform_state,
3014                    &BlockInfo::default(),
3015                    &tx,
3016                    platform_version,
3017                    false,
3018                    None,
3019                )
3020                .expect("process");
3021
3022            assert_matches!(
3023                processing_result.execution_results().as_slice(),
3024                [StateTransitionExecutionResult::SuccessfulExecution { .. }]
3025            );
3026
3027            platform
3028                .drive
3029                .grove
3030                .commit_transaction(tx)
3031                .unwrap()
3032                .expect("commit");
3033
3034            (owner_identity, signer, key)
3035        }
3036
3037        #[tokio::test]
3038        async fn owner_can_update_short_description_document() {
3039            let platform_version = PlatformVersion::latest();
3040            let mut platform = TestPlatformBuilder::new()
3041                .build_with_mock_rpc()
3042                .set_genesis_state();
3043
3044            let (_owner, signer, key) =
3045                create_contract_with_keywords_and_description(&mut platform).await;
3046
3047            // 🔎 fetch shortDescription doc through query
3048            let search_contract =
3049                load_system_data_contract(SystemDataContract::KeywordSearch, platform_version)
3050                    .expect("load search contract");
3051
3052            let doc_type = search_contract
3053                .document_type_for_name("shortDescription")
3054                .unwrap();
3055
3056            let query = DriveDocumentQuery::all_items_query(&search_contract, doc_type, None);
3057            let existing_docs = platform
3058                .drive
3059                .query_documents(
3060                    query,
3061                    None,
3062                    false,
3063                    None,
3064                    Some(platform_version.protocol_version),
3065                )
3066                .expect("query failed");
3067
3068            let mut doc = existing_docs.documents().first().expect("doc").clone();
3069            doc.set_revision(Some(doc.revision().unwrap_or_default() + 1));
3070            doc.set("description", "updated description".into());
3071
3072            let transition = BatchTransition::new_document_replacement_transition_from_document(
3073                doc,
3074                doc_type,
3075                &key,
3076                2,
3077                0,
3078                None,
3079                &signer,
3080                platform_version,
3081                None,
3082            )
3083            .await
3084            .expect("replace");
3085
3086            let serialized = transition.serialize_to_bytes().unwrap();
3087            let platform_state = platform.state.load();
3088
3089            let processing_result = platform
3090                .platform
3091                .process_raw_state_transitions(
3092                    &[serialized],
3093                    &platform_state,
3094                    &BlockInfo::default(),
3095                    &platform.drive.grove.start_transaction(),
3096                    platform_version,
3097                    false,
3098                    None,
3099                )
3100                .expect("process");
3101
3102            assert_matches!(
3103                processing_result.into_execution_results().remove(0),
3104                SuccessfulExecution { .. }
3105            );
3106        }
3107
3108        #[tokio::test]
3109        async fn owner_can_not_delete_keyword_document() {
3110            let platform_version = PlatformVersion::latest();
3111            let mut platform = TestPlatformBuilder::new()
3112                .build_with_mock_rpc()
3113                .set_genesis_state();
3114
3115            let (_owner, signer, key) =
3116                create_contract_with_keywords_and_description(&mut platform).await;
3117
3118            let search_contract =
3119                load_system_data_contract(SystemDataContract::KeywordSearch, platform_version)
3120                    .expect("load search contract");
3121            let doc_type = search_contract
3122                .document_type_for_name("contractKeywords")
3123                .unwrap();
3124
3125            let query = DriveDocumentQuery::all_items_query(&search_contract, doc_type, None);
3126            let existing_docs = platform
3127                .drive
3128                .query_documents(
3129                    query,
3130                    None,
3131                    false,
3132                    None,
3133                    Some(platform_version.protocol_version),
3134                )
3135                .expect("query failed");
3136
3137            let doc = existing_docs.documents().first().unwrap().clone();
3138
3139            let transition = BatchTransition::new_document_deletion_transition_from_document(
3140                doc,
3141                doc_type,
3142                &key,
3143                2,
3144                0,
3145                None,
3146                &signer,
3147                platform_version,
3148                None,
3149            )
3150            .await
3151            .expect("delete");
3152
3153            let serialized = transition.serialize_to_bytes().unwrap();
3154            let platform_state = platform.state.load();
3155
3156            let processing_result = platform
3157                .platform
3158                .process_raw_state_transitions(
3159                    &[serialized],
3160                    &platform_state,
3161                    &BlockInfo::default(),
3162                    &platform.drive.grove.start_transaction(),
3163                    platform_version,
3164                    false,
3165                    None,
3166                )
3167                .expect("process");
3168            assert_matches!(
3169                processing_result.execution_results().as_slice(),
3170                [PaidConsensusError {
3171                    error: ConsensusError::BasicError(
3172                        BasicError::InvalidDocumentTransitionActionError { .. }
3173                    ),
3174                    ..
3175                }]
3176            );
3177        }
3178    }
3179
3180    /// End-to-end regression test for the byteArray on-disk encoding-flip
3181    /// chain-halt vulnerability (Dash Platform v4.0.0-rc.1).
3182    ///
3183    /// This drives the full per-block DAO vote-poll resolver path so that
3184    /// `Platform::check_for_ended_vote_polls` returns `Err` today (pre-fix). On
3185    /// chain that `Err` propagates through `run_dao_platform_events` ->
3186    /// `run_block_proposal` (a per-block event handler with a bare `?`), so it is
3187    /// NOT caught into a per-state-transition result -- it halts the chain on
3188    /// every validator at the block where the contested vote poll ends.
3189    mod byte_array_encoding_flip_chain_halt {
3190        use super::*;
3191        use crate::test::helpers::fast_forward_to_block::fast_forward_to_block;
3192        use dpp::data_contract::document_type::schema::validate_schema_compatibility;
3193
3194        const CUSTOM_CONTESTED_CONTRACT: &str =
3195            "tests/supporting_files/contract/dpns/dpns-contract-contested-unique-index.json";
3196        // Identical to the contract above except `domain.preorderSalt` byteArray
3197        // has its `maxItems` widened from 32 to 64 -- the malicious update.
3198        const CUSTOM_CONTESTED_CONTRACT_WIDENED: &str =
3199            "tests/supporting_files/contract/dpns/dpns-contract-contested-unique-index-byte-array-widened.json";
3200
3201        /// Builds one preorder + one domain (contender) document for `identity`,
3202        /// with `preorderSalt` forced to `[0xFF; 32]` so that, after the
3203        /// byteArray encoding flips to varint-length-prefixed, the first stored
3204        /// byte (0xFF, a varint continuation byte) decodes to an enormous length
3205        /// and overruns the buffer.
3206        #[allow(clippy::too_many_arguments)]
3207        async fn build_preorder_and_domain(
3208            contract: &DataContract,
3209            identity: &Identity,
3210            signer: &SimpleSigner,
3211            key: &IdentityPublicKey,
3212            name: &str,
3213            // A per-contender distinguishing byte placed in the *tail* of the
3214            // salt so the two contenders' `saltedDomainHash` differ (avoiding a
3215            // unique-index collision on the preorder), while byte[0] stays 0xFF.
3216            salt_discriminator: u8,
3217            rng: &mut StdRng,
3218            platform_version: &PlatformVersion,
3219        ) -> (Vec<u8>, Vec<u8>) {
3220            let preorder = contract
3221                .document_type_for_name("preorder")
3222                .expect("expected preorder document type");
3223            let domain = contract
3224                .document_type_for_name("domain")
3225                .expect("expected domain document type");
3226
3227            let entropy = Bytes32::random_with_rng(rng);
3228
3229            let mut preorder_document = preorder
3230                .random_document_with_identifier_and_entropy(
3231                    rng,
3232                    identity.id(),
3233                    entropy,
3234                    DocumentFieldFillType::FillIfNotRequired,
3235                    DocumentFieldFillSize::AnyDocumentFillSize,
3236                    platform_version,
3237                )
3238                .expect("expected a random preorder document");
3239
3240            let mut domain_document = domain
3241                .random_document_with_identifier_and_entropy(
3242                    rng,
3243                    identity.id(),
3244                    entropy,
3245                    DocumentFieldFillType::FillIfNotRequired,
3246                    DocumentFieldFillSize::AnyDocumentFillSize,
3247                    platform_version,
3248                )
3249                .expect("expected a random domain document");
3250
3251            domain_document.set("parentDomainName", "dash".into());
3252            domain_document.set("normalizedParentDomainName", "dash".into());
3253            domain_document.set("label", name.into());
3254            domain_document.set(
3255                "normalizedLabel",
3256                convert_to_homograph_safe_chars(name).into(),
3257            );
3258            domain_document.set("records.identity", domain_document.owner_id().into());
3259            domain_document.set("subdomainRules.allowSubdomains", false.into());
3260
3261            // The crux of the attack: a 32-byte salt whose FIRST byte is 0xFF
3262            // (a varint continuation byte). The last byte distinguishes the two
3263            // contenders so their preorder salted hashes do not collide.
3264            let mut salt: [u8; 32] = [0xFF; 32];
3265            salt[31] = salt_discriminator;
3266
3267            let mut salted_domain_buffer: Vec<u8> = vec![];
3268            salted_domain_buffer.extend(salt);
3269            salted_domain_buffer
3270                .extend((convert_to_homograph_safe_chars(name) + ".dash").as_bytes());
3271            let salted_domain_hash = hash_double(salted_domain_buffer);
3272
3273            preorder_document.set("saltedDomainHash", salted_domain_hash.into());
3274            domain_document.set("preorderSalt", salt.into());
3275
3276            let preorder_transition =
3277                BatchTransition::new_document_creation_transition_from_document(
3278                    preorder_document,
3279                    preorder,
3280                    entropy.0,
3281                    key,
3282                    2,
3283                    0,
3284                    None,
3285                    signer,
3286                    platform_version,
3287                    None,
3288                )
3289                .await
3290                .expect("expect to create preorder batch transition");
3291
3292            let domain_transition =
3293                BatchTransition::new_document_creation_transition_from_document(
3294                    domain_document,
3295                    domain,
3296                    entropy.0,
3297                    key,
3298                    3,
3299                    0,
3300                    None,
3301                    signer,
3302                    platform_version,
3303                    None,
3304                )
3305                .await
3306                .expect("expect to create domain batch transition");
3307
3308            (
3309                preorder_transition
3310                    .serialize_to_bytes()
3311                    .expect("serialize preorder transition"),
3312                domain_transition
3313                    .serialize_to_bytes()
3314                    .expect("serialize domain transition"),
3315            )
3316        }
3317
3318        /// Runs the full contest setup and returns the result of the per-block
3319        /// vote-poll resolver. When `widen` is true the byteArray `maxItems` is
3320        /// flipped (the attack); when false the original contract is left in
3321        /// place (the control).
3322        async fn run_contest_then_resolve(widen: bool) -> Result<(), crate::error::Error> {
3323            let mut platform = TestPlatformBuilder::new()
3324                .with_latest_protocol_version()
3325                .build_with_mock_rpc()
3326                .set_initial_state_structure();
3327
3328            let platform_version = PlatformVersion::latest();
3329
3330            let mut rng = StdRng::seed_from_u64(0xB17E_A77A);
3331
3332            // Two contenders, each prefunded so they can pay the contested-index
3333            // voting balance.
3334            let identity_1_info = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5));
3335            let identity_2_info = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5));
3336
3337            let contract_owner = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5));
3338
3339            // (1) Create the CUSTOM contested contract owned by `contract_owner`.
3340            // It has a contested unique index on `normalizedLabel` and a fixed
3341            // byteArray `preorderSalt` {minItems:32, maxItems:32}.
3342            let contract = setup_contract(
3343                &platform.drive,
3344                CUSTOM_CONTESTED_CONTRACT,
3345                None,
3346                Some(contract_owner.0.id().to_buffer()),
3347                None::<fn(&mut DataContract)>,
3348                None,
3349                Some(platform_version),
3350            );
3351
3352            let name = "quantum";
3353
3354            let platform_state = platform.state.load();
3355
3356            // (2) Create TWO contested documents (contenders) with the same
3357            // contested-index values and a 0xFF-prefixed 32-byte byteArray.
3358            let (preorder_tx_1, domain_tx_1) = build_preorder_and_domain(
3359                &contract,
3360                &identity_1_info.0,
3361                &identity_1_info.1,
3362                &identity_1_info.2,
3363                name,
3364                0x01,
3365                &mut rng,
3366                platform_version,
3367            )
3368            .await;
3369
3370            let (preorder_tx_2, domain_tx_2) = build_preorder_and_domain(
3371                &contract,
3372                &identity_2_info.0,
3373                &identity_2_info.1,
3374                &identity_2_info.2,
3375                name,
3376                0x02,
3377                &mut rng,
3378                platform_version,
3379            )
3380            .await;
3381
3382            // Submit the preorders.
3383            let transaction = platform.drive.grove.start_transaction();
3384            let processing_result = platform
3385                .platform
3386                .process_raw_state_transitions(
3387                    &[preorder_tx_1, preorder_tx_2],
3388                    &platform_state,
3389                    &BlockInfo::default_with_time(
3390                        platform_state
3391                            .last_committed_block_time_ms()
3392                            .unwrap_or_default()
3393                            + 3000,
3394                    ),
3395                    &transaction,
3396                    platform_version,
3397                    false,
3398                    None,
3399                )
3400                .expect("expected to process preorder state transitions");
3401            platform
3402                .drive
3403                .grove
3404                .commit_transaction(transaction)
3405                .unwrap()
3406                .expect("expected to commit transaction");
3407            assert_eq!(
3408                processing_result.valid_count(),
3409                2,
3410                "both preorders should be accepted"
3411            );
3412
3413            // Submit the domains -> this opens the contested vote poll.
3414            let transaction = platform.drive.grove.start_transaction();
3415            let processing_result = platform
3416                .platform
3417                .process_raw_state_transitions(
3418                    &[domain_tx_1, domain_tx_2],
3419                    &platform_state,
3420                    &BlockInfo::default_with_time(
3421                        platform_state
3422                            .last_committed_block_time_ms()
3423                            .unwrap_or_default()
3424                            + 3000,
3425                    ),
3426                    &transaction,
3427                    platform_version,
3428                    false,
3429                    None,
3430                )
3431                .expect("expected to process domain state transitions");
3432            platform
3433                .drive
3434                .grove
3435                .commit_transaction(transaction)
3436                .unwrap()
3437                .expect("expected to commit transaction");
3438            assert_eq!(
3439                processing_result.valid_count(),
3440                2,
3441                "both contenders should be accepted, opening the contest"
3442            );
3443
3444            // (3) Simulate the malicious DataContractUpdate that widens the
3445            // byteArray `maxItems` from 32 to 64 (only in the attack scenario).
3446            if widen {
3447                // The JSON-schema compatibility layer still treats widening
3448                // `maxItems` as a compatible change -- which is exactly why the
3449                // dedicated byte-array-encoding check added to `validate_update`
3450                // (the fix) is required to reject it. We pin that compatibility
3451                // verdict here to document the gap the fix closes.
3452                let original_domain_schema = serde_json::json!({
3453                    "type": "object",
3454                    "properties": {
3455                        "preorderSalt": {
3456                            "type": "array",
3457                            "byteArray": true,
3458                            "minItems": 32,
3459                            "maxItems": 32,
3460                            "position": 4
3461                        }
3462                    },
3463                    "additionalProperties": false
3464                });
3465                let widened_domain_schema = serde_json::json!({
3466                    "type": "object",
3467                    "properties": {
3468                        "preorderSalt": {
3469                            "type": "array",
3470                            "byteArray": true,
3471                            "minItems": 32,
3472                            "maxItems": 64,
3473                            "position": 4
3474                        }
3475                    },
3476                    "additionalProperties": false
3477                });
3478                let compatibility = validate_schema_compatibility(
3479                    &original_domain_schema,
3480                    &widened_domain_schema,
3481                    platform_version,
3482                )
3483                .expect("schema compatibility validation must not error");
3484                assert!(
3485                    compatibility.is_valid(),
3486                    "the maxItems 32 -> 64 widening must be accepted by update \
3487                     validation for this attack to be reachable on chain; \
3488                     reported incompatibilities: {:?}",
3489                    compatibility.errors
3490                );
3491
3492                // Apply the widened contract directly to grovedb to simulate the
3493                // corrupt on-disk state that WOULD result if such an update were
3494                // committed. On a real chain `validate_update` now rejects this
3495                // update at the source (see the rs-dpp validate_byte_array_encoding
3496                // tests), so the resolver is never reached with corrupt bytes;
3497                // this reproduces the consequence the fix prevents.
3498                // The derived `properties` for `preorderSalt` flip to the
3499                // variable-length (varint-prefixed) decode path.
3500                let widened_contract = setup_contract(
3501                    &platform.drive,
3502                    CUSTOM_CONTESTED_CONTRACT_WIDENED,
3503                    None,
3504                    Some(contract_owner.0.id().to_buffer()),
3505                    None::<fn(&mut DataContract)>,
3506                    None,
3507                    Some(platform_version),
3508                );
3509                assert_eq!(
3510                    widened_contract.id(),
3511                    contract.id(),
3512                    "the widened contract must replace the original at the same id"
3513                );
3514            }
3515
3516            // (4) Advance past the vote-poll end date and invoke the resolver.
3517            let time_after_distribution_limit = platform_version
3518                .dpp
3519                .voting_versions
3520                .default_vote_poll_time_duration_test_network_ms
3521                + 10_000;
3522
3523            fast_forward_to_block(&platform, time_after_distribution_limit, 900, 42, 0, false);
3524
3525            let platform_state = platform.state.load();
3526            let transaction = platform.drive.grove.start_transaction();
3527
3528            // This is the exact per-block call. In the attack scenario it returns
3529            // Err because the resolver loads the OLD contender bytes and decodes
3530            // them against the WIDENED `preorderSalt` type, misreading the 0xFF
3531            // first byte as a varint length -> CorruptedSerialization.
3532            platform.check_for_ended_vote_polls(
3533                &platform_state,
3534                &platform_state,
3535                &BlockInfo {
3536                    time_ms: time_after_distribution_limit,
3537                    height: 900,
3538                    core_height: 42,
3539                    epoch: Default::default(),
3540                },
3541                Some(&transaction),
3542                platform_version,
3543            )
3544        }
3545
3546        /// THE CHAIN-HALT CONSEQUENCE. If a byteArray `maxItems` widening were
3547        /// ever committed, the stored contender documents become undecodable and
3548        /// the per-block vote-poll resolver returns `Err`, which propagates out of
3549        /// the bare-`?` per-block event handler and halts every validator.
3550        ///
3551        /// The fix prevents that state at the source: `validate_update` now
3552        /// rejects the widening (see the rs-dpp validate_byte_array_encoding
3553        /// tests). This test deliberately writes the corrupt state directly to
3554        /// grovedb (bypassing validation) to reproduce the consequence the
3555        /// validation fix prevents.
3556        #[tokio::test]
3557        async fn widening_byte_array_max_items_halts_vote_poll_resolver() {
3558            let result = run_contest_then_resolve(true).await;
3559
3560            assert!(
3561                result.is_err(),
3562                "check_for_ended_vote_polls must return Err when stored contender \
3563                 documents are decoded against a widened byteArray encoding -- the \
3564                 chain-halt consequence the `validate_update` fix prevents by \
3565                 rejecting the update at the source. Got Ok instead."
3566            );
3567
3568            // Confirm it is the expected decode-failure variant from the encoding
3569            // flip, not some unrelated error. Matching the variant (rather than the
3570            // Debug string) keeps the test robust to message/format changes.
3571            assert_matches!(
3572                result.unwrap_err(),
3573                crate::error::Error::Protocol(dpp::ProtocolError::DataContractError(
3574                    dpp::data_contract::errors::DataContractError::CorruptedSerialization(_)
3575                        | dpp::data_contract::errors::DataContractError::DecodingContractError(_)
3576                ))
3577            );
3578        }
3579
3580        /// CONTROL (causation proof). The exact same contest, but WITHOUT the
3581        /// byteArray widening, resolves successfully (`Ok`). Together with the
3582        /// test above this proves the widening is what causes the halt -- the
3583        /// only difference between the two runs is the `maxItems` flip.
3584        #[tokio::test]
3585        async fn contest_without_widening_resolves_successfully() {
3586            let result = run_contest_then_resolve(false).await;
3587
3588            assert!(
3589                result.is_ok(),
3590                "control: without the byteArray widening the vote-poll resolver \
3591                 must succeed; got {:?}",
3592                result
3593            );
3594        }
3595    }
3596
3597    /// End-to-end regression test for the contested-index cross-check.
3598    ///
3599    /// The index name in a create transition's `prefunded_voting_balance` is
3600    /// what keys the vote poll, its stored info, its end-date entry and its
3601    /// prefunded specialized balance, while the contested index tree the
3602    /// contender is inserted into always comes from
3603    /// `DocumentType::find_contested_index`. Before the cross-check nothing
3604    /// tied the two together.
3605    ///
3606    /// The contract used here has a non-contested index (`parentName`) whose
3607    /// properties are a strict prefix of the contested index
3608    /// (`parentNameAndLabel`). That is what makes the mismatch survive: the
3609    /// bogus vote poll's contenders path is a tree the contested insert
3610    /// creates anyway, so the poll is registered — one level above the contest
3611    /// it describes.
3612    ///
3613    /// Run against the pre-fix structure validation (v0) this exact test shows
3614    /// the transition executing successfully and the resolver then returning
3615    /// `Err(Drive(CorruptedCodeExecution("expected a locked tally")))`, because
3616    /// the tally query finds no lock/abstain sum trees where the poll points.
3617    /// That `Err` propagates through `run_dao_platform_events` ->
3618    /// `run_block_proposal` (bare `?`), so it is not caught into a
3619    /// per-state-transition result — it halts every validator at the block
3620    /// where the poll ends. The end-date entry is never cleaned up, so every
3621    /// subsequent block proposal fails the same way.
3622    mod contested_index_mismatch_chain_halt {
3623        use super::*;
3624        use crate::config::PlatformConfig;
3625        use crate::test::helpers::fast_forward_to_block::fast_forward_to_block;
3626        use dpp::consensus::state::state_error::StateError;
3627        use dpp::consensus::ConsensusError;
3628        use dpp::dashcore::Network;
3629        use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
3630        use dpp::state_transition::batch_transition::document_create_transition::DocumentCreateTransitionV0;
3631        use dpp::state_transition::batch_transition::{
3632            BatchTransitionV0, DocumentCreateTransition,
3633        };
3634
3635        /// A contested `domain` document type that also carries a
3636        /// non-contested `parentName` index on `[normalizedParentDomainName]`,
3637        /// a strict prefix of the contested `[normalizedParentDomainName,
3638        /// normalizedLabel]`.
3639        const CONTRACT_WITH_PREFIX_INDEX: &str =
3640            "tests/supporting_files/contract/dpns/dpns-contract-contested-unique-index-and-prefix-index.json";
3641
3642        /// Opens a contest on the prefix-index contract with the voting
3643        /// balance prefunded for `prefunded_index`, then advances past the
3644        /// vote poll end date and runs the per-block resolver.
3645        ///
3646        /// Returns the execution result of the create transition and, when it
3647        /// was executed, the resolver's result.
3648        async fn run_contest_then_resolve(
3649            prefunded_index: &str,
3650        ) -> (
3651            StateTransitionExecutionResult,
3652            Option<Result<(), crate::error::Error>>,
3653        ) {
3654            // Mainnet: on testnet the contested structure validation is skipped
3655            // altogether before epoch 2080.
3656            let platform_config = PlatformConfig {
3657                network: Network::Mainnet,
3658                ..Default::default()
3659            };
3660            let mut platform = TestPlatformBuilder::new()
3661                .with_latest_protocol_version()
3662                .with_config(platform_config)
3663                .build_with_mock_rpc()
3664                .set_initial_state_structure();
3665
3666            let platform_version = PlatformVersion::latest();
3667
3668            let mut rng = StdRng::seed_from_u64(0x0C01_7E57);
3669
3670            let (identity, signer, key) =
3671                setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5));
3672            let contract_owner = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5));
3673
3674            let contract = setup_contract(
3675                &platform.drive,
3676                CONTRACT_WITH_PREFIX_INDEX,
3677                None,
3678                Some(contract_owner.0.id().to_buffer()),
3679                None::<fn(&mut DataContract)>,
3680                None,
3681                Some(platform_version),
3682            );
3683
3684            let domain = contract
3685                .document_type_for_name("domain")
3686                .expect("expected a domain document type");
3687
3688            let entropy = Bytes32::random_with_rng(&mut rng);
3689
3690            let mut document = domain
3691                .random_document_with_identifier_and_entropy(
3692                    &mut rng,
3693                    identity.id(),
3694                    entropy,
3695                    DocumentFieldFillType::FillIfNotRequired,
3696                    DocumentFieldFillSize::AnyDocumentFillSize,
3697                    platform_version,
3698                )
3699                .expect("expected a random domain document");
3700
3701            document.set("parentDomainName", "dash".into());
3702            document.set("normalizedParentDomainName", "dash".into());
3703            document.set("label", "quantum".into());
3704            document.set("normalizedLabel", "quantum".into());
3705            document.set("records.identity", document.owner_id().into());
3706            document.set("subdomainRules.allowSubdomains", false.into());
3707            document.set("preorderSalt", rng.gen::<[u8; 32]>().into());
3708
3709            let owner_id = document.owner_id();
3710            let create_transition: DocumentCreateTransition = DocumentCreateTransitionV0 {
3711                base: DocumentBaseTransition::from_document(
3712                    &document,
3713                    domain,
3714                    None,
3715                    2,
3716                    platform_version,
3717                    None,
3718                )
3719                .expect("expected a base transition"),
3720                entropy: entropy.0,
3721                data: document.clone().properties_consumed(),
3722                prefunded_voting_balance: Some((
3723                    prefunded_index.to_string(),
3724                    platform_version
3725                        .fee_version
3726                        .vote_resolution_fund_fees
3727                        .contested_document_vote_resolution_fund_required_amount,
3728                )),
3729            }
3730            .into();
3731
3732            let inner_transition: BatchTransition = BatchTransitionV0 {
3733                owner_id,
3734                transitions: vec![create_transition.into()],
3735                user_fee_increase: 0,
3736                signature_public_key_id: 0,
3737                signature: Default::default(),
3738            }
3739            .into();
3740            let mut transition: StateTransition = inner_transition.into();
3741            transition
3742                .sign_external(&key, &signer, Some(|_, _| Ok(SecurityLevel::HIGH)))
3743                .await
3744                .expect("expected to sign");
3745
3746            let serialized_transition = transition
3747                .serialize_to_bytes()
3748                .expect("expected to serialize the transition");
3749
3750            let platform_state = platform.state.load();
3751            let transaction = platform.drive.grove.start_transaction();
3752
3753            let processing_result = platform
3754                .platform
3755                .process_raw_state_transitions(
3756                    &[serialized_transition],
3757                    &platform_state,
3758                    &BlockInfo::default_with_time(3000),
3759                    &transaction,
3760                    platform_version,
3761                    false,
3762                    None,
3763                )
3764                .expect("expected to process the state transition");
3765
3766            platform
3767                .drive
3768                .grove
3769                .commit_transaction(transaction)
3770                .unwrap()
3771                .expect("expected to commit transaction");
3772
3773            let execution_result = processing_result.into_execution_results().remove(0);
3774
3775            if !matches!(
3776                execution_result,
3777                StateTransitionExecutionResult::SuccessfulExecution { .. }
3778            ) {
3779                return (execution_result, None);
3780            }
3781
3782            let after_vote_poll_end = platform_version
3783                .dpp
3784                .voting_versions
3785                .default_vote_poll_time_duration_mainnet_ms
3786                + 10_000;
3787
3788            fast_forward_to_block(&platform, after_vote_poll_end, 900, 42, 0, false);
3789
3790            let platform_state = platform.state.load();
3791            let transaction = platform.drive.grove.start_transaction();
3792
3793            let resolver_result = platform.check_for_ended_vote_polls(
3794                &platform_state,
3795                &platform_state,
3796                &BlockInfo {
3797                    time_ms: after_vote_poll_end,
3798                    height: 900,
3799                    core_height: 42,
3800                    epoch: Default::default(),
3801                },
3802                Some(&transaction),
3803                platform_version,
3804            );
3805
3806            (execution_result, Some(resolver_result))
3807        }
3808
3809        /// THE FIX. A vote poll prefunded for `parentName` describes a
3810        /// different resource than the contest the document would create, so
3811        /// the transition never executes and no vote poll is registered.
3812        #[tokio::test]
3813        async fn prefunding_a_prefix_index_is_rejected() {
3814            let (execution_result, resolver_result) = run_contest_then_resolve("parentName").await;
3815
3816            assert_matches!(
3817                execution_result,
3818                StateTransitionExecutionResult::PaidConsensusError {
3819                    error: ConsensusError::StateError(
3820                        StateError::DocumentContestIndexMismatchError(_)
3821                    ),
3822                    ..
3823                },
3824                "a prefunded voting balance naming an index other than the contested one must be \
3825                 rejected before it can register a vote poll"
3826            );
3827
3828            assert!(
3829                resolver_result.is_none(),
3830                "the transition must not have executed"
3831            );
3832        }
3833
3834        /// CONTROL (causation proof). The exact same contest prefunded for the
3835        /// contested index executes, and its vote poll resolves without error
3836        /// when it ends. The only difference between the two runs is the index
3837        /// name in the prefunded voting balance.
3838        #[tokio::test]
3839        async fn prefunding_the_contested_index_resolves_successfully() {
3840            let (execution_result, resolver_result) =
3841                run_contest_then_resolve("parentNameAndLabel").await;
3842
3843            assert_matches!(
3844                execution_result,
3845                StateTransitionExecutionResult::SuccessfulExecution { .. }
3846            );
3847
3848            let resolver_result =
3849                resolver_result.expect("expected the vote poll resolver to have run");
3850
3851            assert!(
3852                resolver_result.is_ok(),
3853                "control: a contest whose vote poll names its own contested index must resolve; \
3854                 got {:?}",
3855                resolver_result
3856            );
3857        }
3858    }
3859}