Skip to main content

dpp/state_transition/
serialization.rs

1use crate::serialization::PlatformDeserializableUntrusted;
2use crate::state_transition::*;
3use crate::ProtocolError;
4
5impl StateTransition {
6    pub fn deserialize_many_untrusted(
7        raw_state_transitions: &[Vec<u8>],
8    ) -> Result<Vec<Self>, ProtocolError> {
9        raw_state_transitions
10            .iter()
11            .map(|raw_state_transition| {
12                Self::deserialize_from_bytes_untrusted(raw_state_transition)
13            })
14            .collect()
15    }
16
17    /// Decodes one transition of `state_transition_type` serialized on its own, without the
18    /// `StateTransition` variant tag in front (what `IdentityUpdateTransition::serialize_to_bytes`
19    /// produces, for example). Bytes left over after the transition are refused, and the
20    /// `StateTransition` byte budget is applied to the body up front.
21    pub fn deserialize_untagged_untrusted_exact(
22        state_transition_type: StateTransitionType,
23        bytes: &[u8],
24    ) -> Result<Self, ProtocolError> {
25        // The inner transition types declare no budget of their own, so the `StateTransition`
26        // one is applied here: an untagged body is the tagged transition less its one-byte tag.
27        if bytes.len() >= STATE_TRANSITION_MAX_ENCODED_BYTES {
28            return Err(ProtocolError::MaxEncodedBytesReachedError {
29                max_size_kbytes: STATE_TRANSITION_MAX_ENCODED_BYTES,
30                size_hit: bytes.len(),
31            });
32        }
33        let state_transition: Self = match state_transition_type {
34            StateTransitionType::DataContractCreate => {
35                DataContractCreateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
36            }
37            StateTransitionType::DataContractUpdate => {
38                DataContractUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
39            }
40            StateTransitionType::Batch => {
41                BatchTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
42            }
43            StateTransitionType::IdentityCreate => {
44                IdentityCreateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
45            }
46            StateTransitionType::IdentityTopUp => {
47                IdentityTopUpTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
48            }
49            StateTransitionType::IdentityCreditWithdrawal => {
50                IdentityCreditWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?
51                    .into()
52            }
53            StateTransitionType::IdentityUpdate => {
54                IdentityUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
55            }
56            StateTransitionType::IdentityCreditTransfer => {
57                IdentityCreditTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?
58                    .into()
59            }
60            StateTransitionType::MasternodeVote => {
61                MasternodeVoteTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
62            }
63            StateTransitionType::IdentityCreditTransferToAddresses => {
64                IdentityCreditTransferToAddressesTransition::deserialize_from_bytes_untrusted_exact(
65                    bytes,
66                )?
67                .into()
68            }
69            StateTransitionType::IdentityCreateFromAddresses => {
70                IdentityCreateFromAddressesTransition::deserialize_from_bytes_untrusted_exact(
71                    bytes,
72                )?
73                .into()
74            }
75            StateTransitionType::IdentityTopUpFromAddresses => {
76                IdentityTopUpFromAddressesTransition::deserialize_from_bytes_untrusted_exact(bytes)?
77                    .into()
78            }
79            StateTransitionType::AddressFundsTransfer => {
80                AddressFundsTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?
81                    .into()
82            }
83            StateTransitionType::AddressFundingFromAssetLock => {
84                AddressFundingFromAssetLockTransition::deserialize_from_bytes_untrusted_exact(
85                    bytes,
86                )?
87                .into()
88            }
89            StateTransitionType::AddressCreditWithdrawal => {
90                AddressCreditWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?
91                    .into()
92            }
93            StateTransitionType::Shield => {
94                ShieldTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
95            }
96            StateTransitionType::ShieldedTransfer => {
97                ShieldedTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
98            }
99            StateTransitionType::Unshield => {
100                UnshieldTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
101            }
102            StateTransitionType::ShieldFromAssetLock => {
103                ShieldFromAssetLockTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
104            }
105            StateTransitionType::ShieldedWithdrawal => {
106                ShieldedWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
107            }
108            StateTransitionType::IdentityCreateFromShieldedPool => {
109                IdentityCreateFromShieldedPoolTransition::deserialize_from_bytes_untrusted_exact(
110                    bytes,
111                )?
112                .into()
113            }
114            StateTransitionType::ShieldFromIdentity => {
115                ShieldFromIdentityTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
116            }
117            StateTransitionType::IdentityTopUpFromShieldedPool => {
118                IdentityTopUpFromShieldedPoolTransition::deserialize_from_bytes_untrusted_exact(
119                    bytes,
120                )?
121                .into()
122            }
123            StateTransitionType::TokenShieldedTransferWithShieldedFee => {
124                TokenShieldedTransferWithShieldedFeeTransition::deserialize_from_bytes_untrusted_exact(
125                    bytes,
126                )?
127                .into()
128            }
129            StateTransitionType::TokenUnshieldWithShieldedFee => {
130                TokenUnshieldWithShieldedFeeTransition::deserialize_from_bytes_untrusted_exact(
131                    bytes,
132                )?
133                .into()
134            }
135            StateTransitionType::TokenPurchaseFromShieldedPool => {
136                TokenPurchaseFromShieldedPoolTransition::deserialize_from_bytes_untrusted_exact(
137                    bytes,
138                )?
139                .into()
140            }
141            StateTransitionType::IdentityKeyLimitsUpdate => {
142                IdentityKeyLimitsUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?
143                    .into()
144            }
145            StateTransitionType::ContractUserModeration => {
146                ContractUserModerationTransition::deserialize_from_bytes_untrusted_exact(bytes)?
147                    .into()
148            }
149            StateTransitionType::ContractFeeClaim => {
150                ContractFeeClaimTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
151            }
152        };
153        // Every arm converts into `StateTransition`, so a mismatched arm would still compile.
154        if state_transition.state_transition_type() != state_transition_type {
155            return Err(ProtocolError::CorruptedCodeExecution(format!(
156                "untagged {state_transition_type} bytes decoded as {}",
157                state_transition.state_transition_type()
158            )));
159        }
160        Ok(state_transition)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use hex::ToHex;
167    use base64::engine::general_purpose::STANDARD;
168    use base64::Engine;
169    use platform_value::string_encoding::Encoding;
170    use platform_value::{Identifier, Value};
171    use crate::bls::native_bls::NativeBlsModule;
172    use crate::data_contract::accessors::v0::DataContractV0Getters;
173    use crate::identity::state_transition::AssetLockProved;
174    use crate::identity::accessors::IdentityGettersV0;
175    use crate::identity::core_script::CoreScript;
176    use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
177    use crate::identity::Identity;
178    use crate::prelude::AssetLockProof;
179    use crate::serialization::PlatformMessageSignable;
180    use crate::serialization::Signable;
181    use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
182    use crate::state_transition::data_contract_create_transition::DataContractCreateTransition;
183    use crate::state_transition::data_contract_update_transition::{
184        DataContractUpdateTransition, DataContractUpdateTransitionV0,
185    };
186    use crate::state_transition::batch_transition::batched_transition::document_create_transition::{
187        DocumentCreateTransition, DocumentCreateTransitionV0,
188    };
189    use crate::state_transition::batch_transition::batched_transition::document_transition::{
190        DocumentTransition, DocumentTransitionV0Methods,
191    };
192    use crate::state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType;
193    use crate::state_transition::batch_transition::batched_transition::BatchedTransition;
194    use crate::state_transition::batch_transition::{
195        BatchTransition, BatchTransitionV1,
196    };
197    use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0;
198    use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
199    use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0;
200    use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0;
201    use crate::state_transition::identity_create_transition::IdentityCreateTransition;
202    use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
203    use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0;
204    use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0;
205    use crate::state_transition::identity_update_transition::IdentityUpdateTransition;
206    use crate::state_transition::StateTransitionType;
207    use crate::state_transition::STATE_TRANSITION_MAX_ENCODED_BYTES;
208    use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
209    use crate::state_transition::StateTransition;
210    use crate::tests::fixtures::{
211        get_data_contract_fixture, get_batched_transitions_fixture,
212        get_extended_documents_fixture_with_owner_id_from_contract,
213        raw_instant_asset_lock_proof_fixture,
214    };
215    use crate::version::PlatformVersion;
216    use crate::withdrawal::Pooling;
217    use crate::ProtocolError;
218    use platform_version::version::LATEST_PLATFORM_VERSION;
219    use platform_version::TryIntoPlatformVersioned;
220    use rand::rngs::StdRng;
221    use rand::SeedableRng;
222    use std::collections::BTreeMap;
223
224    #[test]
225    /// Given mainnet transaction 6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D,
226    /// when deserialized, it should be identity create transition.
227    fn should_build_identity_create_transition_from_mainnet_asset_lock_transaction() {
228        const EXPECTED_STATE_TRANSITION_HASH: &str =
229            "6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D";
230        const RAW_TRANSACTION_BASE64: &str = "AwADAAAAAAAAACEDeLqSkwVyfHvYThgegiZUvPu0+dU4kyd3PJKigGLC1spBH+wrzjjA/ZGZdQmUzpQyOiC3GyP2eBp8ga9cNlnIOkptMzAtfXPA2daH3xTqt25JQ+fZ6UKB3ypzTK3fOXaAATgAAQAAAgAAIQPoVeBC6iyS0jFV0Dly5WV0SEl6uDciQqqi4EATeUJutEEfAd6+/HbUM4FLS6+lNc6AH8vaD9lViiYny4GPsl/AlBxdr0WjJxxU/B0cNVH8kRMo+W6a+1iSN+NZS7MTyzmTHwACAAEDAAAhA6S0TKbm1a/xyrYMG+Y2odspJ1roL1TcoK9h552yE1VCQSA+KpHiQ8lDBseXI/1ZCMxEvu0qopdjDojaQ4FzaZMgUGfPBeXSfMbQGksLMNseKRBLob/g0DHJWqZAxSDOuAwZAfwAIQxGIDIHY9cjWxS0tJupeJuKMZwzFKmLxkU3NmqFTcFscilVAABBH9R3vwbfA3q5XJG4m4z87OAA1uG8wup915wGGKAxdEObXPSqIvPBWrHlGTf/Uymanc2cDH1uKdsniJyoORwauPBIqlz61/Kf9HDnubX4GoHRYdnb4WzE+Tdh+L39a2dN2A==";
231        const EXPECTED_IDENTITY_ADDRESS: &str = "5tf2QotaJw8kRNpQEa8TXtRQ6FLxwUrY4Mtee2JF2nco";
232        const EXPECTED_CORE_TRANSACTION_HASH: &str =
233            "5529726cc14d856a363745c68ba914339c318a9b78a99bb4b4145b23d7630732";
234        let raw_transaction = STANDARD
235            .decode(RAW_TRANSACTION_BASE64)
236            .expect("base64 transaction should decode");
237        let state_transition = StateTransition::deserialize_from_bytes_untrusted(&raw_transaction)
238            .expect("State transition deserializes correctly");
239
240        assert_eq!(
241            &state_transition
242                .transaction_id()
243                .expect("expected transaction id")
244                .encode_hex_upper::<String>(),
245            EXPECTED_STATE_TRANSITION_HASH
246        );
247
248        let StateTransition::IdentityCreate(identity_create_transition) = state_transition else {
249            panic!("expected identity create transition");
250        };
251
252        // This mainnet transaction uses a ChainAssetLockProof (not InstantAssetLockProof)
253        // ChainAssetLockProof doesn't embed the full transaction, just the out_point reference
254        let asset_lock_proof = identity_create_transition.asset_lock_proof();
255        let AssetLockProof::Chain(chain_proof) = asset_lock_proof else {
256            panic!("expected chain asset lock proof for this mainnet transaction");
257        };
258
259        // Verify the out_point references the expected transaction
260        assert_eq!(
261            &chain_proof.out_point.txid.to_string().to_lowercase(),
262            EXPECTED_CORE_TRANSACTION_HASH
263        );
264
265        let identity_address = identity_create_transition
266            .identity_id()
267            .to_string(Encoding::Base58);
268
269        assert_eq!(identity_address, EXPECTED_IDENTITY_ADDRESS);
270    }
271
272    #[test]
273    #[cfg(feature = "random-identities")]
274    fn identity_create_transition_ser_de() {
275        let platform_version = LATEST_PLATFORM_VERSION;
276        let identity = Identity::random_identity(5, Some(5), platform_version)
277            .expect("expected a random identity");
278        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
279
280        let identity_create_transition = IdentityCreateTransition::V0(
281            IdentityCreateTransitionV0::try_from_identity(
282                &identity,
283                AssetLockProof::Instant(asset_lock_proof),
284                platform_version,
285            )
286            .expect("expected to make an identity create transition"),
287        );
288
289        let state_transition: StateTransition = identity_create_transition.into();
290        let bytes = state_transition
291            .serialize_to_bytes()
292            .expect("expected to serialize");
293        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
294            .expect("expected to deserialize state transition");
295        assert_eq!(state_transition, recovered_state_transition);
296    }
297
298    #[test]
299    #[cfg(feature = "random-identities")]
300    fn identity_topup_transition_ser_de() {
301        let platform_version = PlatformVersion::latest();
302        let identity = Identity::random_identity(5, Some(5), platform_version)
303            .expect("expected a random identity");
304        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
305
306        let identity_topup_transition = IdentityTopUpTransitionV0 {
307            asset_lock_proof: AssetLockProof::Instant(asset_lock_proof),
308            identity_id: identity.id(),
309            user_fee_increase: 0,
310            signature: [1u8; 65].to_vec().into(),
311        };
312        let state_transition: StateTransition = identity_topup_transition.into();
313        let bytes = state_transition
314            .serialize_to_bytes()
315            .expect("expected to serialize");
316        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
317            .expect("expected to deserialize state transition");
318        assert_eq!(state_transition, recovered_state_transition);
319    }
320
321    #[test]
322    #[cfg(feature = "random-identities")]
323    fn identity_update_transition_add_keys_ser_de() {
324        let mut rng = StdRng::seed_from_u64(5);
325        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
326            Identity::random_identity_with_main_keys_with_private_key(
327                5,
328                &mut rng,
329                LATEST_PLATFORM_VERSION,
330            )
331            .expect("expected to get identity");
332        let bls = NativeBlsModule;
333        let add_public_keys_in_creation = identity
334            .public_keys()
335            .values()
336            .map(|public_key| public_key.into())
337            .collect();
338        let mut identity_update_transition = IdentityUpdateTransitionV0 {
339            signature: Default::default(),
340            signature_public_key_id: 0,
341            identity_id: identity.id(),
342            revision: 1,
343            nonce: 1,
344            add_public_keys: add_public_keys_in_creation,
345            disable_public_keys: vec![],
346            user_fee_increase: 0,
347        };
348
349        let key_signable_bytes = identity_update_transition
350            .signable_bytes()
351            .expect("expected to get signable bytes");
352
353        identity_update_transition
354            .add_public_keys
355            .iter_mut()
356            .zip(identity.public_keys().clone().into_values())
357            .try_for_each(|(public_key_with_witness, public_key)| {
358                if public_key.key_type().is_unique_key_type() {
359                    let private_key = keys
360                        .get(&public_key)
361                        .expect("expected to have the private key");
362                    let signature = key_signable_bytes
363                        .as_slice()
364                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
365                        .into();
366                    public_key_with_witness.set_signature(signature);
367                }
368
369                Ok::<(), ProtocolError>(())
370            })
371            .expect("expected to update keys");
372
373        let (public_key, private_key) = keys.pop_first().unwrap();
374
375        let mut state_transition: StateTransition = identity_update_transition.into();
376
377        state_transition
378            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
379            .expect("expected to sign IdentityUpdateTransition");
380        let bytes = state_transition
381            .serialize_to_bytes()
382            .expect("expected to serialize");
383        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
384            .expect("expected to deserialize state transition");
385        assert_eq!(state_transition, recovered_state_transition);
386    }
387
388    #[test]
389    #[cfg(feature = "state-transition-signing")]
390    fn identity_update_transition_disable_keys_ser_de() {
391        let mut rng = StdRng::seed_from_u64(5);
392        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
393            Identity::random_identity_with_main_keys_with_private_key(
394                5,
395                &mut rng,
396                LATEST_PLATFORM_VERSION,
397            )
398            .expect("expected to get identity");
399        let bls = NativeBlsModule;
400        let add_public_keys_in_creation = identity
401            .public_keys()
402            .values()
403            .map(|public_key| public_key.into())
404            .collect();
405        let mut identity_update_transition = IdentityUpdateTransitionV0 {
406            signature: Default::default(),
407            signature_public_key_id: 0,
408            identity_id: identity.id(),
409            revision: 1,
410            nonce: 1,
411            add_public_keys: add_public_keys_in_creation,
412            disable_public_keys: vec![3, 4, 5],
413            user_fee_increase: 0,
414        };
415
416        let key_signable_bytes = identity_update_transition
417            .signable_bytes()
418            .expect("expected to get signable bytes");
419
420        identity_update_transition
421            .add_public_keys
422            .iter_mut()
423            .zip(identity.public_keys().clone().into_values())
424            .try_for_each(|(public_key_with_witness, public_key)| {
425                if public_key.key_type().is_unique_key_type() {
426                    let private_key = keys
427                        .get(&public_key)
428                        .expect("expected to have the private key");
429                    let signature = key_signable_bytes
430                        .as_slice()
431                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
432                        .into();
433                    public_key_with_witness.set_signature(signature);
434                }
435
436                Ok::<(), ProtocolError>(())
437            })
438            .expect("expected to update keys");
439
440        let (public_key, private_key) = keys.pop_first().unwrap();
441
442        let mut state_transition: StateTransition = identity_update_transition.into();
443
444        state_transition
445            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
446            .expect("expected to sign IdentityUpdateTransition");
447        let bytes = state_transition
448            .serialize_to_bytes()
449            .expect("expected to serialize");
450        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
451            .expect("expected to deserialize state transition");
452        assert_eq!(state_transition, recovered_state_transition);
453    }
454
455    #[test]
456    #[cfg(feature = "random-identities")]
457    fn identity_credit_withdrawal_transition_ser_de() {
458        let platform_version = PlatformVersion::latest();
459        let identity = Identity::random_identity(5, Some(5), platform_version)
460            .expect("expected a random identity");
461        let identity_credit_withdrawal_transition = IdentityCreditWithdrawalTransitionV0 {
462            identity_id: identity.id(),
463            amount: 5000000,
464            core_fee_per_byte: 34,
465            pooling: Pooling::Standard,
466            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
467            nonce: 1,
468            user_fee_increase: 0,
469            signature_public_key_id: 0,
470            signature: [1u8; 65].to_vec().into(),
471        };
472        let state_transition: StateTransition = identity_credit_withdrawal_transition.into();
473        let bytes = state_transition
474            .serialize_to_bytes()
475            .expect("expected to serialize");
476        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
477            .expect("expected to deserialize state transition");
478        assert_eq!(state_transition, recovered_state_transition);
479    }
480
481    #[test]
482    #[cfg(feature = "random-identities")]
483    fn data_contract_create_ser_de() {
484        let platform_version = LATEST_PLATFORM_VERSION;
485        let identity = Identity::random_identity(5, Some(5), platform_version)
486            .expect("expected a random identity");
487        let created_data_contract = get_data_contract_fixture(
488            Some(identity.id()),
489            0,
490            LATEST_PLATFORM_VERSION.protocol_version,
491        );
492        let data_contract_create_transition: DataContractCreateTransition = created_data_contract
493            .try_into_platform_versioned(platform_version)
494            .expect("expected to transform into a DataContractCreateTransition");
495        let state_transition: StateTransition = data_contract_create_transition.into();
496        let bytes = state_transition
497            .serialize_to_bytes()
498            .expect("expected to serialize");
499        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
500            .expect("expected to deserialize state transition");
501        assert_eq!(state_transition, recovered_state_transition);
502    }
503
504    #[test]
505    #[cfg(feature = "random-identities")]
506    fn data_contract_update_ser_de() {
507        let platform_version = PlatformVersion::latest();
508        let identity = Identity::random_identity(5, Some(5), platform_version)
509            .expect("expected a random identity");
510        let created_data_contract =
511            get_data_contract_fixture(Some(identity.id()), 0, platform_version.protocol_version);
512        let data_contract_update_transition =
513            DataContractUpdateTransition::V0(DataContractUpdateTransitionV0 {
514                identity_contract_nonce: 1,
515                data_contract: created_data_contract
516                    .data_contract_owned()
517                    .try_into_platform_versioned(platform_version)
518                    .expect("expected a data contract"),
519                user_fee_increase: 0,
520                signature_public_key_id: 0,
521                signature: [1u8; 65].to_vec().into(),
522            });
523        let state_transition: StateTransition = data_contract_update_transition.into();
524        let bytes = state_transition
525            .serialize_to_bytes()
526            .expect("expected to serialize");
527        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
528            .expect("expected to deserialize state transition");
529        assert_eq!(state_transition, recovered_state_transition);
530    }
531
532    #[test]
533    fn document_batch_transition_10_created_documents_ser_de() {
534        let platform_version = PlatformVersion::latest();
535
536        let mut nonces = BTreeMap::new();
537        let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
538            .data_contract_owned();
539        let documents = get_extended_documents_fixture_with_owner_id_from_contract(
540            &data_contract,
541            platform_version.protocol_version,
542        )
543        .unwrap();
544        let documents = documents
545            .iter()
546            .map(|extended_document| {
547                let document = extended_document.document().clone();
548                let data_contract = extended_document.data_contract();
549                (
550                    document,
551                    data_contract
552                        .document_type_for_name(extended_document.document_type_name())
553                        .unwrap(),
554                    *extended_document.entropy(),
555                    None,
556                )
557            })
558            .collect::<Vec<_>>();
559        let transitions = get_batched_transitions_fixture(
560            [(DocumentTransitionActionType::Create, documents)],
561            &mut nonces,
562        );
563        let documents_batch_transition: BatchTransition = BatchTransitionV1 {
564            owner_id: data_contract.owner_id(),
565            transitions,
566            ..Default::default()
567        }
568        .into();
569        let state_transition: StateTransition = documents_batch_transition.into();
570        let bytes = state_transition
571            .serialize_to_bytes()
572            .expect("expected to serialize");
573        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
574            .expect("expected to deserialize state transition");
575        assert_eq!(state_transition, recovered_state_transition);
576    }
577
578    /// Stack size for tests that build `Value`s nested to the decoder depth ceiling.
579    ///
580    /// Only decoding is iterative: the derived `Encode`, `PartialEq` and drop glue recurse once
581    /// per nesting level, and in debug builds those frames cost roughly 7 KiB per level, so a
582    /// value ~256 levels deep exhausts libtest's default 2 MiB per-test thread. nextest runs each
583    /// test on the 8 MiB main thread, which is why CI does not see the overflow.
584    const DEEP_VALUE_TEST_STACK_SIZE: usize = 16 * 1024 * 1024;
585
586    fn on_deep_value_stack(test: impl FnOnce() + Send + 'static) {
587        std::thread::Builder::new()
588            .stack_size(DEEP_VALUE_TEST_STACK_SIZE)
589            .spawn(test)
590            .expect("the deep value test thread should spawn")
591            .join()
592            .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
593    }
594
595    #[test]
596    fn document_batch_rejects_excessive_value_depth_during_decode() {
597        on_deep_value_stack(|| {
598            let nested = (0..300).fold(Value::Null, |value, _| Value::Array(vec![value]));
599            let document_transition = DocumentTransition::Create(DocumentCreateTransition::V0(
600                DocumentCreateTransitionV0 {
601                    base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
602                        id: Identifier::default(),
603                        identity_contract_nonce: 1,
604                        document_type_name: "test".to_string(),
605                        data_contract_id: Identifier::default(),
606                    }),
607                    entropy: [0; 32],
608                    data: BTreeMap::from([("nested".to_string(), nested)]),
609                    prefunded_voting_balance: None,
610                },
611            ));
612            assert_eq!(
613                document_transition.first_data_depth_exceeding(256),
614                Some(257)
615            );
616
617            let state_transition = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 {
618                transitions: vec![BatchedTransition::Document(document_transition)],
619                ..Default::default()
620            }));
621            let bytes = state_transition
622                .serialize_to_bytes()
623                .expect("the state transition should encode below the byte limit");
624            assert!(
625                bytes.len() as u64
626                    <= PlatformVersion::latest()
627                        .system_limits
628                        .max_state_transition_size
629            );
630
631            // The intentionally invalid transition is no longer needed after encoding. Avoid walking
632            // its recursive data during drop so this regression test only exercises decoder behavior.
633            std::mem::forget(state_transition);
634
635            let error = StateTransition::deserialize_from_bytes_untrusted_in_version(
636                &bytes,
637                PlatformVersion::latest(),
638            )
639            .expect_err("excessive nesting must be rejected during decode");
640            assert!(error
641                .to_string()
642                .contains("value nesting depth 257 exceeds maximum 256"));
643        });
644    }
645
646    #[test]
647    fn document_batch_value_depth_limits_align_between_decode_and_validation() {
648        on_deep_value_stack(|| {
649            // Every decodable document value must also satisfy the consensus depth rule, so depth
650            // violations always fail the same way: as an undecodable transition. A value at the
651            // decoder ceiling must therefore round-trip and pass the validation-side depth check.
652            let max_depth = PlatformVersion::latest()
653                .system_limits
654                .max_document_value_depth
655                .expect("latest protocol should enforce document value depth")
656                as usize;
657            let nested = (1..max_depth).fold(Value::Array(vec![Value::Null]), |value, _| {
658                Value::Array(vec![value])
659            });
660            let document_transition = DocumentTransition::Create(DocumentCreateTransition::V0(
661                DocumentCreateTransitionV0 {
662                    base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
663                        id: Identifier::default(),
664                        identity_contract_nonce: 1,
665                        document_type_name: "test".to_string(),
666                        data_contract_id: Identifier::default(),
667                    }),
668                    entropy: [0; 32],
669                    data: BTreeMap::from([("nested".to_string(), nested)]),
670                    prefunded_voting_balance: None,
671                },
672            ));
673            assert_eq!(
674                document_transition.first_data_depth_exceeding(max_depth),
675                None
676            );
677            assert_eq!(
678                document_transition.first_data_depth_exceeding(max_depth - 1),
679                Some(max_depth)
680            );
681
682            let state_transition = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 {
683                transitions: vec![BatchedTransition::Document(document_transition)],
684                ..Default::default()
685            }));
686            let bytes = state_transition
687                .serialize_to_bytes()
688                .expect("the state transition should encode below the byte limit");
689
690            let recovered = StateTransition::deserialize_from_bytes_untrusted_in_version(
691                &bytes,
692                PlatformVersion::latest(),
693            )
694            .expect("a value at the decoder ceiling must decode");
695            assert_eq!(state_transition, recovered);
696        });
697    }
698
699    #[test]
700    fn deserialize_empty_bytes_should_fail() {
701        let result = StateTransition::deserialize_from_bytes_untrusted(&[]);
702        assert!(
703            result.is_err(),
704            "deserialization of empty bytes should fail"
705        );
706    }
707
708    #[test]
709    fn deserialize_single_byte_should_fail() {
710        let result = StateTransition::deserialize_from_bytes_untrusted(&[0xFF]);
711        assert!(
712            result.is_err(),
713            "deserialization of a single 0xFF byte should fail"
714        );
715    }
716
717    #[test]
718    #[cfg(feature = "random-identities")]
719    fn deserialize_truncated_bytes_should_fail() {
720        let platform_version = PlatformVersion::latest();
721        let identity = Identity::random_identity(5, Some(5), platform_version)
722            .expect("expected a random identity");
723        let transition = IdentityCreditWithdrawalTransitionV0 {
724            identity_id: identity.id(),
725            amount: 5000000,
726            core_fee_per_byte: 34,
727            pooling: Pooling::Standard,
728            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
729            nonce: 1,
730            user_fee_increase: 0,
731            signature_public_key_id: 0,
732            signature: [1u8; 65].to_vec().into(),
733        };
734        let state_transition: StateTransition = transition.into();
735        let bytes = state_transition
736            .serialize_to_bytes()
737            .expect("expected to serialize");
738
739        // Truncate to half
740        let half = &bytes[..bytes.len() / 2];
741        assert!(
742            StateTransition::deserialize_from_bytes_untrusted(half).is_err(),
743            "deserialization of truncated-to-half bytes should fail"
744        );
745
746        // Truncate by removing last byte
747        let minus_one = &bytes[..bytes.len() - 1];
748        assert!(
749            StateTransition::deserialize_from_bytes_untrusted(minus_one).is_err(),
750            "deserialization of bytes missing last byte should fail"
751        );
752
753        // Keep only first byte
754        let first_only = &bytes[..1];
755        assert!(
756            StateTransition::deserialize_from_bytes_untrusted(first_only).is_err(),
757            "deserialization of only the first byte should fail"
758        );
759    }
760
761    #[test]
762    #[cfg(feature = "random-identities")]
763    fn deserialize_corrupted_bytes_should_not_panic() {
764        let platform_version = PlatformVersion::latest();
765        let identity = Identity::random_identity(5, Some(5), platform_version)
766            .expect("expected a random identity");
767        let transition = IdentityCreditWithdrawalTransitionV0 {
768            identity_id: identity.id(),
769            amount: 5000000,
770            core_fee_per_byte: 34,
771            pooling: Pooling::Standard,
772            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
773            nonce: 1,
774            user_fee_increase: 0,
775            signature_public_key_id: 0,
776            signature: [1u8; 65].to_vec().into(),
777        };
778        let state_transition: StateTransition = transition.into();
779        let mut bytes = state_transition
780            .serialize_to_bytes()
781            .expect("expected to serialize");
782
783        // Flip bits in the middle of the payload
784        let mid = bytes.len() / 2;
785        bytes[mid] ^= 0xFF;
786
787        // Should either fail or return a different value - must not panic
788        let result = StateTransition::deserialize_from_bytes_untrusted(&bytes);
789        if let Ok(recovered) = result {
790            assert_ne!(
791                state_transition, recovered,
792                "corrupted bytes should not deserialize to the original value"
793            );
794        }
795    }
796
797    /// Crafts a minimal payload that looks like a StateTransition variant
798    /// (IdentityCreditWithdrawal = discriminant 5) followed by a version byte
799    /// and then a Vec<u8> field with a varint-encoded length that claims to
800    /// contain `fake_len` bytes. The total payload is small but the decoded
801    /// length would trigger a huge allocation without the limit guard.
802    fn craft_oversized_vec_payload(fake_len: u64) -> Vec<u8> {
803        let config = bincode::config::standard()
804            .with_big_endian()
805            .with_no_limit();
806        // Build the payload: variant discriminant + version + bogus vec length + filler
807        let mut buf = Vec::new();
808        // StateTransition enum discriminant for IdentityCreditWithdrawal (index 5)
809        buf.extend_from_slice(&bincode::encode_to_vec(5u32, config).unwrap());
810        // Version byte (0 = V0)
811        buf.push(0);
812        // identity_id: 32 bytes (Identifier)
813        buf.extend_from_slice(&[0u8; 32]);
814        // amount: u64
815        buf.extend_from_slice(&bincode::encode_to_vec(1000u64, config).unwrap());
816        // core_fee_per_byte: u32
817        buf.extend_from_slice(&bincode::encode_to_vec(1u32, config).unwrap());
818        // pooling: enum variant 0
819        buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
820        // output_script (CoreScript = BinaryData = Vec<u8>): encode the malicious length
821        buf.extend_from_slice(&bincode::encode_to_vec(fake_len, config).unwrap());
822        // Don't provide the actual bytes — the limit check should fire before allocation
823        buf
824    }
825
826    #[test]
827    fn deserialize_crafted_huge_vec_length_does_not_oom() {
828        // Craft a small payload (~80 bytes) with a Vec<u8> field claiming 8 GB.
829        // Without the limit fix this would attempt `vec![0u8; 8_000_000_000]` and abort.
830        let payload = craft_oversized_vec_payload(8_000_000_000);
831        let result = StateTransition::deserialize_from_bytes_untrusted(&payload);
832        // Must return an error, not OOM-abort the process
833        assert!(
834            result.is_err(),
835            "crafted payload with 8GB vec length must be rejected, not cause OOM"
836        );
837    }
838
839    #[test]
840    fn deserialize_crafted_vec_exceeding_limit_is_rejected() {
841        // Craft a payload with a Vec<u8> claiming 200,000 bytes — exceeds the
842        // 100,000 byte budget configured on StateTransition.
843        // The limit causes bincode to reject the read (either as Io/LimitExceeded
844        // or UnexpectedEnd depending on the exact code path). Either way, the
845        // deserialization must fail safely without OOM.
846        let payload = craft_oversized_vec_payload(200_000);
847        let result = StateTransition::deserialize_from_bytes_untrusted(&payload);
848        assert!(
849            result.is_err(),
850            "Vec length exceeding byte budget must be rejected"
851        );
852    }
853
854    #[test]
855    fn deserialize_no_limit_does_not_enforce_byte_budget() {
856        // Same crafted payload with 200,000-byte Vec — the no_limit variant
857        // should NOT reject it for byte budget reasons (it will still fail
858        // because the data doesn't actually contain 200,000 bytes).
859        let payload = craft_oversized_vec_payload(200_000);
860        let result = StateTransition::deserialize_from_bytes_untrusted_no_limit(&payload);
861        assert!(result.is_err());
862        // any other error is fine — the data is garbage
863        if let ProtocolError::MaxEncodedBytesReachedError { .. } = result.unwrap_err() {
864            panic!("deserialize_from_bytes_untrusted_no_limit should NOT enforce byte budget");
865        }
866    }
867
868    #[test]
869    fn deserialize_many_empty_list() {
870        let result = StateTransition::deserialize_many_untrusted(&[]);
871        assert_eq!(result.unwrap(), vec![]);
872    }
873
874    #[test]
875    fn deserialize_many_with_invalid_entry() {
876        let result = StateTransition::deserialize_many_untrusted(&[vec![0xFF]]);
877        assert!(
878            result.is_err(),
879            "deserialize_many with invalid entry should fail"
880        );
881    }
882
883    #[test]
884    #[cfg(feature = "random-identities")]
885    fn deserialize_many_with_valid_entries() {
886        let platform_version = PlatformVersion::latest();
887        let identity = Identity::random_identity(5, Some(5), platform_version)
888            .expect("expected a random identity");
889
890        let make_transition = |amount: u64, nonce: u64| -> StateTransition {
891            let t = IdentityCreditWithdrawalTransitionV0 {
892                identity_id: identity.id(),
893                amount,
894                core_fee_per_byte: 34,
895                pooling: Pooling::Standard,
896                output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
897                nonce,
898                user_fee_increase: 0,
899                signature_public_key_id: 0,
900                signature: [1u8; 65].to_vec().into(),
901            };
902            t.into()
903        };
904
905        let st1 = make_transition(1000000, 1);
906        let st2 = make_transition(2000000, 2);
907        let st3 = make_transition(3000000, 3);
908
909        let raw: Vec<Vec<u8>> = vec![
910            st1.serialize_to_bytes().unwrap(),
911            st2.serialize_to_bytes().unwrap(),
912            st3.serialize_to_bytes().unwrap(),
913        ];
914
915        let recovered =
916            StateTransition::deserialize_many_untrusted(&raw).expect("should deserialize all");
917        assert_eq!(recovered.len(), 3);
918        assert_eq!(recovered[0], st1);
919        assert_eq!(recovered[1], st2);
920        assert_eq!(recovered[2], st3);
921    }
922
923    #[test]
924    #[cfg(feature = "random-identities")]
925    fn exact_decode_refuses_trailing_bytes() {
926        let platform_version = PlatformVersion::latest();
927        let identity = Identity::random_identity(5, Some(5), platform_version)
928            .expect("expected a random identity");
929        let st: StateTransition = IdentityCreditWithdrawalTransitionV0 {
930            identity_id: identity.id(),
931            amount: 1000000,
932            core_fee_per_byte: 34,
933            pooling: Pooling::Standard,
934            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
935            nonce: 1,
936            user_fee_increase: 0,
937            signature_public_key_id: 0,
938            signature: [1u8; 65].to_vec().into(),
939        }
940        .into();
941        let bytes = st.serialize_to_bytes().unwrap();
942
943        assert_eq!(
944            StateTransition::deserialize_from_bytes_untrusted_exact(&bytes).unwrap(),
945            st
946        );
947
948        let mut padded = bytes.clone();
949        padded.push(0);
950        // The derived decoder ignores the suffix; the exact one refuses it.
951        assert_eq!(
952            StateTransition::deserialize_from_bytes_untrusted(&padded).unwrap(),
953            st
954        );
955        assert!(matches!(
956            StateTransition::deserialize_from_bytes_untrusted_exact(&padded),
957            Err(ProtocolError::PlatformDeserializationError(message))
958                if message.contains("1 bytes left over")
959        ));
960    }
961
962    /// A tagged transition followed by another decodes loosely as the first alone; only the
963    /// exact decoder reports the suffix.
964    #[test]
965    fn exact_decode_refuses_a_transition_followed_by_another() {
966        let update: StateTransition = IdentityUpdateTransitionV0 {
967            identity_id: Identifier::from([0x21; 32]),
968            revision: 1,
969            nonce: 1,
970            disable_public_keys: vec![1],
971            ..Default::default()
972        }
973        .into();
974        let withdrawal: StateTransition = IdentityCreditWithdrawalTransitionV0 {
975            identity_id: Identifier::from([0x21; 32]),
976            amount: 1000000,
977            core_fee_per_byte: 34,
978            pooling: Pooling::Standard,
979            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
980            nonce: 2,
981            user_fee_increase: 0,
982            signature_public_key_id: 0,
983            signature: Default::default(),
984        }
985        .into();
986
987        let mut both = update.serialize_to_bytes().unwrap();
988        both.extend_from_slice(&withdrawal.serialize_to_bytes().unwrap());
989
990        assert_eq!(
991            StateTransition::deserialize_from_bytes_untrusted(&both).unwrap(),
992            update
993        );
994        assert!(matches!(
995            StateTransition::deserialize_from_bytes_untrusted_exact(&both),
996            Err(ProtocolError::PlatformDeserializationError(message)) if message.contains("left over")
997        ));
998    }
999
1000    #[test]
1001    #[cfg(feature = "random-identities")]
1002    fn untagged_decode_matches_the_tagged_transition() {
1003        let platform_version = PlatformVersion::latest();
1004        let identity = Identity::random_identity(5, Some(5), platform_version)
1005            .expect("expected a random identity");
1006        let update = IdentityUpdateTransitionV0 {
1007            signature: [7u8; 65].to_vec().into(),
1008            signature_public_key_id: 0,
1009            identity_id: identity.id(),
1010            revision: 1,
1011            nonce: 1,
1012            add_public_keys: identity
1013                .public_keys()
1014                .values()
1015                .map(|public_key| public_key.into())
1016                .collect(),
1017            disable_public_keys: vec![],
1018            user_fee_increase: 0,
1019        };
1020        let inner_bytes = IdentityUpdateTransition::from(update.clone())
1021            .serialize_to_bytes()
1022            .unwrap();
1023        let st: StateTransition = update.into();
1024
1025        let decoded = StateTransition::deserialize_untagged_untrusted_exact(
1026            StateTransitionType::IdentityUpdate,
1027            &inner_bytes,
1028        )
1029        .expect("untagged identity update decodes");
1030        assert_eq!(decoded, st);
1031        assert_eq!(
1032            &decoded.serialize_to_bytes().unwrap()[1..],
1033            &inner_bytes[..]
1034        );
1035
1036        assert!(StateTransition::deserialize_untagged_untrusted_exact(
1037            StateTransitionType::Batch,
1038            &inner_bytes,
1039        )
1040        .is_err());
1041
1042        let mut padded = inner_bytes.clone();
1043        padded.push(0);
1044        assert!(StateTransition::deserialize_untagged_untrusted_exact(
1045            StateTransitionType::IdentityUpdate,
1046            &padded,
1047        )
1048        .is_err());
1049    }
1050
1051    /// The untagged decoder refuses a body at the `StateTransition` byte budget, naming it.
1052    #[test]
1053    fn untagged_decode_honours_the_state_transition_budget() {
1054        let st: StateTransition = IdentityCreditWithdrawalTransitionV0 {
1055            identity_id: Identifier::from([0x21; 32]),
1056            amount: 1000000,
1057            core_fee_per_byte: 34,
1058            pooling: Pooling::Standard,
1059            output_script: CoreScript::from_bytes(vec![0; STATE_TRANSITION_MAX_ENCODED_BYTES]),
1060            nonce: 1,
1061            user_fee_increase: 0,
1062            signature_public_key_id: 0,
1063            signature: Default::default(),
1064        }
1065        .into();
1066        let tagged = bincode::encode_to_vec(&st, bincode::config::standard().with_big_endian())
1067            .expect("encodes");
1068
1069        match StateTransition::deserialize_untagged_untrusted_exact(
1070            StateTransitionType::IdentityCreditWithdrawal,
1071            &tagged[1..],
1072        ) {
1073            Err(ProtocolError::MaxEncodedBytesReachedError {
1074                max_size_kbytes, ..
1075            }) => assert_eq!(max_size_kbytes, STATE_TRANSITION_MAX_ENCODED_BYTES),
1076            other => panic!("expected the budget error, got {other:?}"),
1077        }
1078    }
1079}