pathfinder_common/
state_update.rs

1use std::collections::{hash_map, HashMap, HashSet};
2use std::slice;
3
4use fake::Dummy;
5
6use crate::{
7    BlockHash,
8    CasmHash,
9    ClassHash,
10    ContractAddress,
11    ContractNonce,
12    SierraHash,
13    StateCommitment,
14    StateDiffCommitment,
15    StorageAddress,
16    StorageValue,
17};
18
19#[derive(Default, Debug, Clone, PartialEq)]
20pub struct StateUpdate {
21    pub block_hash: BlockHash,
22    pub parent_state_commitment: StateCommitment,
23    pub state_commitment: StateCommitment,
24    pub contract_updates: HashMap<ContractAddress, ContractUpdate>,
25    pub system_contract_updates: HashMap<ContractAddress, SystemContractUpdate>,
26    pub declared_cairo_classes: HashSet<ClassHash>,
27    pub declared_sierra_classes: HashMap<SierraHash, CasmHash>,
28    pub migrated_compiled_classes: HashMap<SierraHash, CasmHash>,
29}
30
31#[derive(Default, Debug, Clone, PartialEq, Dummy)]
32pub struct StateUpdateData {
33    pub contract_updates: HashMap<ContractAddress, ContractUpdate>,
34    pub system_contract_updates: HashMap<ContractAddress, SystemContractUpdate>,
35    pub declared_cairo_classes: HashSet<ClassHash>,
36    pub declared_sierra_classes: HashMap<SierraHash, CasmHash>,
37    pub migrated_compiled_classes: HashMap<SierraHash, CasmHash>,
38}
39
40#[derive(Default, Debug, Clone, PartialEq, Dummy)]
41pub struct ContractUpdate {
42    pub storage: HashMap<StorageAddress, StorageValue>,
43    /// The class associated with this update as the result of either a deploy
44    /// or class replacement transaction.
45    pub class: Option<ContractClassUpdate>,
46    pub nonce: Option<ContractNonce>,
47}
48
49#[derive(Default, Debug, Clone, PartialEq, Dummy)]
50pub struct SystemContractUpdate {
51    pub storage: HashMap<StorageAddress, StorageValue>,
52}
53
54#[derive(Debug, Copy, Clone, PartialEq, Dummy)]
55pub enum ContractClassUpdate {
56    Deploy(ClassHash),
57    Replace(ClassHash),
58}
59
60pub struct StateUpdateRef<'a> {
61    pub contract_updates: Vec<(&'a ContractAddress, ContractUpdateRef<'a>)>,
62    pub system_contract_updates: Vec<(&'a ContractAddress, SystemContractUpdateRef<'a>)>,
63    pub declared_sierra_classes: &'a HashMap<SierraHash, CasmHash>,
64    pub migrated_compiled_classes: &'a HashMap<SierraHash, CasmHash>,
65}
66
67pub struct ContractUpdateRef<'a> {
68    pub storage: StorageRef<'a>,
69    pub class: &'a Option<ContractClassUpdate>,
70    pub nonce: &'a Option<ContractNonce>,
71}
72
73pub struct SystemContractUpdateRef<'a> {
74    pub storage: StorageRef<'a>,
75}
76
77#[derive(Copy, Clone)]
78pub enum StorageRef<'a> {
79    HashMap(&'a HashMap<StorageAddress, StorageValue>),
80    Vec(&'a Vec<(StorageAddress, StorageValue)>),
81}
82
83pub enum StorageRefIter<'a> {
84    HashMap(hash_map::Iter<'a, StorageAddress, StorageValue>),
85    Vec(slice::Iter<'a, (StorageAddress, StorageValue)>),
86}
87
88impl ContractUpdate {
89    pub fn replaced_class(&self) -> Option<&ClassHash> {
90        match &self.class {
91            Some(ContractClassUpdate::Replace(hash)) => Some(hash),
92            _ => None,
93        }
94    }
95
96    pub fn deployed_class(&self) -> Option<&ClassHash> {
97        match &self.class {
98            Some(ContractClassUpdate::Deploy(hash)) => Some(hash),
99            _ => None,
100        }
101    }
102}
103
104impl ContractClassUpdate {
105    pub fn class_hash(&self) -> ClassHash {
106        match self {
107            ContractClassUpdate::Deploy(x) => *x,
108            ContractClassUpdate::Replace(x) => *x,
109        }
110    }
111
112    pub fn is_replaced(&self) -> bool {
113        matches!(self, ContractClassUpdate::Replace(_))
114    }
115}
116
117impl StateUpdate {
118    pub fn with_block_hash(mut self, block_hash: BlockHash) -> Self {
119        self.block_hash = block_hash;
120        self
121    }
122
123    pub fn with_state_commitment(mut self, state_commitment: StateCommitment) -> Self {
124        self.state_commitment = state_commitment;
125        self
126    }
127
128    pub fn with_parent_state_commitment(
129        mut self,
130        parent_state_commitment: StateCommitment,
131    ) -> Self {
132        self.parent_state_commitment = parent_state_commitment;
133        self
134    }
135
136    pub fn with_contract_nonce(mut self, contract: ContractAddress, nonce: ContractNonce) -> Self {
137        self.contract_updates.entry(contract).or_default().nonce = Some(nonce);
138        self
139    }
140
141    pub fn with_storage_update(
142        mut self,
143        contract: ContractAddress,
144        key: StorageAddress,
145        value: StorageValue,
146    ) -> Self {
147        self.contract_updates
148            .entry(contract)
149            .or_default()
150            .storage
151            .insert(key, value);
152        self
153    }
154
155    pub fn with_system_storage_update(
156        mut self,
157        contract: ContractAddress,
158        key: StorageAddress,
159        value: StorageValue,
160    ) -> Self {
161        self.system_contract_updates
162            .entry(contract)
163            .or_default()
164            .storage
165            .insert(key, value);
166        self
167    }
168
169    pub fn with_deployed_contract(mut self, contract: ContractAddress, class: ClassHash) -> Self {
170        self.contract_updates.entry(contract).or_default().class =
171            Some(ContractClassUpdate::Deploy(class));
172        self
173    }
174
175    pub fn with_replaced_class(mut self, contract: ContractAddress, class: ClassHash) -> Self {
176        self.contract_updates.entry(contract).or_default().class =
177            Some(ContractClassUpdate::Replace(class));
178        self
179    }
180
181    pub fn with_declared_sierra_class(mut self, sierra: SierraHash, casm: CasmHash) -> Self {
182        self.declared_sierra_classes.insert(sierra, casm);
183        self
184    }
185
186    pub fn with_declared_cairo_class(mut self, cairo: ClassHash) -> Self {
187        self.declared_cairo_classes.insert(cairo);
188        self
189    }
190
191    pub fn with_migrated_compiled_class(mut self, sierra: SierraHash, casm: CasmHash) -> Self {
192        self.migrated_compiled_classes.insert(sierra, casm);
193        self
194    }
195
196    /// The number of individual changes in this state update.
197    ///
198    /// The total amount of:
199    /// - system storage updates
200    /// - contract storage updates
201    /// - contract nonce updates
202    /// - contract deployments
203    /// - contract class replacements
204    /// - class declarations
205    pub fn change_count(&self) -> usize {
206        self.declared_cairo_classes.len()
207            + self.declared_sierra_classes.len()
208            + self
209                .system_contract_updates
210                .iter()
211                .map(|x| x.1.storage.len())
212                .sum::<usize>()
213            + self
214                .contract_updates
215                .iter()
216                .map(|x| {
217                    x.1.storage.len()
218                        + x.1.class.as_ref().map(|_| 1).unwrap_or_default()
219                        + x.1.nonce.as_ref().map(|_| 1).unwrap_or_default()
220                })
221                .sum::<usize>()
222    }
223
224    /// Returns the contract's new [nonce](ContractNonce) value if it exists in
225    /// this state update.
226    ///
227    /// Note that this will return [Some(ContractNonce::ZERO)] for a contract
228    /// that has been deployed, but without an explicit nonce update. This
229    /// is consistent with expectations.
230    pub fn contract_nonce(&self, contract: ContractAddress) -> Option<ContractNonce> {
231        self.contract_updates.get(&contract).and_then(|x| {
232            x.nonce.or_else(|| {
233                x.class.as_ref().and_then(|c| match c {
234                    ContractClassUpdate::Deploy(_) => {
235                        // The contract has been just deployed in the pending block, so
236                        // its nonce is zero.
237                        Some(ContractNonce::ZERO)
238                    }
239                    ContractClassUpdate::Replace(_) => None,
240                })
241            })
242        })
243    }
244
245    /// A contract's new class hash, if it was deployed or replaced in this
246    /// state update.
247    pub fn contract_class(&self, contract: ContractAddress) -> Option<ClassHash> {
248        self.contract_updates
249            .get(&contract)
250            .and_then(|x| x.class.as_ref().map(|x| x.class_hash()))
251    }
252
253    /// Returns true if the class was declared as either a cairo 0 or sierra
254    /// class.
255    pub fn class_is_declared(&self, class: ClassHash) -> bool {
256        if self.declared_cairo_classes.contains(&class) {
257            return true;
258        }
259
260        self.declared_sierra_classes
261            .contains_key(&SierraHash(class.0))
262    }
263
264    /// The new storage value if it exists in this state update.
265    ///
266    /// Note that this will also return the default zero value for a contract
267    /// that has been deployed, but without an explicit storage update.
268    pub fn storage_value(
269        &self,
270        contract: ContractAddress,
271        key: StorageAddress,
272    ) -> Option<StorageValue> {
273        self.contract_updates
274            .get(&contract)
275            .and_then(|update| {
276                update
277                    .storage
278                    .iter()
279                    .find_map(|(k, v)| (k == &key).then_some(*v))
280                    .or_else(|| {
281                        update.class.as_ref().and_then(|c| match c {
282                            // If the contract has been deployed in pending but the key has not been
283                            // set yet return the default value of zero.
284                            ContractClassUpdate::Deploy(_) => Some(StorageValue::ZERO),
285                            ContractClassUpdate::Replace(_) => None,
286                        })
287                    })
288            })
289            .or_else(|| {
290                self.system_contract_updates
291                    .get(&contract)
292                    .and_then(|update| {
293                        update
294                            .storage
295                            .iter()
296                            .find_map(|(k, v)| (k == &key).then_some(*v))
297                    })
298            })
299    }
300
301    pub fn compute_state_diff_commitment(&self) -> StateDiffCommitment {
302        state_diff_commitment::compute(
303            &self.contract_updates,
304            &self.system_contract_updates,
305            &self.declared_cairo_classes,
306            &self.declared_sierra_classes,
307            &self.migrated_compiled_classes,
308        )
309    }
310
311    pub fn state_diff_length(&self) -> u64 {
312        let mut len = 0;
313        self.contract_updates.iter().for_each(|(_, update)| {
314            len += update.storage.len();
315            len += usize::from(update.nonce.is_some());
316            len += usize::from(update.class.is_some());
317        });
318        self.system_contract_updates.iter().for_each(|(_, update)| {
319            len += update.storage.len();
320        });
321        len += self.declared_cairo_classes.len()
322            + self.declared_sierra_classes.len()
323            + self.migrated_compiled_classes.len();
324        len.try_into().expect("ptr size is 64bits")
325    }
326
327    /// Apply another state update on top of this one.
328    pub fn apply(mut self, other: &StateUpdate) -> Self {
329        self.block_hash = other.block_hash;
330        self.parent_state_commitment = other.parent_state_commitment;
331        self.state_commitment = other.state_commitment;
332
333        for (contract, other_update) in &other.contract_updates {
334            let update = self.contract_updates.entry(*contract).or_default();
335
336            // Merge storage updates.
337            for (key, value) in &other_update.storage {
338                update.storage.insert(*key, *value);
339            }
340
341            // Merge class updates.
342            if let Some(class_update) = &other_update.class {
343                update.class = Some(*class_update);
344            }
345
346            // Merge nonce updates.
347            if let Some(nonce) = other_update.nonce {
348                update.nonce = Some(nonce);
349            }
350        }
351
352        for (contract, other_update) in &other.system_contract_updates {
353            let update = self.system_contract_updates.entry(*contract).or_default();
354
355            // Merge storage updates.
356            for (key, value) in &other_update.storage {
357                update.storage.insert(*key, *value);
358            }
359        }
360
361        // Merge declared classes.
362        self.declared_cairo_classes
363            .extend(other.declared_cairo_classes.iter().copied());
364        self.declared_sierra_classes
365            .extend(other.declared_sierra_classes.iter().map(|(k, v)| (*k, *v)));
366
367        self
368    }
369}
370
371impl StateUpdateData {
372    pub fn compute_state_diff_commitment(&self) -> StateDiffCommitment {
373        state_diff_commitment::compute(
374            &self.contract_updates,
375            &self.system_contract_updates,
376            &self.declared_cairo_classes,
377            &self.declared_sierra_classes,
378            &self.migrated_compiled_classes,
379        )
380    }
381
382    pub fn is_empty(&self) -> bool {
383        self.contract_updates.is_empty()
384            && self.system_contract_updates.is_empty()
385            && self.declared_cairo_classes.is_empty()
386            && self.declared_sierra_classes.is_empty()
387    }
388
389    pub fn declared_classes(&self) -> DeclaredClasses {
390        DeclaredClasses {
391            sierra: self.declared_sierra_classes.clone(),
392            cairo: self.declared_cairo_classes.clone(),
393        }
394    }
395
396    pub fn state_diff_length(&self) -> u64 {
397        let mut len = 0;
398        self.contract_updates.iter().for_each(|(_, update)| {
399            len += update.storage.len();
400            len += usize::from(update.nonce.is_some());
401            len += usize::from(update.class.is_some());
402        });
403        self.system_contract_updates.iter().for_each(|(_, update)| {
404            len += update.storage.len();
405        });
406        len += self.declared_cairo_classes.len() + self.declared_sierra_classes.len();
407        len.try_into().expect("ptr size is 64bits")
408    }
409
410    pub fn as_ref(&self) -> StateUpdateRef<'_> {
411        StateUpdateRef::from(self)
412    }
413}
414
415impl From<StateUpdate> for StateUpdateData {
416    fn from(state_update: StateUpdate) -> Self {
417        Self {
418            contract_updates: state_update.contract_updates,
419            system_contract_updates: state_update.system_contract_updates,
420            declared_cairo_classes: state_update.declared_cairo_classes,
421            declared_sierra_classes: state_update.declared_sierra_classes,
422            migrated_compiled_classes: state_update.migrated_compiled_classes,
423        }
424    }
425}
426
427impl<'a> From<&'a StateUpdate> for StateUpdateRef<'a> {
428    fn from(state_update: &'a StateUpdate) -> Self {
429        Self {
430            contract_updates: state_update
431                .contract_updates
432                .iter()
433                .map(|(k, v)| {
434                    (
435                        k,
436                        ContractUpdateRef {
437                            storage: StorageRef::HashMap(&v.storage),
438                            class: &v.class,
439                            nonce: &v.nonce,
440                        },
441                    )
442                })
443                .collect(),
444            system_contract_updates: state_update
445                .system_contract_updates
446                .iter()
447                .map(|(k, v)| {
448                    (
449                        k,
450                        SystemContractUpdateRef {
451                            storage: StorageRef::HashMap(&v.storage),
452                        },
453                    )
454                })
455                .collect(),
456            declared_sierra_classes: &state_update.declared_sierra_classes,
457            migrated_compiled_classes: &state_update.migrated_compiled_classes,
458        }
459    }
460}
461
462impl<'a> From<&'a mut StateUpdate> for StateUpdateRef<'a> {
463    fn from(state_update: &'a mut StateUpdate) -> Self {
464        Self::from(state_update as &'a StateUpdate)
465    }
466}
467
468impl<'a> From<&'a StateUpdateData> for StateUpdateRef<'a> {
469    fn from(state_update: &'a StateUpdateData) -> Self {
470        Self {
471            contract_updates: state_update
472                .contract_updates
473                .iter()
474                .map(|(k, v)| {
475                    (
476                        k,
477                        ContractUpdateRef {
478                            storage: StorageRef::HashMap(&v.storage),
479                            class: &v.class,
480                            nonce: &v.nonce,
481                        },
482                    )
483                })
484                .collect(),
485            system_contract_updates: state_update
486                .system_contract_updates
487                .iter()
488                .map(|(k, v)| {
489                    (
490                        k,
491                        SystemContractUpdateRef {
492                            storage: StorageRef::HashMap(&v.storage),
493                        },
494                    )
495                })
496                .collect(),
497            declared_sierra_classes: &state_update.declared_sierra_classes,
498            migrated_compiled_classes: &state_update.migrated_compiled_classes,
499        }
500    }
501}
502
503impl<'a> From<&'a mut StateUpdateData> for StateUpdateRef<'a> {
504    fn from(state_update: &'a mut StateUpdateData) -> Self {
505        Self::from(state_update as &'a StateUpdateData)
506    }
507}
508
509impl StorageRef<'_> {
510    pub fn iter(&self) -> StorageRefIter<'_> {
511        match self {
512            StorageRef::HashMap(map) => StorageRefIter::HashMap(map.iter()),
513            StorageRef::Vec(vec) => StorageRefIter::Vec(vec.iter()),
514        }
515    }
516
517    pub fn is_empty(&self) -> bool {
518        match self {
519            StorageRef::HashMap(map) => map.is_empty(),
520            StorageRef::Vec(vec) => vec.is_empty(),
521        }
522    }
523}
524
525impl<'a> From<&'a ContractUpdate> for ContractUpdateRef<'a> {
526    fn from(x: &'a ContractUpdate) -> Self {
527        ContractUpdateRef {
528            storage: (&x.storage).into(),
529            class: &x.class,
530            nonce: &x.nonce,
531        }
532    }
533}
534
535impl<'a> From<&'a SystemContractUpdate> for SystemContractUpdateRef<'a> {
536    fn from(x: &'a SystemContractUpdate) -> Self {
537        SystemContractUpdateRef {
538            storage: (&x.storage).into(),
539        }
540    }
541}
542
543impl<'a> From<&'a HashMap<StorageAddress, StorageValue>> for StorageRef<'a> {
544    fn from(x: &'a HashMap<StorageAddress, StorageValue>) -> Self {
545        StorageRef::HashMap(x)
546    }
547}
548
549impl<'a> From<&'a Vec<(StorageAddress, StorageValue)>> for StorageRef<'a> {
550    fn from(x: &'a Vec<(StorageAddress, StorageValue)>) -> Self {
551        StorageRef::Vec(x)
552    }
553}
554
555impl<'a> IntoIterator for &'a StorageRef<'a> {
556    type Item = (&'a StorageAddress, &'a StorageValue);
557    type IntoIter = StorageRefIter<'a>;
558
559    fn into_iter(self) -> Self::IntoIter {
560        self.iter()
561    }
562}
563
564impl<'a> Iterator for StorageRefIter<'a> {
565    type Item = (&'a StorageAddress, &'a StorageValue);
566
567    fn next(&mut self) -> Option<Self::Item> {
568        match self {
569            StorageRefIter::HashMap(iter) => iter.next(),
570            StorageRefIter::Vec(iter) => iter.next().map(|(k, v)| (k, v)),
571        }
572    }
573}
574
575mod state_diff_commitment {
576    use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
577
578    use pathfinder_crypto::hash::PoseidonHasher;
579    use pathfinder_crypto::MontFelt;
580
581    use super::{ContractUpdate, SystemContractUpdate};
582    use crate::{
583        felt_bytes,
584        CasmHash,
585        ClassHash,
586        ContractAddress,
587        SierraHash,
588        StateDiffCommitment,
589    };
590
591    /// Compute the state diff commitment used in block commitment signatures.
592    ///
593    /// How to compute the value is documented in the [Starknet documentation](https://docs.starknet.io/architecture-and-concepts/network-architecture/block-structure/#state_diff_hash).
594    pub fn compute(
595        contract_updates: &HashMap<ContractAddress, ContractUpdate>,
596        system_contract_updates: &HashMap<ContractAddress, SystemContractUpdate>,
597        declared_cairo_classes: &HashSet<ClassHash>,
598        declared_sierra_classes: &HashMap<SierraHash, CasmHash>,
599        migrated_compiled_classes: &HashMap<SierraHash, CasmHash>,
600    ) -> StateDiffCommitment {
601        let mut hasher = PoseidonHasher::new();
602        hasher.write(felt_bytes!(b"STARKNET_STATE_DIFF0").into());
603        // Hash the deployed contracts.
604        let deployed_contracts: BTreeMap<_, _> = contract_updates
605            .iter()
606            .filter_map(|(address, update)| {
607                update
608                    .class
609                    .as_ref()
610                    .map(|update| (*address, update.class_hash()))
611            })
612            .collect();
613        hasher.write(MontFelt::from(deployed_contracts.len() as u64));
614        for (address, class_hash) in deployed_contracts {
615            hasher.write(MontFelt::from(address.0));
616            hasher.write(MontFelt::from(class_hash.0));
617        }
618        // Hash the declared classes and the migrated compiled classes.
619        let declared_classes: BTreeSet<_> = declared_sierra_classes
620            .iter()
621            .chain(migrated_compiled_classes.iter())
622            .map(|(sierra, casm)| (*sierra, *casm))
623            .collect();
624        hasher.write(MontFelt::from(declared_classes.len() as u64));
625        for (sierra, casm) in declared_classes {
626            hasher.write(MontFelt::from(sierra.0));
627            hasher.write(MontFelt::from(casm.0));
628        }
629        // Hash the old declared classes.
630        let deprecated_declared_classes: BTreeSet<_> =
631            declared_cairo_classes.iter().copied().collect();
632        hasher.write(MontFelt::from(deprecated_declared_classes.len() as u64));
633        for class_hash in deprecated_declared_classes {
634            hasher.write(MontFelt::from(class_hash.0));
635        }
636        hasher.write(MontFelt::ONE);
637        hasher.write(MontFelt::ZERO);
638        // Hash the storage diffs.
639        let storage_diffs: BTreeMap<_, _> = contract_updates
640            .iter()
641            .map(|(address, update)| (address, &update.storage))
642            .chain(
643                system_contract_updates
644                    .iter()
645                    .map(|(address, update)| (address, &update.storage)),
646            )
647            .filter_map(|(address, storage)| {
648                if storage.is_empty() {
649                    None
650                } else {
651                    let updates: BTreeMap<_, _> =
652                        storage.iter().map(|(key, value)| (*key, *value)).collect();
653                    Some((*address, updates))
654                }
655            })
656            .collect();
657        hasher.write(MontFelt::from(storage_diffs.len() as u64));
658        for (address, updates) in storage_diffs {
659            hasher.write(MontFelt::from(address.0));
660            hasher.write(MontFelt::from(updates.len() as u64));
661            for (key, value) in updates {
662                hasher.write(MontFelt::from(key.0));
663                hasher.write(MontFelt::from(value.0));
664            }
665        }
666        // Hash the nonce updates.
667        let nonces: BTreeMap<_, _> = contract_updates
668            .iter()
669            .filter_map(|(address, update)| update.nonce.map(|nonce| (*address, nonce)))
670            .collect();
671        hasher.write(MontFelt::from(nonces.len() as u64));
672        for (address, nonce) in nonces {
673            hasher.write(MontFelt::from(address.0));
674            hasher.write(MontFelt::from(nonce.0));
675        }
676        StateDiffCommitment(hasher.finish().into())
677    }
678}
679
680#[derive(Debug, PartialEq)]
681pub enum ReverseContractUpdate {
682    Deleted,
683    Updated(ContractUpdate),
684}
685
686impl ReverseContractUpdate {
687    pub fn update_mut(&mut self) -> Option<&mut ContractUpdate> {
688        match self {
689            Self::Deleted => None,
690            Self::Updated(update) => Some(update),
691        }
692    }
693}
694
695#[derive(Clone, Debug, PartialEq)]
696pub struct DeclaredClasses {
697    pub sierra: HashMap<SierraHash, CasmHash>,
698    pub cairo: HashSet<ClassHash>,
699}
700
701impl DeclaredClasses {
702    pub fn is_empty(&self) -> bool {
703        self.len() == 0
704    }
705
706    pub fn len(&self) -> usize {
707        self.sierra.len() + self.cairo.len()
708    }
709}
710
711#[derive(Debug, thiserror::Error)]
712pub enum StateUpdateError {
713    #[error("Contract class hash missing for contract {0}")]
714    ContractClassHashMissing(ContractAddress),
715    #[error(transparent)]
716    StorageError(#[from] anyhow::Error),
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722    use crate::macro_prelude::*;
723
724    #[test]
725    fn change_count() {
726        let state_update = StateUpdate::default()
727            .with_contract_nonce(contract_address!("0x1"), contract_nonce!("0x2"))
728            .with_contract_nonce(contract_address!("0x4"), contract_nonce!("0x5"))
729            .with_declared_cairo_class(class_hash!("0x3"))
730            .with_declared_sierra_class(sierra_hash!("0x4"), casm_hash!("0x5"))
731            .with_deployed_contract(contract_address!("0x1"), class_hash!("0x3"))
732            .with_replaced_class(contract_address!("0x33"), class_hash!("0x35"))
733            .with_system_storage_update(
734                ContractAddress::ONE,
735                storage_address!("0x10"),
736                storage_value!("0x99"),
737            )
738            .with_storage_update(
739                contract_address!("0x33"),
740                storage_address!("0x10"),
741                storage_value!("0x99"),
742            );
743
744        assert_eq!(state_update.change_count(), 8);
745    }
746
747    #[test]
748    fn contract_nonce() {
749        let state_update = StateUpdate::default()
750            .with_contract_nonce(contract_address!("0x1"), contract_nonce!("0x2"))
751            .with_deployed_contract(contract_address!("0x2"), class_hash!("0x4"))
752            .with_contract_nonce(contract_address!("0x10"), contract_nonce!("0x20"))
753            .with_deployed_contract(contract_address!("0x10"), class_hash!("0x12"))
754            .with_replaced_class(contract_address!("0x123"), class_hash!("0x1244"))
755            .with_replaced_class(contract_address!("0x1234"), class_hash!("0x12445"))
756            .with_contract_nonce(contract_address!("0x1234"), contract_nonce!("0x1111"));
757
758        assert!(state_update
759            .contract_nonce(contract_address_bytes!(b"not present"))
760            .is_none());
761
762        let result = state_update.contract_nonce(contract_address!("0x1"));
763        assert_eq!(result, Some(contract_nonce!("0x2")));
764
765        // A newly deployed contract with an explicit nonce set.
766        let result = state_update.contract_nonce(contract_address!("0x10"));
767        assert_eq!(result, Some(contract_nonce!("0x20")));
768
769        // A newly deployed contract without an explicit nonce set should be zero
770        let result = state_update.contract_nonce(contract_address!("0x2"));
771        assert_eq!(result, Some(ContractNonce::ZERO));
772
773        // A replaced contract with an explicit nonce set.
774        let result = state_update.contract_nonce(contract_address!("0x1234"));
775        assert_eq!(result, Some(contract_nonce!("0x1111")));
776
777        // A replaced class without an explicit nonce.
778        assert!(state_update
779            .contract_nonce(contract_address!("0x123"))
780            .is_none());
781    }
782
783    mod storage_value {
784        use super::*;
785
786        #[test]
787        fn set() {
788            let c = contract_address!("0x1");
789            let k = storage_address!("0x2");
790            let v = storage_value!("0x3");
791            let state_update = StateUpdate::default().with_storage_update(c, k, v);
792            let result = state_update.storage_value(c, k);
793            assert_eq!(result, Some(v))
794        }
795
796        #[test]
797        fn not_set() {
798            let c = contract_address!("0x1");
799            let k = storage_address!("0x2");
800            let v = storage_value!("0x3");
801            let state_update = StateUpdate::default().with_storage_update(c, k, v);
802            let result = state_update.storage_value(contract_address!("0x4"), k);
803            assert!(result.is_none());
804
805            let result = state_update.storage_value(c, storage_address!("0x24"));
806            assert!(result.is_none());
807        }
808
809        #[test]
810        fn deployed_and_not_set() {
811            let c = contract_address!("0x1");
812            let state_update = StateUpdate::default().with_deployed_contract(c, class_hash!("0x1"));
813            let result = state_update.storage_value(c, storage_address!("0x2"));
814            assert_eq!(result, Some(StorageValue::ZERO));
815        }
816
817        #[test]
818        fn deployed_and_set() {
819            let c = contract_address!("0x1");
820            let k = storage_address!("0x2");
821            let v = storage_value!("0x3");
822            let state_update = StateUpdate::default()
823                .with_deployed_contract(c, class_hash!("0x1"))
824                .with_storage_update(c, k, v);
825            let result = state_update.storage_value(c, k);
826            assert_eq!(result, Some(v));
827        }
828
829        #[test]
830        fn replaced_and_not_set() {
831            let c = contract_address!("0x1");
832            let state_update = StateUpdate::default().with_replaced_class(c, class_hash!("0x1"));
833            let result = state_update.storage_value(c, storage_address!("0x2"));
834            assert!(result.is_none());
835        }
836
837        #[test]
838        fn replaced_and_set() {
839            let c = contract_address!("0x1");
840            let k = storage_address!("0x2");
841            let v = storage_value!("0x3");
842            let state_update = StateUpdate::default()
843                .with_replaced_class(c, class_hash!("0x1"))
844                .with_storage_update(c, k, v);
845            let result = state_update.storage_value(c, k);
846            assert_eq!(result, Some(v));
847        }
848
849        #[test]
850        fn system_contract_and_set() {
851            let c = contract_address!("0x1");
852            let k = storage_address!("0x2");
853            let v = storage_value!("0x3");
854            let state_update = StateUpdate::default().with_system_storage_update(c, k, v);
855            let result = state_update.storage_value(c, k);
856            assert_eq!(result, Some(v))
857        }
858
859        #[test]
860        fn system_contract_and_not_set() {
861            let c = contract_address!("0x1");
862            let k = storage_address!("0x2");
863            let v = storage_value!("0x3");
864            let state_update = StateUpdate::default().with_system_storage_update(c, k, v);
865            let result = state_update.storage_value(contract_address!("0x4"), k);
866            assert_eq!(result, None);
867            let result = state_update.storage_value(c, storage_address!("0x24"));
868            assert_eq!(result, None);
869        }
870    }
871
872    #[test]
873    fn class_is_declared() {
874        let cairo = class_hash_bytes!(b"cairo class");
875        let sierra = class_hash_bytes!(b"sierra class");
876
877        let state_update = StateUpdate::default()
878            .with_declared_cairo_class(cairo)
879            .with_declared_sierra_class(SierraHash(sierra.0), casm_hash_bytes!(b"anything"));
880
881        assert!(state_update.class_is_declared(cairo));
882        assert!(state_update.class_is_declared(sierra));
883        assert!(!state_update.class_is_declared(class_hash_bytes!(b"nope")));
884    }
885
886    #[test]
887    fn contract_class() {
888        let deployed = contract_address_bytes!(b"deployed");
889        let deployed_class = class_hash_bytes!(b"deployed class");
890        let replaced = contract_address_bytes!(b"replaced");
891        let replaced_class = class_hash_bytes!(b"replaced class");
892
893        let state_update = StateUpdate::default()
894            .with_deployed_contract(deployed, deployed_class)
895            .with_replaced_class(replaced, replaced_class);
896
897        let result = state_update.contract_class(deployed);
898        assert_eq!(result, Some(deployed_class));
899
900        let result = state_update.contract_class(replaced);
901        assert_eq!(result, Some(replaced_class));
902
903        assert!(state_update
904            .contract_class(contract_address_bytes!(b"bogus"))
905            .is_none());
906    }
907
908    /// Source:
909    /// https://github.com/starkware-libs/starknet-api/blob/5565e5282f5fead364a41e49c173940fd83dee00/src/block_hash/state_diff_hash_test.rs#L14
910    #[test]
911    fn test_0_13_2_state_diff_commitment() {
912        let contract_updates: HashMap<_, _> = [
913            (
914                ContractAddress(0u64.into()),
915                ContractUpdate {
916                    class: Some(ContractClassUpdate::Deploy(ClassHash(1u64.into()))),
917                    ..Default::default()
918                },
919            ),
920            (
921                ContractAddress(2u64.into()),
922                ContractUpdate {
923                    class: Some(ContractClassUpdate::Deploy(ClassHash(3u64.into()))),
924                    ..Default::default()
925                },
926            ),
927            (
928                ContractAddress(4u64.into()),
929                ContractUpdate {
930                    storage: [
931                        (StorageAddress(5u64.into()), StorageValue(6u64.into())),
932                        (StorageAddress(7u64.into()), StorageValue(8u64.into())),
933                    ]
934                    .iter()
935                    .cloned()
936                    .collect(),
937                    ..Default::default()
938                },
939            ),
940            (
941                ContractAddress(9u64.into()),
942                ContractUpdate {
943                    storage: [(StorageAddress(10u64.into()), StorageValue(11u64.into()))]
944                        .iter()
945                        .cloned()
946                        .collect(),
947                    ..Default::default()
948                },
949            ),
950            (
951                ContractAddress(17u64.into()),
952                ContractUpdate {
953                    nonce: Some(ContractNonce(18u64.into())),
954                    ..Default::default()
955                },
956            ),
957            (
958                ContractAddress(19u64.into()),
959                ContractUpdate {
960                    class: Some(ContractClassUpdate::Replace(ClassHash(20u64.into()))),
961                    ..Default::default()
962                },
963            ),
964        ]
965        .into_iter()
966        .collect();
967        let declared_sierra_classes: HashMap<_, _> = [
968            (SierraHash(12u64.into()), CasmHash(13u64.into())),
969            (SierraHash(14u64.into()), CasmHash(15u64.into())),
970        ]
971        .iter()
972        .cloned()
973        .collect();
974        let declared_cairo_classes: HashSet<_> =
975            [ClassHash(16u64.into())].iter().cloned().collect();
976
977        let expected_hash = StateDiffCommitment(felt!(
978            "0x0281f5966e49ad7dad9323826d53d1d27c0c4e6ebe5525e2e2fbca549bfa0a67"
979        ));
980
981        assert_eq!(
982            expected_hash,
983            state_diff_commitment::compute(
984                &contract_updates,
985                &Default::default(),
986                &declared_cairo_classes,
987                &declared_sierra_classes,
988                &Default::default(),
989            )
990        );
991    }
992
993    /// Source:
994    /// https://github.com/starkware-libs/starknet-api/blob/5565e5282f5fead364a41e49c173940fd83dee00/src/block_hash/state_diff_hash_test.rs#L14
995    #[test]
996    fn test_0_13_2_state_diff_commitment_with_migrated_compiled_classes() {
997        let contract_updates: HashMap<_, _> = [
998            (
999                ContractAddress(0u64.into()),
1000                ContractUpdate {
1001                    class: Some(ContractClassUpdate::Deploy(ClassHash(1u64.into()))),
1002                    ..Default::default()
1003                },
1004            ),
1005            (
1006                ContractAddress(2u64.into()),
1007                ContractUpdate {
1008                    class: Some(ContractClassUpdate::Deploy(ClassHash(3u64.into()))),
1009                    ..Default::default()
1010                },
1011            ),
1012            (
1013                ContractAddress(4u64.into()),
1014                ContractUpdate {
1015                    storage: [
1016                        (StorageAddress(5u64.into()), StorageValue(6u64.into())),
1017                        (StorageAddress(7u64.into()), StorageValue(8u64.into())),
1018                    ]
1019                    .iter()
1020                    .cloned()
1021                    .collect(),
1022                    ..Default::default()
1023                },
1024            ),
1025            (
1026                ContractAddress(9u64.into()),
1027                ContractUpdate {
1028                    storage: [(StorageAddress(10u64.into()), StorageValue(11u64.into()))]
1029                        .iter()
1030                        .cloned()
1031                        .collect(),
1032                    ..Default::default()
1033                },
1034            ),
1035            (
1036                ContractAddress(17u64.into()),
1037                ContractUpdate {
1038                    nonce: Some(ContractNonce(18u64.into())),
1039                    ..Default::default()
1040                },
1041            ),
1042            (
1043                ContractAddress(19u64.into()),
1044                ContractUpdate {
1045                    class: Some(ContractClassUpdate::Replace(ClassHash(20u64.into()))),
1046                    ..Default::default()
1047                },
1048            ),
1049        ]
1050        .into_iter()
1051        .collect();
1052        let declared_sierra_classes: HashMap<_, _> =
1053            [(SierraHash(12u64.into()), CasmHash(13u64.into()))]
1054                .iter()
1055                .cloned()
1056                .collect();
1057        let migrated_compiled_classes: HashMap<_, _> =
1058            [(SierraHash(14u64.into()), CasmHash(15u64.into()))]
1059                .iter()
1060                .cloned()
1061                .collect();
1062        let declared_cairo_classes: HashSet<_> =
1063            [ClassHash(16u64.into())].iter().cloned().collect();
1064
1065        let expected_hash = StateDiffCommitment(felt!(
1066            "0x0281f5966e49ad7dad9323826d53d1d27c0c4e6ebe5525e2e2fbca549bfa0a67"
1067        ));
1068
1069        assert_eq!(
1070            expected_hash,
1071            state_diff_commitment::compute(
1072                &contract_updates,
1073                &Default::default(),
1074                &declared_cairo_classes,
1075                &declared_sierra_classes,
1076                &migrated_compiled_classes,
1077            )
1078        );
1079    }
1080
1081    #[test]
1082    fn apply() {
1083        let state_update = StateUpdate::default()
1084            .with_contract_nonce(contract_address!("0x1"), contract_nonce!("0x2"))
1085            .with_contract_nonce(contract_address!("0x4"), contract_nonce!("0x5"))
1086            .with_declared_cairo_class(class_hash!("0x3"))
1087            .with_declared_sierra_class(sierra_hash!("0x4"), casm_hash!("0x5"))
1088            .with_deployed_contract(contract_address!("0x1"), class_hash!("0x3"))
1089            .with_replaced_class(contract_address!("0x33"), class_hash!("0x35"))
1090            .with_system_storage_update(
1091                ContractAddress::ONE,
1092                storage_address!("0x10"),
1093                storage_value!("0x99"),
1094            )
1095            .with_storage_update(
1096                contract_address!("0x33"),
1097                storage_address!("0x10"),
1098                storage_value!("0x99"),
1099            );
1100
1101        let second_state_update = StateUpdate::default()
1102            .with_contract_nonce(contract_address!("0x1"), contract_nonce!("0x3"))
1103            .with_contract_nonce(contract_address!("0x5"), contract_nonce!("0x5"))
1104            .with_declared_cairo_class(class_hash!("0x6"))
1105            .with_declared_sierra_class(sierra_hash!("0x7"), casm_hash!("0x8"))
1106            .with_deployed_contract(contract_address!("0x9"), class_hash!("0x7"))
1107            .with_replaced_class(contract_address!("0x33"), class_hash!("0x37"))
1108            .with_system_storage_update(
1109                ContractAddress::ONE,
1110                storage_address!("0x11"),
1111                storage_value!("0x100"),
1112            )
1113            .with_storage_update(
1114                contract_address!("0x33"),
1115                storage_address!("0x10"),
1116                storage_value!("0x100"),
1117            );
1118
1119        let combined = state_update.apply(&second_state_update);
1120        let expected = StateUpdate::default()
1121            .with_contract_nonce(contract_address!("0x1"), contract_nonce!("0x3"))
1122            .with_contract_nonce(contract_address!("0x4"), contract_nonce!("0x5"))
1123            .with_contract_nonce(contract_address!("0x5"), contract_nonce!("0x5"))
1124            .with_declared_cairo_class(class_hash!("0x3"))
1125            .with_declared_cairo_class(class_hash!("0x6"))
1126            .with_declared_sierra_class(sierra_hash!("0x4"), casm_hash!("0x5"))
1127            .with_declared_sierra_class(sierra_hash!("0x7"), casm_hash!("0x8"))
1128            .with_deployed_contract(contract_address!("0x1"), class_hash!("0x3"))
1129            .with_deployed_contract(contract_address!("0x9"), class_hash!("0x7"))
1130            .with_replaced_class(contract_address!("0x33"), class_hash!("0x37"))
1131            .with_system_storage_update(
1132                ContractAddress::ONE,
1133                storage_address!("0x10"),
1134                storage_value!("0x99"),
1135            )
1136            .with_system_storage_update(
1137                ContractAddress::ONE,
1138                storage_address!("0x11"),
1139                storage_value!("0x100"),
1140            )
1141            .with_storage_update(
1142                contract_address!("0x33"),
1143                storage_address!("0x10"),
1144                storage_value!("0x100"),
1145            );
1146        assert_eq!(combined, expected);
1147    }
1148}