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