soroban_env_host/vm/
module_cache.rs

1use super::{
2    func_info::HOST_FUNCTIONS,
3    parsed_module::{ParsedModule, VersionedContractCodeCostInputs},
4};
5use crate::{
6    budget::{get_wasmi_config, AsBudget},
7    host::metered_clone::{MeteredClone, MeteredContainer},
8    xdr::{Hash, ScErrorCode, ScErrorType},
9    Host, HostError, MeteredOrdMap,
10};
11use std::{collections::BTreeSet, rc::Rc};
12use wasmi::Engine;
13
14/// A [ModuleCache] is a cache of a set of Wasm modules that have been parsed
15/// but not yet instantiated, along with a shared and reusable [Engine] storing
16/// their code. The cache must be populated eagerly with all the contracts in a
17/// single [Host]'s lifecycle (at least) added all at once, since each wasmi
18/// [Engine] is locked during execution and no new modules can be added to it.
19#[derive(Clone, Default)]
20pub struct ModuleCache {
21    pub(crate) engine: Engine,
22    modules: MeteredOrdMap<Hash, Rc<ParsedModule>, Host>,
23}
24
25impl ModuleCache {
26    pub fn new(host: &Host) -> Result<Self, HostError> {
27        let config = get_wasmi_config(host.as_budget())?;
28        let engine = Engine::new(&config);
29        let modules = MeteredOrdMap::new();
30        let mut cache = Self { engine, modules };
31        cache.add_stored_contracts(host)?;
32        Ok(cache)
33    }
34
35    pub fn add_stored_contracts(&mut self, host: &Host) -> Result<(), HostError> {
36        use crate::xdr::{ContractCodeEntry, ContractCodeEntryExt, LedgerEntryData, LedgerKey};
37        let storage = host.try_borrow_storage()?;
38        for (k, v) in storage.map.iter(host.as_budget())? {
39            // In recording mode we build the module cache *after* the contract invocation has
40            // finished. This means that if any new Wasm has been uploaded, then we will add it to
41            // the cache. However, in the 'real' flow we build the cache first, so any new Wasm
42            // upload won't be cached. That's why we should look at the storage in its initial
43            // state, which is conveniently provided by the recording mode snapshot.
44            #[cfg(any(test, feature = "recording_mode"))]
45            let init_value = if host.in_storage_recording_mode()? {
46                storage.get_snapshot_value(host, k)?
47            } else {
48                v.clone()
49            };
50            #[cfg(any(test, feature = "recording_mode"))]
51            let v = &init_value;
52
53            if let LedgerKey::ContractCode(_) = &**k {
54                if let Some((e, _)) = v {
55                    if let LedgerEntryData::ContractCode(ContractCodeEntry { code, hash, ext }) =
56                        &e.data
57                    {
58                        // We allow empty contracts in testing mode; they exist
59                        // to exercise as much of the contract-code-storage
60                        // infrastructure as possible, while still redirecting
61                        // the actual execution into a `ContractFunctionSet`.
62                        // They should never be called, so we do not have to go
63                        // as far as making a fake `ParsedModule` for them.
64                        if cfg!(any(test, feature = "testutils")) && code.as_slice().is_empty() {
65                            continue;
66                        }
67
68                        let code_cost_inputs = match ext {
69                            ContractCodeEntryExt::V0 => VersionedContractCodeCostInputs::V0 {
70                                wasm_bytes: code.len(),
71                            },
72                            ContractCodeEntryExt::V1(v1) => VersionedContractCodeCostInputs::V1(
73                                v1.cost_inputs.metered_clone(host.as_budget())?,
74                            ),
75                        };
76                        self.parse_and_cache_module(host, hash, code, code_cost_inputs)?;
77                    }
78                }
79            }
80        }
81        Ok(())
82    }
83
84    pub fn parse_and_cache_module(
85        &mut self,
86        host: &Host,
87        contract_id: &Hash,
88        wasm: &[u8],
89        cost_inputs: VersionedContractCodeCostInputs,
90    ) -> Result<(), HostError> {
91        if self.modules.contains_key(contract_id, host)? {
92            return Err(host.err(
93                ScErrorType::Context,
94                ScErrorCode::InternalError,
95                "module cache already contains contract",
96                &[],
97            ));
98        }
99        let parsed_module = ParsedModule::new(host, &self.engine, &wasm, cost_inputs)?;
100        self.modules =
101            self.modules
102                .insert(contract_id.metered_clone(host)?, parsed_module, host)?;
103        Ok(())
104    }
105
106    pub fn with_import_symbols<T>(
107        &self,
108        host: &Host,
109        callback: impl FnOnce(&BTreeSet<(&str, &str)>) -> Result<T, HostError>,
110    ) -> Result<T, HostError> {
111        let mut import_symbols = BTreeSet::new();
112        for module in self.modules.values(host)? {
113            module.with_import_symbols(host, |module_symbols| {
114                for hf in HOST_FUNCTIONS {
115                    let sym = (hf.mod_str, hf.fn_str);
116                    if module_symbols.contains(&sym) {
117                        import_symbols.insert(sym);
118                    }
119                }
120                Ok(())
121            })?;
122        }
123        // We approximate the cost of `BTreeSet` with the cost of initializng a
124        // `Vec` with the same elements, and we are doing it after the set has
125        // been created. The element count has been limited/charged during the
126        // parsing phase, so there is no DOS factor. We don't charge for
127        // insertion/lookups, since they should be cheap and number of
128        // operations on the set is limited (only used during `Linker`
129        // creation).
130        Vec::<(&str, &str)>::charge_bulk_init_cpy(import_symbols.len() as u64, host)?;
131        callback(&import_symbols)
132    }
133
134    pub fn make_linker(&self, host: &Host) -> Result<wasmi::Linker<Host>, HostError> {
135        self.with_import_symbols(host, |symbols| Host::make_linker(&self.engine, symbols))
136    }
137
138    pub fn get_module(
139        &self,
140        host: &Host,
141        wasm_hash: &Hash,
142    ) -> Result<Option<Rc<ParsedModule>>, HostError> {
143        if let Some(m) = self.modules.get(wasm_hash, host)? {
144            Ok(Some(m.clone()))
145        } else {
146            Ok(None)
147        }
148    }
149}