sui_adapter_latest/data_store/
cached_package_store.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::data_store::PackageStore;
use indexmap::IndexMap;
use move_core_types::identifier::IdentStr;
use std::{
    cell::RefCell,
    collections::{BTreeMap, BTreeSet},
    rc::Rc,
};
use sui_types::{
    base_types::ObjectID,
    error::{ExecutionError, SuiResult},
    move_package::MovePackage,
    storage::BackingPackageStore,
};

/// A package store that caches packages in memory and indexes type origins. This is useful for
/// speeding up package loading and type resolution.
///
/// There is a `max_package_cache_size` that determines how many packages we will cache. If the cache is
/// full, we will drop the cache.
///
/// The `max_type_cache_size` determines how many types we will cache. If the cache is full, we
/// will drop the cache.
///
/// FUTURE: Most/all of this will be replaced by the VM runtime cache in the new VM. For now
/// though, we need to use this.
pub struct CachedPackageStore<'state> {
    /// Underlying store the fetch packages from
    pub package_store: Box<dyn BackingPackageStore + 'state>,
    /// A cache of packages that we've loaded so far. This is used to speed up package loading.
    /// Elements in this are safe to be evicted based on cache decisions.
    package_cache: RefCell<BTreeMap<ObjectID, Option<Rc<MovePackage>>>>,
    /// A cache of type origins that we've loaded so far. This is used to speed up type resolution.
    /// Elements in this are safe to be evicted based on cache decisions.
    type_origin_cache: RefCell<CachedTypeOriginMap>,
    /// Elements in this are packages that are being published or have been published in the current
    /// transaction.
    /// Elements in this are _not_ safe to evict, unless evicted through `pop_package`.
    new_packages: RefCell<IndexMap<ObjectID, Rc<MovePackage>>>,
    /// Maximum size (in number of packages) that the `package_cache` can grow to before it is
    /// cleared.
    max_package_cache_size: usize,
    /// Maximum size (in number of keys) that the `type_origin_cache` can grow to before it is
    /// cleared.,
    max_type_cache_size: usize,
}

type TypeOriginMap = BTreeMap<ObjectID, BTreeMap<(String, String), ObjectID>>;

#[derive(Debug)]
struct CachedTypeOriginMap {
    /// Tracker of all packages that we've loaded so far. This is used to determine if the
    /// `TypeOriginMap` needs to be updated when loading a package, or if that package has already
    /// contributed to the `TypeOriginMap`.
    pub cached_type_origins: BTreeSet<ObjectID>,
    /// A mapping of the (original package ID)::<module_name>::<type_name> to the defining ID for
    /// that type.
    pub type_origin_map: TypeOriginMap,
}

impl CachedTypeOriginMap {
    pub fn new() -> Self {
        Self {
            cached_type_origins: BTreeSet::new(),
            type_origin_map: TypeOriginMap::new(),
        }
    }
}

impl<'state> CachedPackageStore<'state> {
    pub const DEFAULT_MAX_PACKAGE_CACHE_SIZE: usize = 200;
    pub const DEFAULT_MAX_TYPE_ORIGIN_CACHE_SIZE: usize = 1000;

    pub fn new(package_store: Box<dyn BackingPackageStore + 'state>) -> Self {
        Self {
            package_store,
            package_cache: RefCell::new(BTreeMap::new()),
            type_origin_cache: RefCell::new(CachedTypeOriginMap::new()),
            new_packages: RefCell::new(IndexMap::new()),
            max_package_cache_size: Self::DEFAULT_MAX_PACKAGE_CACHE_SIZE,
            max_type_cache_size: Self::DEFAULT_MAX_TYPE_ORIGIN_CACHE_SIZE,
        }
    }

    /// Push a new package into the new packages. This is used to track packages that are being
    /// published or have been published.
    pub fn push_package(
        &self,
        id: ObjectID,
        package: Rc<MovePackage>,
    ) -> Result<(), ExecutionError> {
        // Check that the package ID is not already present anywhere.
        debug_assert!(self.fetch_package(&id).unwrap().is_none());

        // Insert the package into the new packages
        // If the package already exists, we will overwrite it and signal an error.
        if self.new_packages.borrow_mut().insert(id, package).is_some() {
            invariant_violation!(
                "Package with ID {} already exists in the new packages. This should never happen.",
                id
            );
        }

        Ok(())
    }

    /// Rollback a package that was pushed into the new packages. We keep the invariant that:
    /// * You can only pop the most recent package that was pushed.
    /// * The element being popped _must_ exist in the new packages.
    ///
    /// Otherwise this returns an invariant violation.
    pub fn pop_package(&self, id: ObjectID) -> Result<Rc<MovePackage>, ExecutionError> {
        if self
            .new_packages
            .borrow()
            .last()
            .is_none_or(|(pkg_id, _)| *pkg_id != id)
        {
            make_invariant_violation!(
                "Tried to pop package {} from new packages, but new packages was empty or \
                it is not the most recent package inserted. This should never happen.",
                id
            );
        }

        let Some((pkg_id, pkg)) = self.new_packages.borrow_mut().pop() else {
            unreachable!(
                "We just checked that new packages is not empty, so this should never happen."
            );
        };
        assert_eq!(
            pkg_id, id,
            "Popped package ID {} does not match requested ID {}. This should never happen as was checked above.",
            pkg_id, id
        );

        Ok(pkg)
    }

    pub fn take_new_packages(&self) -> IndexMap<ObjectID, Rc<MovePackage>> {
        // Take the new packages and clear the cache.
        let mut new_packages = self.new_packages.borrow_mut();
        std::mem::take(&mut *new_packages)
    }

    /// Get a package by its package ID (i.e., not original ID). This will first look in the new
    /// packages, then in the cache, and then finally try and fetch the pacakge from the underlying
    /// package store.
    ///
    /// Once the package is fetched it will be added to the type origin cache if it is not already
    /// present in the type origin cache.
    pub fn get_package(&self, object_id: &ObjectID) -> SuiResult<Option<Rc<MovePackage>>> {
        let Some(pkg) = self.fetch_package(object_id)? else {
            return Ok(None);
        };

        let package_id = pkg.id();

        // If the number of type origins that we have cached exceeds the max size, drop the cache.
        if self.type_origin_cache.borrow().cached_type_origins.len() >= self.max_type_cache_size {
            *self.type_origin_cache.borrow_mut() = CachedTypeOriginMap::new();
        }

        if !self
            .type_origin_cache
            .borrow()
            .cached_type_origins
            .contains(&package_id)
        {
            let cached_type_origin_map = &mut self.type_origin_cache.borrow_mut();
            cached_type_origin_map
                .cached_type_origins
                .insert(package_id);
            let original_package_id = pkg.original_package_id();
            let package_types = cached_type_origin_map
                .type_origin_map
                .entry(original_package_id)
                .or_default();
            for ((module_name, type_name), defining_id) in pkg.type_origin_map().into_iter() {
                if let Some(other) = package_types.insert(
                    (module_name.to_string(), type_name.to_string()),
                    defining_id,
                ) {
                    assert_eq!(
                        other, defining_id,
                        "type origin map should never have conflicting entries"
                    );
                }
            }
        }
        Ok(Some(pkg))
    }

    /// Get a package by its package ID (i.e., not original ID). This will first look in the new
    /// packages, then in the cache, and then finally try and fetch the pacakge from the underlying
    /// package store. NB: this does not do any indexing of the package.
    fn fetch_package(&self, id: &ObjectID) -> SuiResult<Option<Rc<MovePackage>>> {
        // Look for package in new packages
        if let Some(pkg) = self.new_packages.borrow().get(id).cloned() {
            return Ok(Some(pkg));
        }

        // Look for package in cache
        if let Some(pkg) = self.package_cache.borrow().get(id).cloned() {
            return Ok(pkg);
        }

        if self.package_cache.borrow().len() >= self.max_package_cache_size {
            self.package_cache.borrow_mut().clear();
        }

        let pkg = self
            .package_store
            .get_package_object(id)?
            .map(|pkg_obj| Rc::new(pkg_obj.move_package().clone()));
        self.package_cache.borrow_mut().insert(*id, pkg.clone());
        Ok(pkg)
    }
}

impl PackageStore for CachedPackageStore<'_> {
    fn get_package(&self, id: &ObjectID) -> SuiResult<Option<Rc<MovePackage>>> {
        self.get_package(id)
    }

    fn resolve_type_to_defining_id(
        &self,
        module_address: ObjectID,
        module_name: &IdentStr,
        type_name: &IdentStr,
    ) -> SuiResult<Option<ObjectID>> {
        let Some(pkg) = self.get_package(&module_address)? else {
            return Ok(None);
        };

        Ok(self
            .type_origin_cache
            .borrow()
            .type_origin_map
            .get(&pkg.original_package_id())
            .and_then(|module_map| {
                module_map
                    .get(&(module_name.to_string(), type_name.to_string()))
                    .copied()
            }))
    }
}