soroban_env_host/host/
lifecycle.rs

1use crate::{
2    crypto, err,
3    host::{
4        metered_clone::{MeteredAlloc, MeteredClone},
5        metered_write_xdr, ContractReentryMode,
6    },
7    vm::Vm,
8    xdr::{
9        Asset, ContractCodeEntry, ContractDataDurability, ContractExecutable, ContractIdPreimage,
10        ContractIdPreimageFromAddress, CreateContractArgsV2, ExtensionPoint, Hash, LedgerKey,
11        LedgerKeyContractCode, ScAddress, ScErrorCode, ScErrorType,
12    },
13    AddressObject, BytesObject, Host, HostError, Symbol, TryFromVal, TryIntoVal, Val,
14};
15use std::rc::Rc;
16
17const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor";
18const CONSTRUCTOR_SUPPORT_PROTOCOL: u32 = 22;
19
20impl Host {
21    // Notes on metering: this is covered by the called components.
22    fn create_contract_with_id(
23        &self,
24        contract_id: Hash,
25        contract_executable: ContractExecutable,
26    ) -> Result<(), HostError> {
27        let storage_key = self.contract_instance_ledger_key(&contract_id)?;
28        if self
29            .try_borrow_storage_mut()?
30            .has_with_host(&storage_key, self, None)?
31        {
32            return Err(self.err(
33                ScErrorType::Storage,
34                ScErrorCode::ExistingValue,
35                "contract already exists",
36                &[self
37                    .add_host_object(self.scbytes_from_hash(&contract_id)?)?
38                    .into()],
39            ));
40        }
41        // Make sure the contract code exists. Without this check it would be
42        // possible to accidentally create a contract that never may be invoked
43        // (just by providing a bad hash).
44        if let ContractExecutable::Wasm(wasm_hash) = &contract_executable {
45            if !self.wasm_exists(wasm_hash)? {
46                return Err(err!(
47                    self,
48                    (ScErrorType::Storage, ScErrorCode::MissingValue),
49                    "Wasm does not exist",
50                    *wasm_hash
51                ));
52            }
53        }
54        self.store_contract_instance(Some(contract_executable), None, contract_id, &storage_key)?;
55        Ok(())
56    }
57
58    fn call_constructor(
59        &self,
60        contract_id: &Hash,
61        constructor_args: Vec<Val>,
62    ) -> Result<(), HostError> {
63        // Wasms built for the protocol versions before constructor support
64        // are always treated as having a default no-op constructor with 0
65        // arguments.
66        let contract_protocol = self.get_contract_protocol_version(&contract_id)?;
67        if contract_protocol < CONSTRUCTOR_SUPPORT_PROTOCOL {
68            if constructor_args.is_empty() {
69                return Ok(());
70            }
71            return Err(self.err(
72                ScErrorType::Context,
73                ScErrorCode::InvalidAction,
74                "trying to call non-default constructor on a contract that doesn't support constructors (built prior to protocol 22)",
75                &[],
76            ));
77        }
78        let res = self
79            .call_n_internal(
80                contract_id,
81                CONSTRUCTOR_FUNCTION_NAME.try_into_val(self)?,
82                constructor_args.as_slice(),
83                CallParams {
84                    reentry_mode: ContractReentryMode::Prohibited,
85                    internal_host_call: true,
86                    // Allow 0-argument constructors to be missing, but don't allow passing any arguments
87                    // into a contract without constructor.
88                    treat_missing_function_as_noop: constructor_args.is_empty(),
89                },
90            )
91            .map_err(|err| {
92                // Convert any recoverable error to 'generic' host error
93                // in order to not accidentally leak the constructor errors
94                // to the upstream contracts.
95                if err.is_recoverable() {
96                    // Also log the original error for diagnostics.
97                    self.err(
98                        ScErrorType::Context,
99                        ScErrorCode::InvalidAction,
100                        "constructor invocation has failed with error",
101                        &[err.error.to_val()],
102                    )
103                } else {
104                    err
105                }
106            })?;
107        if !res.is_void() {
108            return Err(self.err(
109                ScErrorType::Value,
110                ScErrorCode::UnexpectedType,
111                "constructor returned non-void value",
112                &[res],
113            ));
114        }
115        Ok(())
116    }
117
118    fn maybe_initialize_stellar_asset_contract(
119        &self,
120        contract_id: &Hash,
121        id_preimage: &ContractIdPreimage,
122    ) -> Result<(), HostError> {
123        if let ContractIdPreimage::Asset(asset) = id_preimage {
124            let mut asset_bytes: Vec<u8> = Default::default();
125            metered_write_xdr(self.budget_ref(), asset, &mut asset_bytes)?;
126            self.call_n_internal(
127                contract_id,
128                Symbol::try_from_val(self, &"init_asset")?,
129                &[self
130                    .add_host_object(self.scbytes_from_vec(asset_bytes)?)?
131                    .into()],
132                CallParams::default_external_call(),
133            )?;
134            Ok(())
135        } else {
136            Ok(())
137        }
138    }
139
140    pub(crate) fn create_contract_internal(
141        &self,
142        deployer: Option<AddressObject>,
143        args: CreateContractArgsV2,
144        constructor_args: Vec<Val>,
145    ) -> Result<AddressObject, HostError> {
146        let has_deployer = deployer.is_some();
147        if has_deployer {
148            self.try_borrow_authorization_manager()?
149                .push_create_contract_host_fn_frame(self, args.metered_clone(self)?)?;
150        }
151        // Make sure that even in case of operation failure we still pop the
152        // stack frame.
153        // This is hacky, but currently this is the only instance where we need
154        // to manually manage auth manager frames (we don't need to authorize
155        // any other host fns and it doesn't seem useful to create extra frames
156        // for them just to make auth work in a single case).
157        let res = self.create_contract_with_optional_auth(deployer, args, constructor_args);
158        if has_deployer {
159            self.try_borrow_authorization_manager()?
160                .pop_frame(self, None)?;
161        }
162        res
163    }
164
165    fn create_contract_with_optional_auth(
166        &self,
167        deployer: Option<AddressObject>,
168        args: CreateContractArgsV2,
169        constructor_args: Vec<Val>,
170    ) -> Result<AddressObject, HostError> {
171        if let Some(deployer_address) = deployer {
172            self.try_borrow_authorization_manager()?.require_auth(
173                self,
174                deployer_address,
175                Default::default(),
176            )?;
177        }
178
179        let id_preimage =
180            self.get_full_contract_id_preimage(args.contract_id_preimage.metered_clone(self)?)?;
181        let contract_id = Hash(self.metered_hash_xdr(&id_preimage)?);
182        self.create_contract_with_id(contract_id.metered_clone(self)?, args.executable.clone())?;
183        self.maybe_initialize_stellar_asset_contract(&contract_id, &args.contract_id_preimage)?;
184        if matches!(args.executable, ContractExecutable::Wasm(_)) {
185            self.call_constructor(&contract_id, constructor_args)?;
186        }
187        self.add_host_object(ScAddress::Contract(contract_id))
188    }
189
190    pub(crate) fn get_contract_id_hash(
191        &self,
192        deployer: AddressObject,
193        salt: BytesObject,
194    ) -> Result<Hash, HostError> {
195        let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
196            address: self.visit_obj(deployer, |addr: &ScAddress| addr.metered_clone(self))?,
197            salt: self.u256_from_bytesobj_input("contract_id_salt", salt)?,
198        });
199
200        let id_preimage =
201            self.get_full_contract_id_preimage(contract_id_preimage.metered_clone(self)?)?;
202        Ok(Hash(self.metered_hash_xdr(&id_preimage)?))
203    }
204
205    pub(crate) fn get_asset_contract_id_hash(&self, asset: Asset) -> Result<Hash, HostError> {
206        let id_preimage = self.get_full_contract_id_preimage(ContractIdPreimage::Asset(asset))?;
207        let id_arr: [u8; 32] = self.metered_hash_xdr(&id_preimage)?;
208        Ok(Hash(id_arr))
209    }
210
211    pub(crate) fn upload_contract_wasm(&self, wasm: Vec<u8>) -> Result<BytesObject, HostError> {
212        let hash_bytes: [u8; 32] = crypto::sha256_hash_from_bytes(wasm.as_slice(), self)?
213            .try_into()
214            .map_err(|_| {
215                self.err(
216                    ScErrorType::Value,
217                    ScErrorCode::InternalError,
218                    "unexpected hash length",
219                    &[],
220                )
221            })?;
222
223        // Check size before instantiation.
224        let wasm_bytes_m: crate::xdr::BytesM = wasm.try_into().map_err(|_| {
225            self.err(
226                ScErrorType::Value,
227                ScErrorCode::ExceededLimit,
228                "Wasm code is too large",
229                &[],
230            )
231        })?;
232
233        let mut ext = crate::xdr::ContractCodeEntryExt::V0;
234
235        // Instantiate a temporary / throwaway VM using this wasm. This will do
236        // both quick checks like "does this wasm have the right protocol number
237        // to run on this network" and also a full parse-and-link pass to check
238        // that the wasm is basically not garbage. It might still fail to run
239        // but it will at least instantiate. This might seem a bit heavyweight
240        // but really "instantiating a VM" is mostly just "parsing the module
241        // and doing those checks" anyway. Revisit in the future if you want to
242        // try to split these costs up some.
243        if cfg!(any(test, feature = "testutils")) && wasm_bytes_m.as_slice().is_empty() {
244            // Allow a zero-byte contract when testing, as this is used to make
245            // native test contracts behave like wasm. They will never be
246            // instantiated, this is just to exercise their storage logic.
247        } else {
248            let _check_vm = Vm::new(
249                self,
250                Hash(hash_bytes.metered_clone(self)?),
251                wasm_bytes_m.as_slice(),
252            )?;
253            // At this point we do a secondary parse on what we've checked to be a valid
254            // module in order to extract a refined cost model, which we'll store in the
255            // code entry's ext field, for future parsing and instantiations.
256            _check_vm.module.cost_inputs.charge_for_parsing(self)?;
257            ext = crate::xdr::ContractCodeEntryExt::V1(crate::xdr::ContractCodeEntryV1 {
258                ext: ExtensionPoint::V0,
259                cost_inputs: crate::vm::ParsedModule::extract_refined_contract_cost_inputs(
260                    self,
261                    wasm_bytes_m.as_slice(),
262                )?,
263            });
264        }
265
266        let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?;
267        let code_key = Rc::metered_new(
268            LedgerKey::ContractCode(LedgerKeyContractCode {
269                hash: Hash(hash_bytes.metered_clone(self)?),
270            }),
271            self,
272        )?;
273
274        let mut storage = self.try_borrow_storage_mut()?;
275
276        // We will definitely put the contract in the ledger if it isn't there yet.
277        #[allow(unused_mut)]
278        let mut should_put_contract = !storage.has_with_host(&code_key, self, None)?;
279
280        // We may also, in the cache-supporting protocol, overwrite the contract if its ext field changed.
281        if !should_put_contract {
282            let entry = storage.get_with_host(&code_key, self, None)?;
283            if let crate::xdr::LedgerEntryData::ContractCode(ContractCodeEntry {
284                ext: old_ext,
285                ..
286            }) = &entry.data
287            {
288                should_put_contract = *old_ext != ext;
289            }
290        }
291
292        if should_put_contract {
293            let data = ContractCodeEntry {
294                hash: Hash(hash_bytes),
295                ext,
296                code: wasm_bytes_m,
297            };
298            storage.put_with_host(
299                &code_key,
300                &Host::new_contract_code(self, data)?,
301                Some(self.get_min_live_until_ledger(ContractDataDurability::Persistent)?),
302                self,
303                None,
304            )?;
305        }
306        Ok(hash_obj)
307    }
308}
309
310use super::frame::CallParams;
311#[cfg(any(test, feature = "testutils"))]
312use super::ContractFunctionSet;
313
314// "testutils" is not covered by budget metering.
315#[cfg(any(test, feature = "testutils"))]
316impl Host {
317    pub fn register_test_contract(
318        &self,
319        contract_address: AddressObject,
320        contract_fns: Rc<dyn ContractFunctionSet>,
321    ) -> Result<(), HostError> {
322        #[cfg(any(test, feature = "testutils"))]
323        let _invocation_meter_scope = self.maybe_meter_invocation()?;
324
325        use crate::Env;
326        self.register_test_contract_with_constructor(
327            contract_address,
328            contract_fns,
329            self.vec_new()?,
330        )
331    }
332
333    pub fn register_test_contract_with_constructor(
334        &self,
335        contract_address: AddressObject,
336        contract_fns: Rc<dyn ContractFunctionSet>,
337        constructor_args: crate::VecObject,
338    ) -> Result<(), HostError> {
339        #[cfg(any(test, feature = "testutils"))]
340        let _invocation_meter_scope = self.maybe_meter_invocation()?;
341
342        let contract_id = self.contract_id_from_address(contract_address)?;
343        let instance_key = self.contract_instance_ledger_key(&contract_id)?;
344        let wasm_hash_obj = self.upload_contract_wasm(vec![])?;
345        let wasm_hash = self.hash_from_bytesobj_input("wasm_hash", wasm_hash_obj)?;
346        // Use the empty Wasm as an executable to a) mark that the contract
347        // calls should be dispatched via provided `contract_fns` and b) have
348        // the same ledger entries as for 'real' contracts that consist of Wasm
349        // entry and the instance entry, so that instance-related host functions
350        // work properly.
351        self.store_contract_instance(
352            Some(ContractExecutable::Wasm(wasm_hash)),
353            None,
354            contract_id.clone(),
355            &instance_key,
356        )?;
357        self.try_borrow_contracts_mut()?
358            .insert(contract_id.clone(), contract_fns);
359
360        self.call_constructor(&contract_id, self.call_args_from_obj(constructor_args)?)
361    }
362
363    // This is a test utility that allows calling constructor on a contract that
364    // already exists in the storage. It is incorrect to call this
365    // on a properly instantiated contract (as it must have already had the
366    // constructor executed), but is useful to support manually instantiated
367    // contracts, i.e. those that were created by writing directly into storage.
368    pub fn call_constructor_for_stored_contract_unsafe(
369        &self,
370        contract_id: &Hash,
371        constructor_args: crate::VecObject,
372    ) -> Result<(), HostError> {
373        self.call_constructor(&contract_id, self.call_args_from_obj(constructor_args)?)
374    }
375}