soroban_env_host/vm/
module_cache.rs

1use super::parsed_module::{CompilationContext, ParsedModule, VersionedContractCodeCostInputs};
2#[cfg(any(test, feature = "testutils"))]
3use crate::budget::AsBudget;
4use crate::{
5    budget::get_wasmi_config,
6    host::metered_clone::MeteredClone,
7    xdr::{Hash, ScErrorCode, ScErrorType},
8    Host, HostError,
9};
10use std::{
11    collections::BTreeMap,
12    sync::{Arc, Mutex, MutexGuard},
13};
14
15/// A [ModuleCache] is a cache of a set of Wasm modules that have been parsed
16/// but not yet instantiated, along with a shared and reusable [Engine] storing
17/// their code. The cache must be populated eagerly with all the contracts in a
18/// single [Host]'s lifecycle (at least) added all at once, since each wasmi
19/// [Engine] is locked during execution and no new modules can be added to it.
20#[derive(Clone, Default)]
21pub struct ModuleCache {
22    pub(crate) wasmi_engine: wasmi::Engine,
23    pub(crate) wasmi_linker: wasmi::Linker<Host>,
24    modules: ModuleCacheMap,
25}
26
27// We may use the ModuleCache from multiple C++ threads where
28// there's no checking of Send+Sync but we can at least ensure
29// Rust thinks its API is thread-safe.
30static_assertions::assert_impl_all!(ModuleCache: Send, Sync);
31
32// The module cache was originally designed as an immutable object
33// established at host creation time and never updated. In order to support
34// longer-lived modules caches it was changed to a new form that is
35// a little unlike the rest of soroban, and works differently:
36//
37// - Modules can be added post-construction, it's not immutable.
38// - Adding an existing module is a harmless no-op, not an error.
39// - The linkers are set to "maximal" mode to cover all possible imports.
40// - The cache easily scales to a large number of modules, unlike MeteredOrdMap.
41// - There is no metering of cache map operations.
42// - The cache can be cloned, but the clone is a shallow copy.
43// - The cache is mutable and shared among all copies, using a mutex.
44
45#[derive(Clone)]
46struct ModuleCacheMap(Arc<Mutex<BTreeMap<Hash, Arc<ParsedModule>>>>);
47
48impl Default for ModuleCacheMap {
49    fn default() -> Self {
50        Self(Arc::new(Mutex::new(BTreeMap::new())))
51    }
52}
53
54impl ModuleCacheMap {
55    fn lock_map(
56        map: &Arc<Mutex<BTreeMap<Hash, Arc<ParsedModule>>>>,
57    ) -> Result<MutexGuard<'_, BTreeMap<Hash, Arc<ParsedModule>>>, HostError> {
58        map.lock()
59            .map_err(|_| HostError::from((ScErrorType::Context, ScErrorCode::InternalError)))
60    }
61
62    fn contains_key(&self, key: &Hash) -> Result<bool, HostError> {
63        Ok(Self::lock_map(&self.0)?.contains_key(key))
64    }
65
66    fn get(&self, key: &Hash) -> Result<Option<Arc<ParsedModule>>, HostError> {
67        Ok(Self::lock_map(&self.0)?.get(key).map(|rc| rc.clone()))
68    }
69
70    fn insert(&self, key: Hash, value: Arc<ParsedModule>) -> Result<(), HostError> {
71        Self::lock_map(&self.0)?.insert(key, value);
72        Ok(())
73    }
74
75    fn clear(&self) -> Result<(), HostError> {
76        Self::lock_map(&self.0)?.clear();
77        Ok(())
78    }
79
80    fn remove(&self, key: &Hash) -> Result<Option<Arc<ParsedModule>>, HostError> {
81        Ok(Self::lock_map(&self.0)?.remove(key))
82    }
83}
84
85impl ModuleCache {
86    pub fn new<Ctx: CompilationContext>(context: &Ctx) -> Result<Self, HostError> {
87        let wasmi_config = get_wasmi_config(context.as_budget())?;
88        let wasmi_engine = wasmi::Engine::new(&wasmi_config);
89        let modules = ModuleCacheMap::default();
90        let wasmi_linker = Host::make_maximal_wasmi_linker(context, &wasmi_engine)?;
91        Ok(Self {
92            wasmi_engine,
93            modules,
94            wasmi_linker,
95        })
96    }
97
98    #[cfg(any(test, feature = "testutils"))]
99    pub fn add_stored_contracts(&self, host: &Host) -> Result<(), HostError> {
100        use crate::xdr::{ContractCodeEntry, ContractCodeEntryExt, LedgerEntryData, LedgerKey};
101        let storage = host.try_borrow_storage()?;
102        for (k, v) in storage.map.iter(host.as_budget())? {
103            if let LedgerKey::ContractCode(_) = &**k {
104                if let Some((e, _)) = v {
105                    if let LedgerEntryData::ContractCode(ContractCodeEntry { code, hash, ext }) =
106                        &e.data
107                    {
108                        // We allow empty contracts in testing mode; they exist
109                        // to exercise as much of the contract-code-storage
110                        // infrastructure as possible, while still redirecting
111                        // the actual execution into a `ContractFunctionSet`.
112                        // They should never be called, so we do not have to go
113                        // as far as making a fake `ParsedModule` for them.
114                        if code.as_slice().is_empty() {
115                            continue;
116                        }
117
118                        let code_cost_inputs = match ext {
119                            ContractCodeEntryExt::V0 => VersionedContractCodeCostInputs::V0 {
120                                wasm_bytes: code.len(),
121                            },
122                            ContractCodeEntryExt::V1(v1) => VersionedContractCodeCostInputs::V1(
123                                v1.cost_inputs.metered_clone(host.as_budget())?,
124                            ),
125                        };
126                        self.parse_and_cache_module(
127                            host,
128                            host.get_ledger_protocol_version()?,
129                            hash,
130                            code,
131                            code_cost_inputs,
132                        )?;
133                    }
134                }
135            }
136        }
137        Ok(())
138    }
139
140    pub fn parse_and_cache_module_simple<Ctx: CompilationContext>(
141        &self,
142        context: &Ctx,
143        curr_ledger_protocol: u32,
144        wasm: &[u8],
145    ) -> Result<(), HostError> {
146        let contract_id = Hash(crate::crypto::sha256_hash_from_bytes_raw(
147            wasm,
148            context.as_budget(),
149        )?);
150        self.parse_and_cache_module(
151            context,
152            curr_ledger_protocol,
153            &contract_id,
154            wasm,
155            VersionedContractCodeCostInputs::V0 {
156                wasm_bytes: wasm.len(),
157            },
158        )
159    }
160
161    pub fn parse_and_cache_module<Ctx: CompilationContext>(
162        &self,
163        context: &Ctx,
164        curr_ledger_protocol: u32,
165        contract_id: &Hash,
166        wasm: &[u8],
167        cost_inputs: VersionedContractCodeCostInputs,
168    ) -> Result<(), HostError> {
169        if self.modules.contains_key(contract_id)? {
170            return Ok(());
171        }
172        let parsed_module = ParsedModule::new(
173            context,
174            curr_ledger_protocol,
175            &self.wasmi_engine,
176            &wasm,
177            cost_inputs,
178        )?;
179        self.modules.insert(
180            contract_id.metered_clone(context.as_budget())?,
181            parsed_module,
182        )?;
183        Ok(())
184    }
185
186    pub fn contains_module(&self, wasm_hash: &Hash) -> Result<bool, HostError> {
187        self.modules.contains_key(wasm_hash)
188    }
189
190    pub fn get_module(&self, wasm_hash: &Hash) -> Result<Option<Arc<ParsedModule>>, HostError> {
191        if let Some(m) = self.modules.get(wasm_hash)? {
192            Ok(Some(m.clone()))
193        } else {
194            Ok(None)
195        }
196    }
197
198    pub fn remove_module(&self, wasm_hash: &Hash) -> Result<Option<Arc<ParsedModule>>, HostError> {
199        self.modules.remove(wasm_hash)
200    }
201
202    pub fn clear(&self) -> Result<(), HostError> {
203        self.modules.clear()
204    }
205}