Skip to main content

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