dpp/state_transition/
serialization.rs

1use crate::serialization::PlatformDeserializable;
2use crate::state_transition::StateTransition;
3use crate::ProtocolError;
4
5impl StateTransition {
6    pub fn deserialize_many(raw_state_transitions: &[Vec<u8>]) -> Result<Vec<Self>, ProtocolError> {
7        raw_state_transitions
8            .iter()
9            .map(|raw_state_transition| Self::deserialize_from_bytes(raw_state_transition))
10            .collect()
11    }
12}
13
14#[cfg(test)]
15mod tests {
16    use hex::ToHex;
17    use base64::engine::general_purpose::STANDARD;
18    use base64::Engine;
19    use platform_value::string_encoding::Encoding;
20    use crate::bls::native_bls::NativeBlsModule;
21    use crate::data_contract::accessors::v0::DataContractV0Getters;
22    use crate::identity::state_transition::AssetLockProved;
23    use crate::identity::accessors::IdentityGettersV0;
24    use crate::identity::core_script::CoreScript;
25    use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
26    use crate::identity::Identity;
27    use crate::prelude::AssetLockProof;
28    use crate::serialization::PlatformMessageSignable;
29    use crate::serialization::Signable;
30    use crate::serialization::{PlatformDeserializable, PlatformSerializable};
31    use crate::state_transition::data_contract_create_transition::DataContractCreateTransition;
32    use crate::state_transition::data_contract_update_transition::{
33        DataContractUpdateTransition, DataContractUpdateTransitionV0,
34    };
35    use crate::state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType;
36    use crate::state_transition::batch_transition::{
37        BatchTransition, BatchTransitionV1,
38    };
39    use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0;
40    use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0;
41    use crate::state_transition::identity_create_transition::IdentityCreateTransition;
42    use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
43    use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0;
44    use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0;
45    use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
46    use crate::state_transition::StateTransition;
47    use crate::tests::fixtures::{
48        get_data_contract_fixture, get_batched_transitions_fixture,
49        get_extended_documents_fixture_with_owner_id_from_contract,
50        raw_instant_asset_lock_proof_fixture,
51    };
52    use crate::version::PlatformVersion;
53    use crate::withdrawal::Pooling;
54    use crate::ProtocolError;
55    use platform_version::version::LATEST_PLATFORM_VERSION;
56    use platform_version::TryIntoPlatformVersioned;
57    use rand::rngs::StdRng;
58    use rand::SeedableRng;
59    use std::collections::BTreeMap;
60
61    #[test]
62    /// Given mainnet transaction 6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D,
63    /// when deserialized, it should be identity create transition.
64    fn should_build_identity_create_transition_from_mainnet_asset_lock_transaction() {
65        const EXPECTED_STATE_TRANSITION_HASH: &str =
66            "6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D";
67        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==";
68        const EXPECTED_IDENTITY_ADDRESS: &str = "5tf2QotaJw8kRNpQEa8TXtRQ6FLxwUrY4Mtee2JF2nco";
69        const EXPECTED_CORE_TRANSACTION_HASH: &str =
70            "5529726cc14d856a363745c68ba914339c318a9b78a99bb4b4145b23d7630732";
71        let raw_transaction = STANDARD
72            .decode(RAW_TRANSACTION_BASE64)
73            .expect("base64 transaction should decode");
74        let state_transition = StateTransition::deserialize_from_bytes(&raw_transaction)
75            .expect("State transition deserializes correctly");
76
77        assert_eq!(
78            &state_transition
79                .transaction_id()
80                .expect("expected transaction id")
81                .encode_hex_upper::<String>(),
82            EXPECTED_STATE_TRANSITION_HASH
83        );
84
85        let StateTransition::IdentityCreate(identity_create_transition) = state_transition else {
86            panic!("expected identity create transition");
87        };
88
89        // This mainnet transaction uses a ChainAssetLockProof (not InstantAssetLockProof)
90        // ChainAssetLockProof doesn't embed the full transaction, just the out_point reference
91        let asset_lock_proof = identity_create_transition.asset_lock_proof();
92        let AssetLockProof::Chain(chain_proof) = asset_lock_proof else {
93            panic!("expected chain asset lock proof for this mainnet transaction");
94        };
95
96        // Verify the out_point references the expected transaction
97        assert_eq!(
98            &chain_proof.out_point.txid.to_string().to_lowercase(),
99            EXPECTED_CORE_TRANSACTION_HASH
100        );
101
102        let identity_address = identity_create_transition
103            .identity_id()
104            .to_string(Encoding::Base58);
105
106        assert_eq!(identity_address, EXPECTED_IDENTITY_ADDRESS);
107    }
108
109    #[test]
110    #[cfg(feature = "random-identities")]
111    fn identity_create_transition_ser_de() {
112        let platform_version = LATEST_PLATFORM_VERSION;
113        let identity = Identity::random_identity(5, Some(5), platform_version)
114            .expect("expected a random identity");
115        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
116
117        let identity_create_transition = IdentityCreateTransition::V0(
118            IdentityCreateTransitionV0::try_from_identity(
119                &identity,
120                AssetLockProof::Instant(asset_lock_proof),
121                platform_version,
122            )
123            .expect("expected to make an identity create transition"),
124        );
125
126        let state_transition: StateTransition = identity_create_transition.into();
127        let bytes = state_transition
128            .serialize_to_bytes()
129            .expect("expected to serialize");
130        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
131            .expect("expected to deserialize state transition");
132        assert_eq!(state_transition, recovered_state_transition);
133    }
134
135    #[test]
136    #[cfg(feature = "random-identities")]
137    fn identity_topup_transition_ser_de() {
138        let platform_version = PlatformVersion::latest();
139        let identity = Identity::random_identity(5, Some(5), platform_version)
140            .expect("expected a random identity");
141        let asset_lock_proof = raw_instant_asset_lock_proof_fixture(None, None);
142
143        let identity_topup_transition = IdentityTopUpTransitionV0 {
144            asset_lock_proof: AssetLockProof::Instant(asset_lock_proof),
145            identity_id: identity.id(),
146            user_fee_increase: 0,
147            signature: [1u8; 65].to_vec().into(),
148        };
149        let state_transition: StateTransition = identity_topup_transition.into();
150        let bytes = state_transition
151            .serialize_to_bytes()
152            .expect("expected to serialize");
153        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
154            .expect("expected to deserialize state transition");
155        assert_eq!(state_transition, recovered_state_transition);
156    }
157
158    #[test]
159    #[cfg(feature = "random-identities")]
160    fn identity_update_transition_add_keys_ser_de() {
161        let mut rng = StdRng::seed_from_u64(5);
162        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
163            Identity::random_identity_with_main_keys_with_private_key(
164                5,
165                &mut rng,
166                LATEST_PLATFORM_VERSION,
167            )
168            .expect("expected to get identity");
169        let bls = NativeBlsModule;
170        let add_public_keys_in_creation = identity
171            .public_keys()
172            .values()
173            .map(|public_key| public_key.into())
174            .collect();
175        let mut identity_update_transition = IdentityUpdateTransitionV0 {
176            signature: Default::default(),
177            signature_public_key_id: 0,
178            identity_id: identity.id(),
179            revision: 1,
180            nonce: 1,
181            add_public_keys: add_public_keys_in_creation,
182            disable_public_keys: vec![],
183            user_fee_increase: 0,
184        };
185
186        let key_signable_bytes = identity_update_transition
187            .signable_bytes()
188            .expect("expected to get signable bytes");
189
190        identity_update_transition
191            .add_public_keys
192            .iter_mut()
193            .zip(identity.public_keys().clone().into_values())
194            .try_for_each(|(public_key_with_witness, public_key)| {
195                if public_key.key_type().is_unique_key_type() {
196                    let private_key = keys
197                        .get(&public_key)
198                        .expect("expected to have the private key");
199                    let signature = key_signable_bytes
200                        .as_slice()
201                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
202                        .into();
203                    public_key_with_witness.set_signature(signature);
204                }
205
206                Ok::<(), ProtocolError>(())
207            })
208            .expect("expected to update keys");
209
210        let (public_key, private_key) = keys.pop_first().unwrap();
211
212        let mut state_transition: StateTransition = identity_update_transition.into();
213
214        state_transition
215            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
216            .expect("expected to sign IdentityUpdateTransition");
217        let bytes = state_transition
218            .serialize_to_bytes()
219            .expect("expected to serialize");
220        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
221            .expect("expected to deserialize state transition");
222        assert_eq!(state_transition, recovered_state_transition);
223    }
224
225    #[test]
226    #[cfg(feature = "state-transition-signing")]
227    fn identity_update_transition_disable_keys_ser_de() {
228        let mut rng = StdRng::seed_from_u64(5);
229        let (identity, mut keys): (Identity, BTreeMap<_, _>) =
230            Identity::random_identity_with_main_keys_with_private_key(
231                5,
232                &mut rng,
233                LATEST_PLATFORM_VERSION,
234            )
235            .expect("expected to get identity");
236        let bls = NativeBlsModule;
237        let add_public_keys_in_creation = identity
238            .public_keys()
239            .values()
240            .map(|public_key| public_key.into())
241            .collect();
242        let mut identity_update_transition = IdentityUpdateTransitionV0 {
243            signature: Default::default(),
244            signature_public_key_id: 0,
245            identity_id: identity.id(),
246            revision: 1,
247            nonce: 1,
248            add_public_keys: add_public_keys_in_creation,
249            disable_public_keys: vec![3, 4, 5],
250            user_fee_increase: 0,
251        };
252
253        let key_signable_bytes = identity_update_transition
254            .signable_bytes()
255            .expect("expected to get signable bytes");
256
257        identity_update_transition
258            .add_public_keys
259            .iter_mut()
260            .zip(identity.public_keys().clone().into_values())
261            .try_for_each(|(public_key_with_witness, public_key)| {
262                if public_key.key_type().is_unique_key_type() {
263                    let private_key = keys
264                        .get(&public_key)
265                        .expect("expected to have the private key");
266                    let signature = key_signable_bytes
267                        .as_slice()
268                        .sign_by_private_key(private_key, public_key.key_type(), &bls)?
269                        .into();
270                    public_key_with_witness.set_signature(signature);
271                }
272
273                Ok::<(), ProtocolError>(())
274            })
275            .expect("expected to update keys");
276
277        let (public_key, private_key) = keys.pop_first().unwrap();
278
279        let mut state_transition: StateTransition = identity_update_transition.into();
280
281        state_transition
282            .sign_by_private_key(private_key.as_slice(), public_key.key_type(), &bls)
283            .expect("expected to sign IdentityUpdateTransition");
284        let bytes = state_transition
285            .serialize_to_bytes()
286            .expect("expected to serialize");
287        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
288            .expect("expected to deserialize state transition");
289        assert_eq!(state_transition, recovered_state_transition);
290    }
291
292    #[test]
293    #[cfg(feature = "random-identities")]
294    fn identity_credit_withdrawal_transition_ser_de() {
295        let platform_version = PlatformVersion::latest();
296        let identity = Identity::random_identity(5, Some(5), platform_version)
297            .expect("expected a random identity");
298        let identity_credit_withdrawal_transition = IdentityCreditWithdrawalTransitionV0 {
299            identity_id: identity.id(),
300            amount: 5000000,
301            core_fee_per_byte: 34,
302            pooling: Pooling::Standard,
303            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
304            nonce: 1,
305            user_fee_increase: 0,
306            signature_public_key_id: 0,
307            signature: [1u8; 65].to_vec().into(),
308        };
309        let state_transition: StateTransition = identity_credit_withdrawal_transition.into();
310        let bytes = state_transition
311            .serialize_to_bytes()
312            .expect("expected to serialize");
313        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
314            .expect("expected to deserialize state transition");
315        assert_eq!(state_transition, recovered_state_transition);
316    }
317
318    #[test]
319    #[cfg(feature = "random-identities")]
320    fn data_contract_create_ser_de() {
321        let platform_version = LATEST_PLATFORM_VERSION;
322        let identity = Identity::random_identity(5, Some(5), platform_version)
323            .expect("expected a random identity");
324        let created_data_contract = get_data_contract_fixture(
325            Some(identity.id()),
326            0,
327            LATEST_PLATFORM_VERSION.protocol_version,
328        );
329        let data_contract_create_transition: DataContractCreateTransition = created_data_contract
330            .try_into_platform_versioned(platform_version)
331            .expect("expected to transform into a DataContractCreateTransition");
332        let state_transition: StateTransition = data_contract_create_transition.into();
333        let bytes = state_transition
334            .serialize_to_bytes()
335            .expect("expected to serialize");
336        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
337            .expect("expected to deserialize state transition");
338        assert_eq!(state_transition, recovered_state_transition);
339    }
340
341    #[test]
342    #[cfg(feature = "random-identities")]
343    fn data_contract_update_ser_de() {
344        let platform_version = PlatformVersion::latest();
345        let identity = Identity::random_identity(5, Some(5), platform_version)
346            .expect("expected a random identity");
347        let created_data_contract =
348            get_data_contract_fixture(Some(identity.id()), 0, platform_version.protocol_version);
349        let data_contract_update_transition =
350            DataContractUpdateTransition::V0(DataContractUpdateTransitionV0 {
351                identity_contract_nonce: 1,
352                data_contract: created_data_contract
353                    .data_contract_owned()
354                    .try_into_platform_versioned(platform_version)
355                    .expect("expected a data contract"),
356                user_fee_increase: 0,
357                signature_public_key_id: 0,
358                signature: [1u8; 65].to_vec().into(),
359            });
360        let state_transition: StateTransition = data_contract_update_transition.into();
361        let bytes = state_transition
362            .serialize_to_bytes()
363            .expect("expected to serialize");
364        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
365            .expect("expected to deserialize state transition");
366        assert_eq!(state_transition, recovered_state_transition);
367    }
368
369    #[test]
370    fn document_batch_transition_10_created_documents_ser_de() {
371        let platform_version = PlatformVersion::latest();
372
373        let mut nonces = BTreeMap::new();
374        let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
375            .data_contract_owned();
376        let documents = get_extended_documents_fixture_with_owner_id_from_contract(
377            &data_contract,
378            platform_version.protocol_version,
379        )
380        .unwrap();
381        let documents = documents
382            .iter()
383            .map(|extended_document| {
384                let document = extended_document.document().clone();
385                let data_contract = extended_document.data_contract();
386                (
387                    document,
388                    data_contract
389                        .document_type_for_name(extended_document.document_type_name())
390                        .unwrap(),
391                    *extended_document.entropy(),
392                    None,
393                )
394            })
395            .collect::<Vec<_>>();
396        let transitions = get_batched_transitions_fixture(
397            [(DocumentTransitionActionType::Create, documents)],
398            &mut nonces,
399        );
400        let documents_batch_transition: BatchTransition = BatchTransitionV1 {
401            owner_id: data_contract.owner_id(),
402            transitions,
403            ..Default::default()
404        }
405        .into();
406        let state_transition: StateTransition = documents_batch_transition.into();
407        let bytes = state_transition
408            .serialize_to_bytes()
409            .expect("expected to serialize");
410        let recovered_state_transition = StateTransition::deserialize_from_bytes(&bytes)
411            .expect("expected to deserialize state transition");
412        assert_eq!(state_transition, recovered_state_transition);
413    }
414
415    #[test]
416    fn deserialize_empty_bytes_should_fail() {
417        let result = StateTransition::deserialize_from_bytes(&[]);
418        assert!(
419            result.is_err(),
420            "deserialization of empty bytes should fail"
421        );
422    }
423
424    #[test]
425    fn deserialize_single_byte_should_fail() {
426        let result = StateTransition::deserialize_from_bytes(&[0xFF]);
427        assert!(
428            result.is_err(),
429            "deserialization of a single 0xFF byte should fail"
430        );
431    }
432
433    #[test]
434    #[cfg(feature = "random-identities")]
435    fn deserialize_truncated_bytes_should_fail() {
436        let platform_version = PlatformVersion::latest();
437        let identity = Identity::random_identity(5, Some(5), platform_version)
438            .expect("expected a random identity");
439        let transition = IdentityCreditWithdrawalTransitionV0 {
440            identity_id: identity.id(),
441            amount: 5000000,
442            core_fee_per_byte: 34,
443            pooling: Pooling::Standard,
444            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
445            nonce: 1,
446            user_fee_increase: 0,
447            signature_public_key_id: 0,
448            signature: [1u8; 65].to_vec().into(),
449        };
450        let state_transition: StateTransition = transition.into();
451        let bytes = state_transition
452            .serialize_to_bytes()
453            .expect("expected to serialize");
454
455        // Truncate to half
456        let half = &bytes[..bytes.len() / 2];
457        assert!(
458            StateTransition::deserialize_from_bytes(half).is_err(),
459            "deserialization of truncated-to-half bytes should fail"
460        );
461
462        // Truncate by removing last byte
463        let minus_one = &bytes[..bytes.len() - 1];
464        assert!(
465            StateTransition::deserialize_from_bytes(minus_one).is_err(),
466            "deserialization of bytes missing last byte should fail"
467        );
468
469        // Keep only first byte
470        let first_only = &bytes[..1];
471        assert!(
472            StateTransition::deserialize_from_bytes(first_only).is_err(),
473            "deserialization of only the first byte should fail"
474        );
475    }
476
477    #[test]
478    #[cfg(feature = "random-identities")]
479    fn deserialize_corrupted_bytes_should_not_panic() {
480        let platform_version = PlatformVersion::latest();
481        let identity = Identity::random_identity(5, Some(5), platform_version)
482            .expect("expected a random identity");
483        let transition = IdentityCreditWithdrawalTransitionV0 {
484            identity_id: identity.id(),
485            amount: 5000000,
486            core_fee_per_byte: 34,
487            pooling: Pooling::Standard,
488            output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
489            nonce: 1,
490            user_fee_increase: 0,
491            signature_public_key_id: 0,
492            signature: [1u8; 65].to_vec().into(),
493        };
494        let state_transition: StateTransition = transition.into();
495        let mut bytes = state_transition
496            .serialize_to_bytes()
497            .expect("expected to serialize");
498
499        // Flip bits in the middle of the payload
500        let mid = bytes.len() / 2;
501        bytes[mid] ^= 0xFF;
502
503        // Should either fail or return a different value - must not panic
504        let result = StateTransition::deserialize_from_bytes(&bytes);
505        if let Ok(recovered) = result {
506            assert_ne!(
507                state_transition, recovered,
508                "corrupted bytes should not deserialize to the original value"
509            );
510        }
511    }
512
513    /// Crafts a minimal payload that looks like a StateTransition variant
514    /// (IdentityCreditWithdrawal = discriminant 5) followed by a version byte
515    /// and then a Vec<u8> field with a varint-encoded length that claims to
516    /// contain `fake_len` bytes. The total payload is small but the decoded
517    /// length would trigger a huge allocation without the limit guard.
518    fn craft_oversized_vec_payload(fake_len: u64) -> Vec<u8> {
519        let config = bincode::config::standard()
520            .with_big_endian()
521            .with_no_limit();
522        // Build the payload: variant discriminant + version + bogus vec length + filler
523        let mut buf = Vec::new();
524        // StateTransition enum discriminant for IdentityCreditWithdrawal (index 5)
525        buf.extend_from_slice(&bincode::encode_to_vec(5u32, config).unwrap());
526        // Version byte (0 = V0)
527        buf.push(0);
528        // identity_id: 32 bytes (Identifier)
529        buf.extend_from_slice(&[0u8; 32]);
530        // amount: u64
531        buf.extend_from_slice(&bincode::encode_to_vec(1000u64, config).unwrap());
532        // core_fee_per_byte: u32
533        buf.extend_from_slice(&bincode::encode_to_vec(1u32, config).unwrap());
534        // pooling: enum variant 0
535        buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
536        // output_script (CoreScript = BinaryData = Vec<u8>): encode the malicious length
537        buf.extend_from_slice(&bincode::encode_to_vec(fake_len, config).unwrap());
538        // Don't provide the actual bytes — the limit check should fire before allocation
539        buf
540    }
541
542    #[test]
543    fn deserialize_crafted_huge_vec_length_does_not_oom() {
544        // Craft a small payload (~80 bytes) with a Vec<u8> field claiming 8 GB.
545        // Without the limit fix this would attempt `vec![0u8; 8_000_000_000]` and abort.
546        let payload = craft_oversized_vec_payload(8_000_000_000);
547        let result = StateTransition::deserialize_from_bytes(&payload);
548        // Must return an error, not OOM-abort the process
549        assert!(
550            result.is_err(),
551            "crafted payload with 8GB vec length must be rejected, not cause OOM"
552        );
553    }
554
555    #[test]
556    fn deserialize_crafted_vec_exceeding_limit_is_rejected() {
557        // Craft a payload with a Vec<u8> claiming 200,000 bytes — exceeds the
558        // 100,000 byte budget configured on StateTransition.
559        // The limit causes bincode to reject the read (either as Io/LimitExceeded
560        // or UnexpectedEnd depending on the exact code path). Either way, the
561        // deserialization must fail safely without OOM.
562        let payload = craft_oversized_vec_payload(200_000);
563        let result = StateTransition::deserialize_from_bytes(&payload);
564        assert!(
565            result.is_err(),
566            "Vec length exceeding byte budget must be rejected"
567        );
568    }
569
570    #[test]
571    fn deserialize_no_limit_does_not_enforce_byte_budget() {
572        // Same crafted payload with 200,000-byte Vec — the no_limit variant
573        // should NOT reject it for byte budget reasons (it will still fail
574        // because the data doesn't actually contain 200,000 bytes).
575        let payload = craft_oversized_vec_payload(200_000);
576        let result = StateTransition::deserialize_from_bytes_no_limit(&payload);
577        assert!(result.is_err());
578        match result.unwrap_err() {
579            ProtocolError::MaxEncodedBytesReachedError { .. } => {
580                panic!("deserialize_from_bytes_no_limit should NOT enforce byte budget");
581            }
582            _ => {} // any other error is fine — the data is garbage
583        }
584    }
585
586    #[test]
587    fn deserialize_many_empty_list() {
588        let result = StateTransition::deserialize_many(&[]);
589        assert_eq!(result.unwrap(), vec![]);
590    }
591
592    #[test]
593    fn deserialize_many_with_invalid_entry() {
594        let result = StateTransition::deserialize_many(&[vec![0xFF]]);
595        assert!(
596            result.is_err(),
597            "deserialize_many with invalid entry should fail"
598        );
599    }
600
601    #[test]
602    #[cfg(feature = "random-identities")]
603    fn deserialize_many_with_valid_entries() {
604        let platform_version = PlatformVersion::latest();
605        let identity = Identity::random_identity(5, Some(5), platform_version)
606            .expect("expected a random identity");
607
608        let make_transition = |amount: u64, nonce: u64| -> StateTransition {
609            let t = IdentityCreditWithdrawalTransitionV0 {
610                identity_id: identity.id(),
611                amount,
612                core_fee_per_byte: 34,
613                pooling: Pooling::Standard,
614                output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
615                nonce,
616                user_fee_increase: 0,
617                signature_public_key_id: 0,
618                signature: [1u8; 65].to_vec().into(),
619            };
620            t.into()
621        };
622
623        let st1 = make_transition(1000000, 1);
624        let st2 = make_transition(2000000, 2);
625        let st3 = make_transition(3000000, 3);
626
627        let raw: Vec<Vec<u8>> = vec![
628            st1.serialize_to_bytes().unwrap(),
629            st2.serialize_to_bytes().unwrap(),
630            st3.serialize_to_bytes().unwrap(),
631        ];
632
633        let recovered = StateTransition::deserialize_many(&raw).expect("should deserialize all");
634        assert_eq!(recovered.len(), 3);
635        assert_eq!(recovered[0], st1);
636        assert_eq!(recovered[1], st2);
637        assert_eq!(recovered[2], st3);
638    }
639}