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