Skip to main content

dpp/state_transition/
serialization.rs

1use crate::serialization::PlatformDeserializableUntrusted;
2use crate::state_transition::StateTransition;
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
18#[cfg(test)]
19mod tests {
20    use hex::ToHex;
21    use base64::engine::general_purpose::STANDARD;
22    use base64::Engine;
23    use platform_value::string_encoding::Encoding;
24    use platform_value::{Identifier, Value};
25    use crate::bls::native_bls::NativeBlsModule;
26    use crate::data_contract::accessors::v0::DataContractV0Getters;
27    use crate::identity::state_transition::AssetLockProved;
28    use crate::identity::accessors::IdentityGettersV0;
29    use crate::identity::core_script::CoreScript;
30    use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
31    use crate::identity::Identity;
32    use crate::prelude::AssetLockProof;
33    use crate::serialization::PlatformMessageSignable;
34    use crate::serialization::Signable;
35    use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
36    use crate::state_transition::data_contract_create_transition::DataContractCreateTransition;
37    use crate::state_transition::data_contract_update_transition::{
38        DataContractUpdateTransition, DataContractUpdateTransitionV0,
39    };
40    use crate::state_transition::batch_transition::batched_transition::document_create_transition::{
41        DocumentCreateTransition, DocumentCreateTransitionV0,
42    };
43    use crate::state_transition::batch_transition::batched_transition::document_transition::{
44        DocumentTransition, DocumentTransitionV0Methods,
45    };
46    use crate::state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType;
47    use crate::state_transition::batch_transition::batched_transition::BatchedTransition;
48    use crate::state_transition::batch_transition::{
49        BatchTransition, BatchTransitionV1,
50    };
51    use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0;
52    use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
53    use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0;
54    use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0;
55    use crate::state_transition::identity_create_transition::IdentityCreateTransition;
56    use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
57    use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0;
58    use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0;
59    use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
60    use crate::state_transition::StateTransition;
61    use crate::tests::fixtures::{
62        get_data_contract_fixture, get_batched_transitions_fixture,
63        get_extended_documents_fixture_with_owner_id_from_contract,
64        raw_instant_asset_lock_proof_fixture,
65    };
66    use crate::version::PlatformVersion;
67    use crate::withdrawal::Pooling;
68    use crate::ProtocolError;
69    use platform_version::version::LATEST_PLATFORM_VERSION;
70    use platform_version::TryIntoPlatformVersioned;
71    use rand::rngs::StdRng;
72    use rand::SeedableRng;
73    use std::collections::BTreeMap;
74
75    #[test]
76    /// Given mainnet transaction 6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D,
77    /// when deserialized, it should be identity create transition.
78    fn should_build_identity_create_transition_from_mainnet_asset_lock_transaction() {
79        const EXPECTED_STATE_TRANSITION_HASH: &str =
80            "6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D";
81        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==";
82        const EXPECTED_IDENTITY_ADDRESS: &str = "5tf2QotaJw8kRNpQEa8TXtRQ6FLxwUrY4Mtee2JF2nco";
83        const EXPECTED_CORE_TRANSACTION_HASH: &str =
84            "5529726cc14d856a363745c68ba914339c318a9b78a99bb4b4145b23d7630732";
85        let raw_transaction = STANDARD
86            .decode(RAW_TRANSACTION_BASE64)
87            .expect("base64 transaction should decode");
88        let state_transition = StateTransition::deserialize_from_bytes_untrusted(&raw_transaction)
89            .expect("State transition deserializes correctly");
90
91        assert_eq!(
92            &state_transition
93                .transaction_id()
94                .expect("expected transaction id")
95                .encode_hex_upper::<String>(),
96            EXPECTED_STATE_TRANSITION_HASH
97        );
98
99        let StateTransition::IdentityCreate(identity_create_transition) = state_transition else {
100            panic!("expected identity create transition");
101        };
102
103        // This mainnet transaction uses a ChainAssetLockProof (not InstantAssetLockProof)
104        // ChainAssetLockProof doesn't embed the full transaction, just the out_point reference
105        let asset_lock_proof = identity_create_transition.asset_lock_proof();
106        let AssetLockProof::Chain(chain_proof) = asset_lock_proof else {
107            panic!("expected chain asset lock proof for this mainnet transaction");
108        };
109
110        // Verify the out_point references the expected transaction
111        assert_eq!(
112            &chain_proof.out_point.txid.to_string().to_lowercase(),
113            EXPECTED_CORE_TRANSACTION_HASH
114        );
115
116        let identity_address = identity_create_transition
117            .identity_id()
118            .to_string(Encoding::Base58);
119
120        assert_eq!(identity_address, EXPECTED_IDENTITY_ADDRESS);
121    }
122
123    #[test]
124    #[cfg(feature = "random-identities")]
125    fn identity_create_transition_ser_de() {
126        let platform_version = LATEST_PLATFORM_VERSION;
127        let identity = Identity::random_identity(5, Some(5), platform_version)
128            .expect("expected a random identity");
129        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
130
131        let identity_create_transition = IdentityCreateTransition::V0(
132            IdentityCreateTransitionV0::try_from_identity(
133                &identity,
134                AssetLockProof::Instant(asset_lock_proof),
135                platform_version,
136            )
137            .expect("expected to make an identity create transition"),
138        );
139
140        let state_transition: StateTransition = identity_create_transition.into();
141        let bytes = state_transition
142            .serialize_to_bytes()
143            .expect("expected to serialize");
144        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
145            .expect("expected to deserialize state transition");
146        assert_eq!(state_transition, recovered_state_transition);
147    }
148
149    #[test]
150    #[cfg(feature = "random-identities")]
151    fn identity_topup_transition_ser_de() {
152        let platform_version = PlatformVersion::latest();
153        let identity = Identity::random_identity(5, Some(5), platform_version)
154            .expect("expected a random identity");
155        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
156
157        let identity_topup_transition = IdentityTopUpTransitionV0 {
158            asset_lock_proof: AssetLockProof::Instant(asset_lock_proof),
159            identity_id: identity.id(),
160            user_fee_increase: 0,
161            signature: [1u8; 65].to_vec().into(),
162        };
163        let state_transition: StateTransition = identity_topup_transition.into();
164        let bytes = state_transition
165            .serialize_to_bytes()
166            .expect("expected to serialize");
167        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
168            .expect("expected to deserialize state transition");
169        assert_eq!(state_transition, recovered_state_transition);
170    }
171
172    #[test]
173    #[cfg(feature = "random-identities")]
174    fn identity_update_transition_add_keys_ser_de() {
175        let mut rng = StdRng::seed_from_u64(5);
176        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
177            Identity::random_identity_with_main_keys_with_private_key(
178                5,
179                &mut rng,
180                LATEST_PLATFORM_VERSION,
181            )
182            .expect("expected to get identity");
183        let bls = NativeBlsModule;
184        let add_public_keys_in_creation = identity
185            .public_keys()
186            .values()
187            .map(|public_key| public_key.into())
188            .collect();
189        let mut identity_update_transition = IdentityUpdateTransitionV0 {
190            signature: Default::default(),
191            signature_public_key_id: 0,
192            identity_id: identity.id(),
193            revision: 1,
194            nonce: 1,
195            add_public_keys: add_public_keys_in_creation,
196            disable_public_keys: vec![],
197            user_fee_increase: 0,
198        };
199
200        let key_signable_bytes = identity_update_transition
201            .signable_bytes()
202            .expect("expected to get signable bytes");
203
204        identity_update_transition
205            .add_public_keys
206            .iter_mut()
207            .zip(identity.public_keys().clone().into_values())
208            .try_for_each(|(public_key_with_witness, public_key)| {
209                if public_key.key_type().is_unique_key_type() {
210                    let private_key = keys
211                        .get(&public_key)
212                        .expect("expected to have the private key");
213                    let signature = key_signable_bytes
214                        .as_slice()
215                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
216                        .into();
217                    public_key_with_witness.set_signature(signature);
218                }
219
220                Ok::<(), ProtocolError>(())
221            })
222            .expect("expected to update keys");
223
224        let (public_key, private_key) = keys.pop_first().unwrap();
225
226        let mut state_transition: StateTransition = identity_update_transition.into();
227
228        state_transition
229            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
230            .expect("expected to sign IdentityUpdateTransition");
231        let bytes = state_transition
232            .serialize_to_bytes()
233            .expect("expected to serialize");
234        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
235            .expect("expected to deserialize state transition");
236        assert_eq!(state_transition, recovered_state_transition);
237    }
238
239    #[test]
240    #[cfg(feature = "state-transition-signing")]
241    fn identity_update_transition_disable_keys_ser_de() {
242        let mut rng = StdRng::seed_from_u64(5);
243        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
244            Identity::random_identity_with_main_keys_with_private_key(
245                5,
246                &mut rng,
247                LATEST_PLATFORM_VERSION,
248            )
249            .expect("expected to get identity");
250        let bls = NativeBlsModule;
251        let add_public_keys_in_creation = identity
252            .public_keys()
253            .values()
254            .map(|public_key| public_key.into())
255            .collect();
256        let mut identity_update_transition = IdentityUpdateTransitionV0 {
257            signature: Default::default(),
258            signature_public_key_id: 0,
259            identity_id: identity.id(),
260            revision: 1,
261            nonce: 1,
262            add_public_keys: add_public_keys_in_creation,
263            disable_public_keys: vec![3, 4, 5],
264            user_fee_increase: 0,
265        };
266
267        let key_signable_bytes = identity_update_transition
268            .signable_bytes()
269            .expect("expected to get signable bytes");
270
271        identity_update_transition
272            .add_public_keys
273            .iter_mut()
274            .zip(identity.public_keys().clone().into_values())
275            .try_for_each(|(public_key_with_witness, public_key)| {
276                if public_key.key_type().is_unique_key_type() {
277                    let private_key = keys
278                        .get(&public_key)
279                        .expect("expected to have the private key");
280                    let signature = key_signable_bytes
281                        .as_slice()
282                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
283                        .into();
284                    public_key_with_witness.set_signature(signature);
285                }
286
287                Ok::<(), ProtocolError>(())
288            })
289            .expect("expected to update keys");
290
291        let (public_key, private_key) = keys.pop_first().unwrap();
292
293        let mut state_transition: StateTransition = identity_update_transition.into();
294
295        state_transition
296            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
297            .expect("expected to sign IdentityUpdateTransition");
298        let bytes = state_transition
299            .serialize_to_bytes()
300            .expect("expected to serialize");
301        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
302            .expect("expected to deserialize state transition");
303        assert_eq!(state_transition, recovered_state_transition);
304    }
305
306    #[test]
307    #[cfg(feature = "random-identities")]
308    fn identity_credit_withdrawal_transition_ser_de() {
309        let platform_version = PlatformVersion::latest();
310        let identity = Identity::random_identity(5, Some(5), platform_version)
311            .expect("expected a random identity");
312        let identity_credit_withdrawal_transition = IdentityCreditWithdrawalTransitionV0 {
313            identity_id: identity.id(),
314            amount: 5000000,
315            core_fee_per_byte: 34,
316            pooling: Pooling::Standard,
317            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
318            nonce: 1,
319            user_fee_increase: 0,
320            signature_public_key_id: 0,
321            signature: [1u8; 65].to_vec().into(),
322        };
323        let state_transition: StateTransition = identity_credit_withdrawal_transition.into();
324        let bytes = state_transition
325            .serialize_to_bytes()
326            .expect("expected to serialize");
327        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
328            .expect("expected to deserialize state transition");
329        assert_eq!(state_transition, recovered_state_transition);
330    }
331
332    #[test]
333    #[cfg(feature = "random-identities")]
334    fn data_contract_create_ser_de() {
335        let platform_version = LATEST_PLATFORM_VERSION;
336        let identity = Identity::random_identity(5, Some(5), platform_version)
337            .expect("expected a random identity");
338        let created_data_contract = get_data_contract_fixture(
339            Some(identity.id()),
340            0,
341            LATEST_PLATFORM_VERSION.protocol_version,
342        );
343        let data_contract_create_transition: DataContractCreateTransition = created_data_contract
344            .try_into_platform_versioned(platform_version)
345            .expect("expected to transform into a DataContractCreateTransition");
346        let state_transition: StateTransition = data_contract_create_transition.into();
347        let bytes = state_transition
348            .serialize_to_bytes()
349            .expect("expected to serialize");
350        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
351            .expect("expected to deserialize state transition");
352        assert_eq!(state_transition, recovered_state_transition);
353    }
354
355    #[test]
356    #[cfg(feature = "random-identities")]
357    fn data_contract_update_ser_de() {
358        let platform_version = PlatformVersion::latest();
359        let identity = Identity::random_identity(5, Some(5), platform_version)
360            .expect("expected a random identity");
361        let created_data_contract =
362            get_data_contract_fixture(Some(identity.id()), 0, platform_version.protocol_version);
363        let data_contract_update_transition =
364            DataContractUpdateTransition::V0(DataContractUpdateTransitionV0 {
365                identity_contract_nonce: 1,
366                data_contract: created_data_contract
367                    .data_contract_owned()
368                    .try_into_platform_versioned(platform_version)
369                    .expect("expected a data contract"),
370                user_fee_increase: 0,
371                signature_public_key_id: 0,
372                signature: [1u8; 65].to_vec().into(),
373            });
374        let state_transition: StateTransition = data_contract_update_transition.into();
375        let bytes = state_transition
376            .serialize_to_bytes()
377            .expect("expected to serialize");
378        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
379            .expect("expected to deserialize state transition");
380        assert_eq!(state_transition, recovered_state_transition);
381    }
382
383    #[test]
384    fn document_batch_transition_10_created_documents_ser_de() {
385        let platform_version = PlatformVersion::latest();
386
387        let mut nonces = BTreeMap::new();
388        let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
389            .data_contract_owned();
390        let documents = get_extended_documents_fixture_with_owner_id_from_contract(
391            &data_contract,
392            platform_version.protocol_version,
393        )
394        .unwrap();
395        let documents = documents
396            .iter()
397            .map(|extended_document| {
398                let document = extended_document.document().clone();
399                let data_contract = extended_document.data_contract();
400                (
401                    document,
402                    data_contract
403                        .document_type_for_name(extended_document.document_type_name())
404                        .unwrap(),
405                    *extended_document.entropy(),
406                    None,
407                )
408            })
409            .collect::<Vec<_>>();
410        let transitions = get_batched_transitions_fixture(
411            [(DocumentTransitionActionType::Create, documents)],
412            &mut nonces,
413        );
414        let documents_batch_transition: BatchTransition = BatchTransitionV1 {
415            owner_id: data_contract.owner_id(),
416            transitions,
417            ..Default::default()
418        }
419        .into();
420        let state_transition: StateTransition = documents_batch_transition.into();
421        let bytes = state_transition
422            .serialize_to_bytes()
423            .expect("expected to serialize");
424        let recovered_state_transition = StateTransition::deserialize_from_bytes_untrusted(&bytes)
425            .expect("expected to deserialize state transition");
426        assert_eq!(state_transition, recovered_state_transition);
427    }
428
429    /// Stack size for tests that build `Value`s nested to the decoder depth ceiling.
430    ///
431    /// Only decoding is iterative: the derived `Encode`, `PartialEq` and drop glue recurse once
432    /// per nesting level, and in debug builds those frames cost roughly 7 KiB per level, so a
433    /// value ~256 levels deep exhausts libtest's default 2 MiB per-test thread. nextest runs each
434    /// test on the 8 MiB main thread, which is why CI does not see the overflow.
435    const DEEP_VALUE_TEST_STACK_SIZE: usize = 16 * 1024 * 1024;
436
437    fn on_deep_value_stack(test: impl FnOnce() + Send + 'static) {
438        std::thread::Builder::new()
439            .stack_size(DEEP_VALUE_TEST_STACK_SIZE)
440            .spawn(test)
441            .expect("the deep value test thread should spawn")
442            .join()
443            .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
444    }
445
446    #[test]
447    fn document_batch_rejects_excessive_value_depth_during_decode() {
448        on_deep_value_stack(|| {
449            let nested = (0..300).fold(Value::Null, |value, _| Value::Array(vec![value]));
450            let document_transition = DocumentTransition::Create(DocumentCreateTransition::V0(
451                DocumentCreateTransitionV0 {
452                    base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
453                        id: Identifier::default(),
454                        identity_contract_nonce: 1,
455                        document_type_name: "test".to_string(),
456                        data_contract_id: Identifier::default(),
457                    }),
458                    entropy: [0; 32],
459                    data: BTreeMap::from([("nested".to_string(), nested)]),
460                    prefunded_voting_balance: None,
461                },
462            ));
463            assert_eq!(
464                document_transition.first_data_depth_exceeding(256),
465                Some(257)
466            );
467
468            let state_transition = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 {
469                transitions: vec![BatchedTransition::Document(document_transition)],
470                ..Default::default()
471            }));
472            let bytes = state_transition
473                .serialize_to_bytes()
474                .expect("the state transition should encode below the byte limit");
475            assert!(
476                bytes.len() as u64
477                    <= PlatformVersion::latest()
478                        .system_limits
479                        .max_state_transition_size
480            );
481
482            // The intentionally invalid transition is no longer needed after encoding. Avoid walking
483            // its recursive data during drop so this regression test only exercises decoder behavior.
484            std::mem::forget(state_transition);
485
486            let error = StateTransition::deserialize_from_bytes_untrusted_in_version(
487                &bytes,
488                PlatformVersion::latest(),
489            )
490            .expect_err("excessive nesting must be rejected during decode");
491            assert!(error
492                .to_string()
493                .contains("value nesting depth 257 exceeds maximum 256"));
494        });
495    }
496
497    #[test]
498    fn document_batch_value_depth_limits_align_between_decode_and_validation() {
499        on_deep_value_stack(|| {
500            // Every decodable document value must also satisfy the consensus depth rule, so depth
501            // violations always fail the same way: as an undecodable transition. A value at the
502            // decoder ceiling must therefore round-trip and pass the validation-side depth check.
503            let max_depth = PlatformVersion::latest()
504                .system_limits
505                .max_document_value_depth
506                .expect("latest protocol should enforce document value depth")
507                as usize;
508            let nested = (1..max_depth).fold(Value::Array(vec![Value::Null]), |value, _| {
509                Value::Array(vec![value])
510            });
511            let document_transition = DocumentTransition::Create(DocumentCreateTransition::V0(
512                DocumentCreateTransitionV0 {
513                    base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
514                        id: Identifier::default(),
515                        identity_contract_nonce: 1,
516                        document_type_name: "test".to_string(),
517                        data_contract_id: Identifier::default(),
518                    }),
519                    entropy: [0; 32],
520                    data: BTreeMap::from([("nested".to_string(), nested)]),
521                    prefunded_voting_balance: None,
522                },
523            ));
524            assert_eq!(
525                document_transition.first_data_depth_exceeding(max_depth),
526                None
527            );
528            assert_eq!(
529                document_transition.first_data_depth_exceeding(max_depth - 1),
530                Some(max_depth)
531            );
532
533            let state_transition = StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 {
534                transitions: vec![BatchedTransition::Document(document_transition)],
535                ..Default::default()
536            }));
537            let bytes = state_transition
538                .serialize_to_bytes()
539                .expect("the state transition should encode below the byte limit");
540
541            let recovered = StateTransition::deserialize_from_bytes_untrusted_in_version(
542                &bytes,
543                PlatformVersion::latest(),
544            )
545            .expect("a value at the decoder ceiling must decode");
546            assert_eq!(state_transition, recovered);
547        });
548    }
549
550    #[test]
551    fn deserialize_empty_bytes_should_fail() {
552        let result = StateTransition::deserialize_from_bytes_untrusted(&[]);
553        assert!(
554            result.is_err(),
555            "deserialization of empty bytes should fail"
556        );
557    }
558
559    #[test]
560    fn deserialize_single_byte_should_fail() {
561        let result = StateTransition::deserialize_from_bytes_untrusted(&[0xFF]);
562        assert!(
563            result.is_err(),
564            "deserialization of a single 0xFF byte should fail"
565        );
566    }
567
568    #[test]
569    #[cfg(feature = "random-identities")]
570    fn deserialize_truncated_bytes_should_fail() {
571        let platform_version = PlatformVersion::latest();
572        let identity = Identity::random_identity(5, Some(5), platform_version)
573            .expect("expected a random identity");
574        let transition = IdentityCreditWithdrawalTransitionV0 {
575            identity_id: identity.id(),
576            amount: 5000000,
577            core_fee_per_byte: 34,
578            pooling: Pooling::Standard,
579            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
580            nonce: 1,
581            user_fee_increase: 0,
582            signature_public_key_id: 0,
583            signature: [1u8; 65].to_vec().into(),
584        };
585        let state_transition: StateTransition = transition.into();
586        let bytes = state_transition
587            .serialize_to_bytes()
588            .expect("expected to serialize");
589
590        // Truncate to half
591        let half = &bytes[..bytes.len() / 2];
592        assert!(
593            StateTransition::deserialize_from_bytes_untrusted(half).is_err(),
594            "deserialization of truncated-to-half bytes should fail"
595        );
596
597        // Truncate by removing last byte
598        let minus_one = &bytes[..bytes.len() - 1];
599        assert!(
600            StateTransition::deserialize_from_bytes_untrusted(minus_one).is_err(),
601            "deserialization of bytes missing last byte should fail"
602        );
603
604        // Keep only first byte
605        let first_only = &bytes[..1];
606        assert!(
607            StateTransition::deserialize_from_bytes_untrusted(first_only).is_err(),
608            "deserialization of only the first byte should fail"
609        );
610    }
611
612    #[test]
613    #[cfg(feature = "random-identities")]
614    fn deserialize_corrupted_bytes_should_not_panic() {
615        let platform_version = PlatformVersion::latest();
616        let identity = Identity::random_identity(5, Some(5), platform_version)
617            .expect("expected a random identity");
618        let transition = IdentityCreditWithdrawalTransitionV0 {
619            identity_id: identity.id(),
620            amount: 5000000,
621            core_fee_per_byte: 34,
622            pooling: Pooling::Standard,
623            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
624            nonce: 1,
625            user_fee_increase: 0,
626            signature_public_key_id: 0,
627            signature: [1u8; 65].to_vec().into(),
628        };
629        let state_transition: StateTransition = transition.into();
630        let mut bytes = state_transition
631            .serialize_to_bytes()
632            .expect("expected to serialize");
633
634        // Flip bits in the middle of the payload
635        let mid = bytes.len() / 2;
636        bytes[mid] ^= 0xFF;
637
638        // Should either fail or return a different value - must not panic
639        let result = StateTransition::deserialize_from_bytes_untrusted(&bytes);
640        if let Ok(recovered) = result {
641            assert_ne!(
642                state_transition, recovered,
643                "corrupted bytes should not deserialize to the original value"
644            );
645        }
646    }
647
648    /// Crafts a minimal payload that looks like a StateTransition variant
649    /// (IdentityCreditWithdrawal = discriminant 5) followed by a version byte
650    /// and then a Vec<u8> field with a varint-encoded length that claims to
651    /// contain `fake_len` bytes. The total payload is small but the decoded
652    /// length would trigger a huge allocation without the limit guard.
653    fn craft_oversized_vec_payload(fake_len: u64) -> Vec<u8> {
654        let config = bincode::config::standard()
655            .with_big_endian()
656            .with_no_limit();
657        // Build the payload: variant discriminant + version + bogus vec length + filler
658        let mut buf = Vec::new();
659        // StateTransition enum discriminant for IdentityCreditWithdrawal (index 5)
660        buf.extend_from_slice(&bincode::encode_to_vec(5u32, config).unwrap());
661        // Version byte (0 = V0)
662        buf.push(0);
663        // identity_id: 32 bytes (Identifier)
664        buf.extend_from_slice(&[0u8; 32]);
665        // amount: u64
666        buf.extend_from_slice(&bincode::encode_to_vec(1000u64, config).unwrap());
667        // core_fee_per_byte: u32
668        buf.extend_from_slice(&bincode::encode_to_vec(1u32, config).unwrap());
669        // pooling: enum variant 0
670        buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
671        // output_script (CoreScript = BinaryData = Vec<u8>): encode the malicious length
672        buf.extend_from_slice(&bincode::encode_to_vec(fake_len, config).unwrap());
673        // Don't provide the actual bytes — the limit check should fire before allocation
674        buf
675    }
676
677    #[test]
678    fn deserialize_crafted_huge_vec_length_does_not_oom() {
679        // Craft a small payload (~80 bytes) with a Vec<u8> field claiming 8 GB.
680        // Without the limit fix this would attempt `vec![0u8; 8_000_000_000]` and abort.
681        let payload = craft_oversized_vec_payload(8_000_000_000);
682        let result = StateTransition::deserialize_from_bytes_untrusted(&payload);
683        // Must return an error, not OOM-abort the process
684        assert!(
685            result.is_err(),
686            "crafted payload with 8GB vec length must be rejected, not cause OOM"
687        );
688    }
689
690    #[test]
691    fn deserialize_crafted_vec_exceeding_limit_is_rejected() {
692        // Craft a payload with a Vec<u8> claiming 200,000 bytes — exceeds the
693        // 100,000 byte budget configured on StateTransition.
694        // The limit causes bincode to reject the read (either as Io/LimitExceeded
695        // or UnexpectedEnd depending on the exact code path). Either way, the
696        // deserialization must fail safely without OOM.
697        let payload = craft_oversized_vec_payload(200_000);
698        let result = StateTransition::deserialize_from_bytes_untrusted(&payload);
699        assert!(
700            result.is_err(),
701            "Vec length exceeding byte budget must be rejected"
702        );
703    }
704
705    #[test]
706    fn deserialize_no_limit_does_not_enforce_byte_budget() {
707        // Same crafted payload with 200,000-byte Vec — the no_limit variant
708        // should NOT reject it for byte budget reasons (it will still fail
709        // because the data doesn't actually contain 200,000 bytes).
710        let payload = craft_oversized_vec_payload(200_000);
711        let result = StateTransition::deserialize_from_bytes_untrusted_no_limit(&payload);
712        assert!(result.is_err());
713        // any other error is fine — the data is garbage
714        if let ProtocolError::MaxEncodedBytesReachedError { .. } = result.unwrap_err() {
715            panic!("deserialize_from_bytes_untrusted_no_limit should NOT enforce byte budget");
716        }
717    }
718
719    #[test]
720    fn deserialize_many_empty_list() {
721        let result = StateTransition::deserialize_many_untrusted(&[]);
722        assert_eq!(result.unwrap(), vec![]);
723    }
724
725    #[test]
726    fn deserialize_many_with_invalid_entry() {
727        let result = StateTransition::deserialize_many_untrusted(&[vec![0xFF]]);
728        assert!(
729            result.is_err(),
730            "deserialize_many with invalid entry should fail"
731        );
732    }
733
734    #[test]
735    #[cfg(feature = "random-identities")]
736    fn deserialize_many_with_valid_entries() {
737        let platform_version = PlatformVersion::latest();
738        let identity = Identity::random_identity(5, Some(5), platform_version)
739            .expect("expected a random identity");
740
741        let make_transition = |amount: u64, nonce: u64| -> StateTransition {
742            let t = IdentityCreditWithdrawalTransitionV0 {
743                identity_id: identity.id(),
744                amount,
745                core_fee_per_byte: 34,
746                pooling: Pooling::Standard,
747                output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
748                nonce,
749                user_fee_increase: 0,
750                signature_public_key_id: 0,
751                signature: [1u8; 65].to_vec().into(),
752            };
753            t.into()
754        };
755
756        let st1 = make_transition(1000000, 1);
757        let st2 = make_transition(2000000, 2);
758        let st3 = make_transition(3000000, 3);
759
760        let raw: Vec<Vec<u8>> = vec![
761            st1.serialize_to_bytes().unwrap(),
762            st2.serialize_to_bytes().unwrap(),
763            st3.serialize_to_bytes().unwrap(),
764        ];
765
766        let recovered =
767            StateTransition::deserialize_many_untrusted(&raw).expect("should deserialize all");
768        assert_eq!(recovered.len(), 3);
769        assert_eq!(recovered[0], st1);
770        assert_eq!(recovered[1], st2);
771        assert_eq!(recovered[2], st3);
772    }
773}