Skip to main content

sui_core/
global_state_hasher.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use itertools::Itertools;
5use mysten_common::ZipDebugEqIteratorExt;
6use mysten_common::fatal;
7use mysten_metrics::monitored_scope;
8use prometheus::{IntGauge, Registry, register_int_gauge_with_registry};
9use serde::Serialize;
10use sui_protocol_config::ProtocolConfig;
11use sui_types::base_types::{ObjectID, ObjectRef, SequenceNumber, VersionNumber};
12use sui_types::committee::EpochId;
13use sui_types::digests::{ObjectDigest, TransactionDigest};
14use sui_types::in_memory_storage::InMemoryStorage;
15use sui_types::storage::{ObjectKey, ObjectStore};
16use tracing::debug;
17
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21use fastcrypto::hash::MultisetHash;
22use sui_types::effects::TransactionEffects;
23use sui_types::effects::TransactionEffectsAPI;
24use sui_types::error::SuiResult;
25use sui_types::global_state_hash::GlobalStateHash;
26use sui_types::messages_checkpoint::{CheckpointSequenceNumber, ECMHLiveObjectSetDigest};
27
28use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
29use crate::authority::authority_store_tables::LiveObject;
30
31pub struct GlobalStateHashMetrics {
32    inconsistent_state: IntGauge,
33}
34
35impl GlobalStateHashMetrics {
36    pub fn new(registry: &Registry) -> Arc<Self> {
37        let this = Self {
38            inconsistent_state: register_int_gauge_with_registry!(
39                "global_state_hasher_inconsistent_state",
40                "1 if accumulated live object set differs from GlobalStateHasher root state hash for the previous epoch",
41                registry
42            )
43            .unwrap(),
44        };
45        Arc::new(this)
46    }
47}
48
49pub struct GlobalStateHasher {
50    store: Arc<dyn GlobalStateHashStore>,
51    metrics: Arc<GlobalStateHashMetrics>,
52}
53
54pub trait GlobalStateHashStore: ObjectStore + Send + Sync {
55    /// This function is only called in older protocol versions, and should no longer be used.
56    /// It creates an explicit dependency to tombstones which is not desired.
57    fn get_object_ref_prior_to_key_deprecated(
58        &self,
59        object_id: &ObjectID,
60        version: VersionNumber,
61    ) -> SuiResult<Option<ObjectRef>>;
62
63    fn get_root_state_hash_for_epoch(
64        &self,
65        epoch: EpochId,
66    ) -> SuiResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>>;
67
68    fn insert_state_hash_for_epoch(
69        &self,
70        epoch: EpochId,
71        checkpoint_seq_num: &CheckpointSequenceNumber,
72        acc: &GlobalStateHash,
73    ) -> SuiResult;
74
75    fn iter_live_object_set(
76        &self,
77        include_wrapped_tombstone: bool,
78    ) -> Box<dyn Iterator<Item = LiveObject> + '_>;
79
80    fn iter_cached_live_object_set_for_testing(
81        &self,
82        include_wrapped_tombstone: bool,
83    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
84        self.iter_live_object_set(include_wrapped_tombstone)
85    }
86}
87
88impl GlobalStateHashStore for InMemoryStorage {
89    fn get_object_ref_prior_to_key_deprecated(
90        &self,
91        _object_id: &ObjectID,
92        _version: VersionNumber,
93    ) -> SuiResult<Option<ObjectRef>> {
94        unreachable!(
95            "get_object_ref_prior_to_key is only called by accumulate_effects_v1, while InMemoryStorage is used by testing and genesis only, which always uses latest protocol "
96        )
97    }
98
99    fn get_root_state_hash_for_epoch(
100        &self,
101        _epoch: EpochId,
102    ) -> SuiResult<Option<(CheckpointSequenceNumber, GlobalStateHash)>> {
103        unreachable!("not used for testing")
104    }
105
106    fn insert_state_hash_for_epoch(
107        &self,
108        _epoch: EpochId,
109        _checkpoint_seq_num: &CheckpointSequenceNumber,
110        _acc: &GlobalStateHash,
111    ) -> SuiResult {
112        unreachable!("not used for testing")
113    }
114
115    fn iter_live_object_set(
116        &self,
117        _include_wrapped_tombstone: bool,
118    ) -> Box<dyn Iterator<Item = LiveObject> + '_> {
119        unreachable!("not used for testing")
120    }
121}
122
123/// Serializable representation of the ObjectRef of an
124/// object that has been wrapped
125/// TODO: This can be replaced with ObjectKey.
126#[derive(Serialize, Debug)]
127pub struct WrappedObject {
128    id: ObjectID,
129    wrapped_at: SequenceNumber,
130    digest: ObjectDigest,
131}
132
133impl WrappedObject {
134    pub fn new(id: ObjectID, wrapped_at: SequenceNumber) -> Self {
135        Self {
136            id,
137            wrapped_at,
138            digest: ObjectDigest::OBJECT_DIGEST_WRAPPED,
139        }
140    }
141}
142
143pub fn accumulate_effects<T, S>(
144    store: S,
145    effects: &[TransactionEffects],
146    protocol_config: &ProtocolConfig,
147) -> GlobalStateHash
148where
149    S: std::ops::Deref<Target = T>,
150    T: GlobalStateHashStore + ?Sized,
151{
152    if protocol_config.enable_effects_v2() {
153        accumulate_effects_v3(effects)
154    } else if protocol_config.simplified_unwrap_then_delete() {
155        accumulate_effects_v2(store, effects)
156    } else {
157        accumulate_effects_v1(store, effects, protocol_config)
158    }
159}
160
161fn accumulate_effects_v1<T, S>(
162    store: S,
163    effects: &[TransactionEffects],
164    protocol_config: &ProtocolConfig,
165) -> GlobalStateHash
166where
167    S: std::ops::Deref<Target = T>,
168    T: GlobalStateHashStore + ?Sized,
169{
170    let mut acc = GlobalStateHash::default();
171
172    // process insertions to the set
173    acc.insert_all(effects.iter().flat_map(|fx| {
174        fx.all_changed_objects()
175            .into_iter()
176            .map(|(oref, _, _)| oref.2)
177    }));
178
179    // insert wrapped tombstones. We use a custom struct in order to contain the tombstone
180    // against the object id and sequence number, as the tombstone by itself is not unique.
181    acc.insert_all(effects.iter().flat_map(|fx| {
182        fx.wrapped()
183            .into_iter()
184            .map(|oref| bcs::to_bytes(&WrappedObject::new(oref.0, oref.1)).unwrap())
185    }));
186
187    let all_unwrapped = effects
188        .iter()
189        .flat_map(|fx| {
190            fx.unwrapped()
191                .into_iter()
192                .map(|(oref, _owner)| (*fx.transaction_digest(), oref.0, oref.1))
193        })
194        .chain(effects.iter().flat_map(|fx| {
195            fx.unwrapped_then_deleted()
196                .into_iter()
197                .map(|oref| (*fx.transaction_digest(), oref.0, oref.1))
198        }))
199        .collect::<Vec<(TransactionDigest, ObjectID, SequenceNumber)>>();
200
201    let unwrapped_ids: HashMap<TransactionDigest, HashSet<ObjectID>> = all_unwrapped
202        .iter()
203        .map(|(digest, id, _)| (*digest, *id))
204        .into_group_map()
205        .iter()
206        .map(|(digest, ids)| (*digest, HashSet::from_iter(ids.iter().cloned())))
207        .collect();
208
209    // Collect keys from modified_at_versions to remove from the hasher.
210    // Filter all unwrapped objects (from unwrapped or unwrapped_then_deleted effects)
211    // as these were inserted into the hasher as a WrappedObject. Will handle these
212    // separately.
213    let modified_at_version_keys: Vec<ObjectKey> = effects
214        .iter()
215        .flat_map(|fx| {
216            fx.modified_at_versions()
217                .into_iter()
218                .map(|(id, seq_num)| (*fx.transaction_digest(), id, seq_num))
219        })
220        .filter_map(|(tx_digest, id, seq_num)| {
221            // unwrapped tx
222            if let Some(ids) = unwrapped_ids.get(&tx_digest) {
223                // object unwrapped in this tx. We handle it later
224                if ids.contains(&id) {
225                    return None;
226                }
227            }
228            Some(ObjectKey(id, seq_num))
229        })
230        .collect();
231
232    let modified_at_digests = store
233        .multi_get_objects_by_key(&modified_at_version_keys.clone())
234        .into_iter()
235        .zip_debug_eq(modified_at_version_keys)
236        .map(|(obj, key)| {
237            obj.unwrap_or_else(|| panic!("Object for key {:?} from modified_at_versions effects does not exist in objects table", key))
238                .compute_object_reference()
239                .2
240        });
241    acc.remove_all(modified_at_digests);
242
243    // Process unwrapped and unwrapped_then_deleted effects, which need to be
244    // removed as WrappedObject using the last sequence number it was tombstoned
245    // against. Since this happened in a past transaction, and the child object may
246    // have been modified since (and hence its sequence number incremented), we
247    // seek the version prior to the unwrapped version from the objects table directly.
248    // If the tombstone is not found, then assume this is a newly created wrapped object hence
249    // we don't expect to find it in the table.
250    let wrapped_objects_to_remove = all_unwrapped
251        .iter()
252        .filter_map(|(_tx_digest, id, seq_num)| {
253            let objref = store
254                .get_object_ref_prior_to_key_deprecated(id, *seq_num)
255                .expect("read cannot fail");
256
257            objref.map(|(id, version, digest)| {
258                assert!(
259                    !protocol_config.loaded_child_objects_fixed() || digest.is_wrapped(),
260                    "{:?}",
261                    id
262                );
263                WrappedObject::new(id, version)
264            })
265        });
266
267    acc.remove_all(wrapped_objects_to_remove.map(|wrapped| bcs::to_bytes(&wrapped).unwrap()));
268
269    acc
270}
271
272fn accumulate_effects_v2<T, S>(store: S, effects: &[TransactionEffects]) -> GlobalStateHash
273where
274    S: std::ops::Deref<Target = T>,
275    T: GlobalStateHashStore + ?Sized,
276{
277    let mut acc = GlobalStateHash::default();
278
279    // process insertions to the set
280    acc.insert_all(effects.iter().flat_map(|fx| {
281        fx.all_changed_objects()
282            .into_iter()
283            .map(|(oref, _, _)| oref.2)
284    }));
285
286    // Collect keys from modified_at_versions to remove from the hasher.
287    let modified_at_version_keys: Vec<_> = effects
288        .iter()
289        .flat_map(|fx| {
290            fx.modified_at_versions()
291                .into_iter()
292                .map(|(id, version)| ObjectKey(id, version))
293        })
294        .collect();
295
296    let modified_at_digests = store
297        .multi_get_objects_by_key(&modified_at_version_keys.clone())
298        .into_iter()
299        .zip_debug_eq(modified_at_version_keys)
300        .map(|(obj, key)| {
301            obj.unwrap_or_else(|| panic!("Object for key {:?} from modified_at_versions effects does not exist in objects table", key))
302                .compute_object_reference()
303                .2
304        });
305    acc.remove_all(modified_at_digests);
306
307    acc
308}
309
310fn accumulate_effects_v3(effects: &[TransactionEffects]) -> GlobalStateHash {
311    let mut acc = GlobalStateHash::default();
312    acc.insert_all(effects.iter().flat_map(|fx| {
313        fx.all_changed_objects()
314            .into_iter()
315            .map(|(object_ref, _, _)| object_ref.2)
316    }));
317    acc.remove_all(effects.iter().flat_map(|fx| {
318        fx.old_object_metadata()
319            .into_iter()
320            .map(|(object_ref, _owner)| object_ref.2)
321    }));
322    acc
323}
324
325impl GlobalStateHasher {
326    pub fn new(store: Arc<dyn GlobalStateHashStore>, metrics: Arc<GlobalStateHashMetrics>) -> Self {
327        Self { store, metrics }
328    }
329
330    pub fn new_for_tests(store: Arc<dyn GlobalStateHashStore>) -> Self {
331        Self::new(store, GlobalStateHashMetrics::new(&Registry::new()))
332    }
333
334    pub fn metrics(&self) -> Arc<GlobalStateHashMetrics> {
335        self.metrics.clone()
336    }
337
338    pub fn set_inconsistent_state(&self, is_inconsistent_state: bool) {
339        self.metrics
340            .inconsistent_state
341            .set(is_inconsistent_state as i64);
342    }
343
344    /// Accumulates the effects of a single checkpoint and persists the hasher.
345    pub fn accumulate_checkpoint(
346        &self,
347        effects: &[TransactionEffects],
348        checkpoint_seq_num: CheckpointSequenceNumber,
349        epoch_store: &AuthorityPerEpochStore,
350    ) -> GlobalStateHash {
351        let _scope = monitored_scope("AccumulateCheckpoint");
352        if let Some(acc) = epoch_store.get_state_hash_for_checkpoint(&checkpoint_seq_num) {
353            return acc;
354        }
355
356        let acc = self.accumulate_effects(effects, epoch_store.protocol_config());
357
358        epoch_store.insert_state_hash_for_checkpoint(&checkpoint_seq_num, &acc);
359        debug!("Accumulated checkpoint {}", checkpoint_seq_num);
360
361        epoch_store
362            .checkpoint_state_notify_read
363            .notify(&checkpoint_seq_num, &acc);
364
365        acc
366    }
367
368    pub fn accumulate_cached_live_object_set_for_testing(
369        &self,
370        include_wrapped_tombstone: bool,
371    ) -> GlobalStateHash {
372        Self::accumulate_live_object_set_impl(
373            self.store
374                .iter_cached_live_object_set_for_testing(include_wrapped_tombstone),
375        )
376    }
377
378    /// Returns the result of accumulating the live object set, without side effects
379    pub fn accumulate_live_object_set(&self, include_wrapped_tombstone: bool) -> GlobalStateHash {
380        Self::accumulate_live_object_set_impl(
381            self.store.iter_live_object_set(include_wrapped_tombstone),
382        )
383    }
384
385    fn accumulate_live_object_set_impl(iter: impl Iterator<Item = LiveObject>) -> GlobalStateHash {
386        let mut acc = GlobalStateHash::default();
387        iter.for_each(|live_object| {
388            Self::accumulate_live_object(&mut acc, &live_object);
389        });
390        acc
391    }
392
393    pub fn accumulate_live_object(acc: &mut GlobalStateHash, live_object: &LiveObject) {
394        match live_object {
395            LiveObject::Normal(object) => {
396                acc.insert(object.compute_object_reference().2);
397            }
398            LiveObject::Wrapped(key) => {
399                acc.insert(
400                    bcs::to_bytes(&WrappedObject::new(key.0, key.1))
401                        .expect("Failed to serialize WrappedObject"),
402                );
403            }
404        }
405    }
406
407    pub fn digest_live_object_set(
408        &self,
409        include_wrapped_tombstone: bool,
410    ) -> ECMHLiveObjectSetDigest {
411        let acc = self.accumulate_live_object_set(include_wrapped_tombstone);
412        acc.digest().into()
413    }
414
415    pub async fn digest_epoch(
416        &self,
417        epoch_store: Arc<AuthorityPerEpochStore>,
418        last_checkpoint_of_epoch: CheckpointSequenceNumber,
419    ) -> SuiResult<ECMHLiveObjectSetDigest> {
420        Ok(self
421            .accumulate_epoch(epoch_store, last_checkpoint_of_epoch)?
422            .digest()
423            .into())
424    }
425
426    pub async fn wait_for_previous_running_root(
427        &self,
428        epoch_store: &AuthorityPerEpochStore,
429        checkpoint_seq_num: CheckpointSequenceNumber,
430    ) -> SuiResult {
431        assert!(checkpoint_seq_num > 0);
432
433        // Check if this is the first checkpoint of the new epoch, in which case
434        // there is nothing to wait for. Look up the previous epoch explicitly: when
435        // this checkpoint is rebuilt after a restart, the root of the current epoch
436        // may already be stored.
437        if let Some(prev_epoch) = epoch_store.epoch().checked_sub(1)
438            && self
439                .store
440                .get_root_state_hash_for_epoch(prev_epoch)?
441                .map(|(last_checkpoint_prev_epoch, _)| last_checkpoint_prev_epoch)
442                == Some(checkpoint_seq_num - 1)
443        {
444            return Ok(());
445        }
446
447        // There is an edge case here where checkpoint_seq_num is 1. This means the previous
448        // checkpoint is the genesis checkpoint. CheckpointExecutor is guaranteed to execute
449        // and accumulate the genesis checkpoint, so this will resolve.
450        epoch_store
451            .notify_read_running_root(checkpoint_seq_num - 1)
452            .await?;
453        Ok(())
454    }
455
456    fn get_prior_root(
457        &self,
458        epoch_store: &AuthorityPerEpochStore,
459        checkpoint_seq_num: CheckpointSequenceNumber,
460    ) -> SuiResult<GlobalStateHash> {
461        if checkpoint_seq_num == 0 {
462            return Ok(GlobalStateHash::default());
463        }
464
465        if let Some(prior_running_root) =
466            epoch_store.get_running_root_state_hash(checkpoint_seq_num - 1)
467        {
468            return Ok(prior_running_root);
469        }
470
471        if let Some((last_checkpoint_prev_epoch, prev_acc)) = self
472            .store
473            .get_root_state_hash_for_epoch(epoch_store.epoch() - 1)?
474            && last_checkpoint_prev_epoch == checkpoint_seq_num - 1
475        {
476            return Ok(prev_acc);
477        }
478
479        fatal!(
480            "Running root state hasher must exist for checkpoint {}",
481            checkpoint_seq_num - 1
482        );
483    }
484
485    // Accumulate the running root.
486    // The previous checkpoint must be accumulated before calling this function, or it will panic.
487    pub fn accumulate_running_root(
488        &self,
489        epoch_store: &AuthorityPerEpochStore,
490        checkpoint_seq_num: CheckpointSequenceNumber,
491        checkpoint_acc: Option<GlobalStateHash>,
492    ) -> SuiResult {
493        let _scope = monitored_scope("AccumulateRunningRoot");
494        tracing::debug!(
495            "accumulating running root for checkpoint {}",
496            checkpoint_seq_num
497        );
498
499        // Idempotency.
500        if epoch_store
501            .get_running_root_state_hash(checkpoint_seq_num)
502            .is_some()
503        {
504            debug!(
505                "accumulate_running_root {:?} {:?} already exists",
506                epoch_store.epoch(),
507                checkpoint_seq_num
508            );
509            return Ok(());
510        }
511
512        let mut running_root = self.get_prior_root(epoch_store, checkpoint_seq_num)?;
513
514        let checkpoint_acc = checkpoint_acc.unwrap_or_else(|| {
515            epoch_store
516                .get_state_hash_for_checkpoint(&checkpoint_seq_num)
517                .expect("Expected checkpoint accumulator to exist")
518        });
519        running_root.union(&checkpoint_acc);
520        epoch_store.insert_running_root_state_hash(&checkpoint_seq_num, &running_root)?;
521        debug!(
522            "Accumulated checkpoint {} to running root accumulator",
523            checkpoint_seq_num,
524        );
525        Ok(())
526    }
527
528    pub fn accumulate_epoch(
529        &self,
530        epoch_store: Arc<AuthorityPerEpochStore>,
531        last_checkpoint_of_epoch: CheckpointSequenceNumber,
532    ) -> SuiResult<GlobalStateHash> {
533        let _scope = monitored_scope("AccumulateEpochV2");
534        let running_root = epoch_store
535            .get_running_root_state_hash(last_checkpoint_of_epoch)
536            .expect("Expected running root accumulator to exist up to last checkpoint of epoch");
537
538        self.store.insert_state_hash_for_epoch(
539            epoch_store.epoch(),
540            &last_checkpoint_of_epoch,
541            &running_root,
542        )?;
543        debug!(
544            "Finalized root state hash for epoch {} (up to checkpoint {})",
545            epoch_store.epoch(),
546            last_checkpoint_of_epoch
547        );
548        Ok(running_root.clone())
549    }
550
551    pub fn accumulate_effects(
552        &self,
553        effects: &[TransactionEffects],
554        protocol_config: &ProtocolConfig,
555    ) -> GlobalStateHash {
556        accumulate_effects(&*self.store, effects, protocol_config)
557    }
558}