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