Skip to main content

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