soroban_env_host_zephyr/host/frame.rs
1use crate::{
2 auth::AuthorizationManagerSnapshot,
3 budget::AsBudget,
4 err,
5 host::{
6 metered_clone::{MeteredClone, MeteredContainer, MeteredIterator},
7 prng::Prng,
8 },
9 storage::{InstanceStorageMap, StorageMap},
10 xdr::{
11 ContractExecutable, ContractIdPreimage, Hash, HostFunction, HostFunctionType, ScAddress,
12 ScContractInstance, ScErrorCode, ScErrorType, ScVal,
13 },
14 AddressObject, Error, Host, HostError, Object, Symbol, SymbolStr, TryFromVal, TryIntoVal, Val,
15 Vm, DEFAULT_HOST_DEPTH_LIMIT,
16};
17
18#[cfg(any(test, feature = "testutils"))]
19use core::cell::RefCell;
20use std::rc::Rc;
21
22/// Determines the re-entry mode for calling a contract.
23pub(crate) enum ContractReentryMode {
24 /// Re-entry is completely prohibited.
25 Prohibited,
26 /// Re-entry is allowed, but only directly into the same contract (i.e. it's
27 /// possible for a contract to do a self-call via host).
28 SelfAllowed,
29 /// Re-entry is fully allowed.
30 #[allow(dead_code)]
31 Allowed,
32}
33
34/// All the contract functions starting with double underscore are considered
35/// to be reserved by the Soroban host and can't be directly called by another
36/// contracts.
37const RESERVED_CONTRACT_FN_PREFIX: &str = "__";
38
39/// Saves host state (storage and objects) for rolling back a (sub-)transaction
40/// on error. A helper type used by [`FrameGuard`].
41// Notes on metering: `RollbackPoint` are metered under Frame operations
42// #[derive(Clone)]
43pub(super) struct RollbackPoint {
44 storage: StorageMap,
45 events: usize,
46 auth: AuthorizationManagerSnapshot,
47}
48
49#[cfg(any(test, feature = "testutils"))]
50pub trait ContractFunctionSet {
51 fn call(&self, func: &Symbol, host: &Host, args: &[Val]) -> Option<Val>;
52}
53
54#[cfg(any(test, feature = "testutils"))]
55#[derive(Debug, Clone)]
56pub(crate) struct TestContractFrame {
57 pub(crate) id: Hash,
58 pub(crate) func: Symbol,
59 pub(crate) args: Vec<Val>,
60 pub(crate) panic: Rc<RefCell<Option<Error>>>,
61 pub(crate) instance: ScContractInstance,
62}
63
64#[cfg(any(test, feature = "testutils"))]
65impl std::hash::Hash for TestContractFrame {
66 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
67 self.id.hash(state);
68 self.func.hash(state);
69 self.args.hash(state);
70 if let Some(panic) = self.panic.borrow().as_ref() {
71 panic.hash(state);
72 }
73 self.instance.hash(state);
74 }
75}
76
77#[cfg(any(test, feature = "testutils"))]
78impl TestContractFrame {
79 pub fn new(id: Hash, func: Symbol, args: Vec<Val>, instance: ScContractInstance) -> Self {
80 Self {
81 id,
82 func,
83 args,
84 panic: Rc::new(RefCell::new(None)),
85 instance,
86 }
87 }
88}
89
90/// Context pairs a variable-case [`Frame`] enum with state that's common to all
91/// cases (eg. a [`Prng`]).
92#[derive(Clone, Hash)]
93pub(crate) struct Context {
94 pub(crate) frame: Frame,
95 pub(crate) prng: Option<Prng>,
96 pub(crate) storage: Option<InstanceStorageMap>,
97}
98
99/// Holds contextual information about a single invocation, either
100/// a reference to a contract [`Vm`] or an enclosing [`HostFunction`]
101/// invocation.
102///
103/// Frames are arranged into a stack in [`HostImpl::context`], and are pushed
104/// with [`Host::push_frame`], which returns a [`FrameGuard`] that will
105/// pop the frame on scope-exit.
106///
107/// Frames are also the units of (sub-)transactions: each frame captures
108/// the host state when it is pushed, and the [`FrameGuard`] will either
109/// commit or roll back that state when it pops the stack.
110#[derive(Clone, Hash)]
111pub(crate) enum Frame {
112 ContractVM {
113 vm: Rc<Vm>,
114 fn_name: Symbol,
115 args: Vec<Val>,
116 instance: ScContractInstance,
117 relative_objects: Vec<Object>,
118 },
119 HostFunction(HostFunctionType),
120 StellarAssetContract(Hash, Symbol, Vec<Val>, ScContractInstance),
121 #[cfg(any(test, feature = "testutils"))]
122 TestContract(TestContractFrame),
123}
124
125impl Frame {
126 fn contract_id(&self) -> Option<&Hash> {
127 match self {
128 Frame::ContractVM { vm, .. } => Some(&vm.contract_id),
129 Frame::HostFunction(_) => None,
130 Frame::StellarAssetContract(id, ..) => Some(id),
131 #[cfg(any(test, feature = "testutils"))]
132 Frame::TestContract(tc) => Some(&tc.id),
133 }
134 }
135
136 fn instance(&self) -> Option<&ScContractInstance> {
137 match self {
138 Frame::ContractVM { instance, .. } => Some(instance),
139 Frame::HostFunction(_) => None,
140 Frame::StellarAssetContract(_, _, _, instance) => Some(instance),
141 #[cfg(any(test, feature = "testutils"))]
142 Frame::TestContract(tc) => Some(&tc.instance),
143 }
144 }
145 #[cfg(any(test, feature = "testutils"))]
146 fn is_contract_vm(&self) -> bool {
147 matches!(self, Frame::ContractVM { .. })
148 }
149}
150
151impl Host {
152 /// Returns if the host currently has a frame on the stack.
153 ///
154 /// A frame being on the stack usually indicates that a contract is currently
155 /// executing, or is in a state just-before or just-after executing.
156 pub fn has_frame(&self) -> Result<bool, HostError> {
157 self.with_current_frame_opt(|opt| Ok(opt.is_some()))
158 }
159
160 /// Helper function for [`Host::with_frame`] below. Pushes a new [`Context`]
161 /// on the context stack, returning a [`RollbackPoint`] such that if
162 /// operation fails, it can be used to roll the [`Host`] back to the state
163 /// it had before its associated [`Context`] was pushed.
164 pub(super) fn push_context(&self, ctx: Context) -> Result<RollbackPoint, HostError> {
165 let _span = tracy_span!("push context");
166 let auth_manager = self.try_borrow_authorization_manager()?;
167 let auth_snapshot = auth_manager.push_frame(self, &ctx.frame)?;
168 // Establish the rp first, since this might run out of gas and fail.
169 let rp = RollbackPoint {
170 storage: self.try_borrow_storage()?.map.metered_clone(self)?,
171 events: self.try_borrow_events()?.vec.len(),
172 auth: auth_snapshot,
173 };
174 // Charge for the push, which might also run out of gas.
175 Vec::<Context>::charge_bulk_init_cpy(1, self.as_budget())?;
176 // Finally commit to doing the push.
177 self.try_borrow_context_stack_mut()?.push(ctx);
178 Ok(rp)
179 }
180
181 /// Helper function for [`Host::with_frame`] below. Pops a [`Context`] off
182 /// the current context stack and optionally rolls back the [`Host`]'s objects
183 /// and storage map to the state in the provided [`RollbackPoint`].
184 pub(super) fn pop_context(&self, orp: Option<RollbackPoint>) -> Result<Context, HostError> {
185 let _span = tracy_span!("pop context");
186
187 let ctx = self.try_borrow_context_stack_mut()?.pop();
188
189 #[cfg(any(test, feature = "recording_mode"))]
190 if self.try_borrow_context_stack()?.is_empty() {
191 // When there are no contexts left, emulate authentication for the
192 // recording auth mode. This is a no-op for the enforcing mode.
193 self.try_borrow_authorization_manager()?
194 .maybe_emulate_authentication(self)?;
195 // See explanation for this line in [crate::vm::Vm::parse_module] -- it exists
196 // to add-back module-parsing costs that were suppressed during the invocation.
197 if self.in_storage_recording_mode()? {
198 if *self.try_borrow_need_to_build_module_cache()? {
199 // Host function calls that upload Wasm and create contracts
200 // don't use the module cache and thus don't need to have it
201 // rebuilt.
202 self.rebuild_module_cache()?;
203 }
204 }
205 // Reset the flag for building the module cache. This is only relevant
206 // for tests that keep reusing the same host for several invocations.
207 *(self.try_borrow_need_to_build_module_cache_mut()?) = false;
208 }
209 let mut auth_snapshot = None;
210 if let Some(rp) = orp {
211 self.try_borrow_storage_mut()?.map = rp.storage;
212 self.try_borrow_events_mut()?.rollback(rp.events)?;
213 auth_snapshot = Some(rp.auth);
214 }
215 self.try_borrow_authorization_manager()?
216 .pop_frame(self, auth_snapshot)?;
217 ctx.ok_or_else(|| {
218 self.err(
219 ScErrorType::Context,
220 ScErrorCode::InternalError,
221 "unmatched host context push/pop",
222 &[],
223 )
224 })
225 }
226
227 /// Applies a function to the top [`Frame`] of the context stack. Returns
228 /// [`HostError`] if the context stack is empty, otherwise returns result of
229 /// function call.
230 //
231 // Notes on metering: aquiring the current frame is cheap and not charged.
232 // Metering happens in the passed-in closure where actual work is being done.
233 pub(super) fn with_current_frame<F, U>(&self, f: F) -> Result<U, HostError>
234 where
235 F: FnOnce(&Frame) -> Result<U, HostError>,
236 {
237 let Ok(context_guard) = self.0.context_stack.try_borrow() else {
238 return Err(self.err(
239 ScErrorType::Context,
240 ScErrorCode::InternalError,
241 "context is already borrowed",
242 &[],
243 ));
244 };
245
246 if let Some(context) = context_guard.last() {
247 f(&context.frame)
248 } else {
249 drop(context_guard);
250 Err(self.err(
251 ScErrorType::Context,
252 ScErrorCode::InternalError,
253 "no contract running",
254 &[],
255 ))
256 }
257 }
258
259 /// Applies a function to a mutable reference to the top [`Context`] of the
260 /// context stack. Returns [`HostError`] if the context stack is empty,
261 /// otherwise returns result of function call.
262 //
263 // Notes on metering: aquiring the current frame is cheap and not charged.
264 // Metering happens in the passed-in closure where actual work is being done.
265 pub(super) fn with_current_context_mut<F, U>(&self, f: F) -> Result<U, HostError>
266 where
267 F: FnOnce(&mut Context) -> Result<U, HostError>,
268 {
269 let Ok(mut context_guard) = self.0.context_stack.try_borrow_mut() else {
270 return Err(self.err(
271 ScErrorType::Context,
272 ScErrorCode::InternalError,
273 "context is already borrowed",
274 &[],
275 ));
276 };
277 if let Some(context) = context_guard.last_mut() {
278 f(context)
279 } else {
280 drop(context_guard);
281 Err(self.err(
282 ScErrorType::Context,
283 ScErrorCode::InternalError,
284 "no contract running",
285 &[],
286 ))
287 }
288 }
289
290 /// Same as [`Self::with_current_frame`] but passes `None` when there is no current
291 /// frame, rather than failing with an error.
292 pub(crate) fn with_current_frame_opt<F, U>(&self, f: F) -> Result<U, HostError>
293 where
294 F: FnOnce(Option<&Frame>) -> Result<U, HostError>,
295 {
296 let Ok(context_guard) = self.0.context_stack.try_borrow() else {
297 return Err(self.err(
298 ScErrorType::Context,
299 ScErrorCode::InternalError,
300 "context is already borrowed",
301 &[],
302 ));
303 };
304 if let Some(context) = context_guard.last() {
305 f(Some(&context.frame))
306 } else {
307 drop(context_guard);
308 f(None)
309 }
310 }
311
312 pub(crate) fn with_current_frame_relative_object_table<F, U>(
313 &self,
314 f: F,
315 ) -> Result<U, HostError>
316 where
317 F: FnOnce(&mut Vec<Object>) -> Result<U, HostError>,
318 {
319 self.with_current_context_mut(|ctx| {
320 if let Frame::ContractVM {
321 relative_objects, ..
322 } = &mut ctx.frame
323 {
324 f(relative_objects)
325 } else {
326 Err(self.err(
327 ScErrorType::Context,
328 ScErrorCode::InternalError,
329 "accessing relative object table in non-VM frame",
330 &[],
331 ))
332 }
333 })
334 }
335
336 pub(crate) fn with_current_prng<F, U>(&self, f: F) -> Result<U, HostError>
337 where
338 F: FnOnce(&mut Prng) -> Result<U, HostError>,
339 {
340 // We mem::take the context's PRNG into a local variable and then put it
341 // back when we're done. This allows the callback to borrow the context
342 // to report errors if anything goes wrong in it. If the callback also
343 // installs a PRNG of its own (it shouldn't!) we notice when putting the
344 // context's PRNG back and fail with an internal error.
345 let curr_prng_opt =
346 self.with_current_context_mut(|ctx| Ok(std::mem::take(&mut ctx.prng)))?;
347
348 let mut curr_prng = match curr_prng_opt {
349 // There's already a context PRNG, so use it.
350 Some(prng) => prng,
351
352 // There's no context PRNG yet, seed one from the base PRNG (unless
353 // the base PRNG itself hasn't been seeded).
354 None => {
355 let mut base_guard = self.try_borrow_base_prng_mut()?;
356 if let Some(base) = base_guard.as_mut() {
357 base.sub_prng(self.as_budget())?
358 } else {
359 return Err(self.err(
360 ScErrorType::Context,
361 ScErrorCode::InternalError,
362 "host base PRNG was not seeded",
363 &[],
364 ));
365 }
366 }
367 };
368
369 // Call the callback with the new-or-existing context PRNG.
370 let res: Result<U, HostError> = f(&mut curr_prng);
371
372 // Put the (possibly newly-initialized frame PRNG-option back)
373 self.with_current_context_mut(|ctx| {
374 if ctx.prng.is_some() {
375 return Err(self.err(
376 ScErrorType::Context,
377 ScErrorCode::InternalError,
378 "callback re-entered with_current_prng",
379 &[],
380 ));
381 }
382 ctx.prng = Some(curr_prng);
383 Ok(())
384 })?;
385 res
386 }
387
388 /// Pushes a [`Frame`], runs a closure, and then pops the frame, rolling back
389 /// if the closure returned an error. Returns the result that the closure
390 /// returned (or any error caused during the frame push/pop).
391 pub(crate) fn with_frame<F>(&self, frame: Frame, f: F) -> Result<Val, HostError>
392 where
393 F: FnOnce() -> Result<Val, HostError>,
394 {
395 let start_depth = self.try_borrow_context_stack()?.len();
396 if start_depth as u32 >= DEFAULT_HOST_DEPTH_LIMIT {
397 return Err(Error::from_type_and_code(
398 ScErrorType::Context,
399 ScErrorCode::ExceededLimit,
400 )
401 .into());
402 }
403 #[cfg(any(test, feature = "testutils"))]
404 {
405 if let Some(ctx) = self.try_borrow_context_stack()?.last() {
406 if frame.is_contract_vm() && ctx.frame.is_contract_vm() {
407 if let Ok(mut scoreboard) = self.try_borrow_coverage_scoreboard_mut() {
408 scoreboard.vm_to_vm_calls += 1;
409 }
410 }
411 }
412 }
413 let ctx = Context {
414 frame,
415 prng: None,
416 storage: None,
417 };
418 let rp = self.push_context(ctx)?;
419 {
420 // We do this _after_ the context is pushed, in order to let the
421 // observation code assume a context exists
422 if let Some(ctx) = self.try_borrow_context_stack()?.last() {
423 self.call_any_lifecycle_hook(crate::host::TraceEvent::PushCtx(ctx))?;
424 }
425 }
426 #[cfg(any(test, feature = "testutils"))]
427 let mut is_top_contract_invocation = false;
428 #[cfg(any(test, feature = "testutils"))]
429 {
430 if self.try_borrow_context_stack()?.len() == 1 {
431 if let Some(ctx) = self.try_borrow_context_stack()?.first() {
432 match ctx.frame {
433 // Don't call the contract invocation hook for
434 // the host functions.
435 Frame::HostFunction(_) => (),
436 // Everything else is some sort of contract call.
437 _ => {
438 is_top_contract_invocation = true;
439 if let Some(contract_invocation_hook) =
440 self.try_borrow_top_contract_invocation_hook()?.as_ref()
441 {
442 contract_invocation_hook(
443 self,
444 crate::host::ContractInvocationEvent::Start,
445 );
446 }
447 }
448 }
449 }
450 }
451 }
452
453 let res = f();
454 let mut res = if let Ok(v) = res {
455 // If a contract function happens to have signature Result<...,
456 // Code> its Wasm ABI encoding will be ambiguous: if it exits with
457 // Err(Code) it'll wind up exiting the Wasm VM "successfully" with a
458 // Val that's of type Error, we'll get Ok(Error) here. To allow this
459 // to work and avoid losing errors, we define _any_ successful
460 // return of Ok(Error) as "a contract failure"; contracts aren't
461 // allowed to return Ok(Error) and have it considered actually-ok.
462 //
463 // (If we were called from try_call, it will actually turn this
464 // Err(ScErrorType::Contract) back into Ok(ScErrorType::Contract)
465 // since that is a "recoverable" type of error.)
466 if let Ok(err) = Error::try_from(v) {
467 // Unfortunately there are still two sub-cases to consider. One
468 // is when a contract returns Ok(Error) with
469 // ScErrorType::Contract, which is allowed and legitimate and
470 // "how a contract would signal a Result::Err(Code) as described
471 // above". In this (good) case we propagate the Error they
472 // provided, just switching it from Ok(Error) to Err(Error)
473 // indicating that the contract "failed" with this Error.
474 //
475 // The second (bad) case is when the contract returns Ok(Error)
476 // with a non-ScErrorType::Contract. This might be some kind of
477 // mistake on their part but it might also be an attempt at
478 // spoofing error reporting, by claiming some subsystem of the
479 // host failed when it really didn't. In particular if a
480 // contract wants to forcibly fail a caller that did `try_call`,
481 // the contract could spoof-return an unrecoverable Error code
482 // like InternalError or BudgetExceeded. We want to deny all
483 // such cases, so we just define them as illegal returns, and
484 // report them all as a specific error type of and description
485 // our own choosing: not a contract's own logic failing, but a
486 // contract failing to live up to a postcondition we're
487 // enforcing of "never returning this sort of error code".
488 if err.is_type(ScErrorType::Contract) {
489 Err(self.error(
490 err,
491 "escalating Ok(ScErrorType::Contract) frame-exit to Err",
492 &[],
493 ))
494 } else {
495 Err(self.err(
496 ScErrorType::Context,
497 ScErrorCode::InvalidAction,
498 "frame-exit with Ok(Error) carrying a non-ScErrorType::Contract Error",
499 &[err.to_val()],
500 ))
501 }
502 } else {
503 Ok(v)
504 }
505 } else {
506 res
507 };
508
509 // We try flushing instance storage at the end of the frame if nothing
510 // else failed. Unfortunately flushing instance storage is _itself_
511 // fallible in a variety of ways, and if it fails we want to roll back
512 // everything else.
513 if res.is_ok() {
514 if let Err(e) = self.persist_instance_storage() {
515 res = Err(e)
516 }
517 }
518 {
519 // We do this _before_ the context is popped, in order to let the
520 // observation code assume a context exists
521 if let Some(ctx) = self.try_borrow_context_stack()?.last() {
522 let res = match &res {
523 Ok(v) => Ok(*v),
524 Err(ref e) => Err(e),
525 };
526 self.call_any_lifecycle_hook(crate::host::TraceEvent::PopCtx(&ctx, &res))?;
527 }
528 }
529 if res.is_err() {
530 // Pop and rollback on error.
531 self.pop_context(Some(rp))?
532 } else {
533 // Just pop on success.
534 self.pop_context(None)?
535 };
536 // Every push and pop should be matched; if not there is a bug.
537 let end_depth = self.try_borrow_context_stack()?.len();
538 if start_depth != end_depth {
539 return Err(err!(
540 self,
541 (ScErrorType::Context, ScErrorCode::InternalError),
542 "frame-depth mismatch",
543 start_depth,
544 end_depth
545 ));
546 }
547 #[cfg(any(test, feature = "testutils"))]
548 if end_depth == 0 {
549 // Empty call stack in tests means that some contract function call
550 // has been finished and hence the authorization manager can be reset.
551 // In non-test scenarios, there should be no need to ever reset
552 // the authorization manager as the host instance shouldn't be
553 // shared between the contract invocations.
554 *self.try_borrow_previous_authorization_manager_mut()? =
555 Some(self.try_borrow_authorization_manager()?.clone());
556 self.try_borrow_authorization_manager_mut()?.reset();
557
558 // Call the contract invocation hook for contract invocations only.
559 if is_top_contract_invocation {
560 if let Some(top_contract_invocation_hook) =
561 self.try_borrow_top_contract_invocation_hook()?.as_ref()
562 {
563 top_contract_invocation_hook(
564 self,
565 crate::host::ContractInvocationEvent::Finish,
566 );
567 }
568 }
569 }
570 res
571 }
572
573 /// Inspects the frame at the top of the context and returns the contract ID
574 /// if it exists. Returns `Ok(None)` if the context stack is empty or has a
575 /// non-contract frame on top.
576 pub(crate) fn get_current_contract_id_opt_internal(&self) -> Result<Option<Hash>, HostError> {
577 self.with_current_frame_opt(|opt_frame| match opt_frame {
578 Some(frame) => frame
579 .contract_id()
580 .map(|id| id.metered_clone(self))
581 .transpose(),
582 None => Ok(None),
583 })
584 }
585
586 /// Returns [`Hash`] contract ID from the VM frame at the top of the context
587 /// stack, or a [`HostError`] if the context stack is empty or has a non-VM
588 /// frame at its top.
589 pub(crate) fn get_current_contract_id_internal(&self) -> Result<Hash, HostError> {
590 if let Some(id) = self.get_current_contract_id_opt_internal()? {
591 Ok(id)
592 } else {
593 // This should only ever happen if we try to access the contract ID
594 // from a HostFunction frame (meaning before a contract is running).
595 // Doing so is a logic bug on our part. If we simply run out of
596 // budget while cloning the Hash we won't get here, the `?` above
597 // will propagate the budget error.
598 Err(self.err(
599 ScErrorType::Context,
600 ScErrorCode::InternalError,
601 "Current context has no contract ID",
602 &[],
603 ))
604 }
605 }
606
607 /// Pushes a test contract [`Frame`], runs a closure, and then pops the
608 /// frame, rolling back if the closure returned an error. Returns the result
609 /// that the closure returned (or any error caused during the frame
610 /// push/pop). Used for testing.
611 #[cfg(any(test, feature = "testutils"))]
612 pub fn with_test_contract_frame<F>(
613 &self,
614 id: Hash,
615 func: Symbol,
616 f: F,
617 ) -> Result<Val, HostError>
618 where
619 F: FnOnce() -> Result<Val, HostError>,
620 {
621 self.with_frame(
622 Frame::TestContract(self.create_test_contract_frame(id, func, vec![])?),
623 f,
624 )
625 }
626
627 #[cfg(any(test, feature = "testutils"))]
628 fn create_test_contract_frame(
629 &self,
630 id: Hash,
631 func: Symbol,
632 args: Vec<Val>,
633 ) -> Result<TestContractFrame, HostError> {
634 let instance_key = self.contract_instance_ledger_key(&id)?;
635 let instance = self.retrieve_contract_instance_from_storage(&instance_key)?;
636 Ok(TestContractFrame::new(id, func, args.to_vec(), instance))
637 }
638
639 // Notes on metering: this is covered by the called components.
640 fn call_contract_fn(&self, id: &Hash, func: &Symbol, args: &[Val]) -> Result<Val, HostError> {
641 // Create key for storage
642 let storage_key = self.contract_instance_ledger_key(id)?;
643 let instance = self.retrieve_contract_instance_from_storage(&storage_key)?;
644 Vec::<Val>::charge_bulk_init_cpy(args.len() as u64, self.as_budget())?;
645 let args_vec = args.to_vec();
646 match &instance.executable {
647 ContractExecutable::Wasm(wasm_hash) => {
648 // If the module cache is not yet built, build it now, before first access.
649 // Unless we're in recording mode, because in that case the cache is built
650 // late in [pop_context] after we've determined the transaction footprint.
651 // In the latter case we just mark that the module cache needs to be built.
652 #[cfg(any(test, feature = "recording_mode"))]
653 {
654 if !self.in_storage_recording_mode()? {
655 self.build_module_cache_if_needed()?;
656 } else {
657 *(self.try_borrow_need_to_build_module_cache_mut()?) = true;
658 }
659 }
660 #[cfg(not(any(test, feature = "recording_mode")))]
661 self.build_module_cache_if_needed()?;
662
663 let contract_id = id.metered_clone(self)?;
664 let parsed_module = if let Some(cache) = &*self.try_borrow_module_cache()? {
665 // Check that storage thinks the entry exists before
666 // checking the cache: this seems like overkill but it
667 // provides some future-proofing, see below.
668 let wasm_key = self.contract_code_ledger_key(wasm_hash)?;
669 if self
670 .try_borrow_storage_mut()?
671 .has_with_host(&wasm_key, self, None)?
672 {
673 cache.get_module(self, wasm_hash)?
674 } else {
675 None
676 }
677 } else {
678 None
679 };
680 let vm = if let Some(module) = parsed_module {
681 Vm::from_parsed_module(self, contract_id, module)?
682 } else {
683 // We can get here a few ways:
684 //
685 // 1. We are running/replaying a protocol that has no
686 // module cache.
687 //
688 // 2. We have a module cache, but it somehow doesn't have
689 // the module requested. This in turn has two
690 // sub-cases:
691 //
692 // - User invoked us with bad input, eg. calling a
693 // contract that wasn't provided in footprint/storage.
694 //
695 // - User uploaded the wasm _in this transaction_ so we
696 // didn't cache it when starting the transaction (and
697 // couldn't due to wasmi locking its engine while
698 // running).
699 //
700 // 3. Even more pathological: the module cache was built,
701 // and contained the module, but someone _removed_ the
702 // wasm from storage after the the cache was built
703 // (this is not currently possible from guest code, but
704 // we do some future-proofing here in case it becomes
705 // possible). This is the case we handle above with the
706 // early check for storage.has(wasm_key) before
707 // checking the cache as well.
708 //
709 // In all these cases, we want to try accessing storage, and
710 // if it has the wasm, make a _throwaway_ module with its
711 // own engine. If it doesn't have the wasm, we want to fail
712 // with a storage error.
713
714 let (code, costs) = self.retrieve_wasm_from_storage(&wasm_hash)?;
715
716 #[cfg(any(test, feature = "recording_mode"))]
717 // In recording mode: if a contract was present in the initial snapshot image, it is part of
718 // the set of contracts that would have been built into a module cache in enforcing mode;
719 // we want to defer the cost of parsing those (simulating them as cache hits) and then charge
720 // once for each such contract the simulated-module-cache-build that happens at the end of
721 // the frame in [`Self::pop_context`].
722 //
723 // If a contract is _not_ in the initial snapshot image, it's because someone just uploaded
724 // it during execution. Those would be cache misses in enforcing mode, and so it is right to
725 // continue to charge for them as such (charging the parse cost on each call) in recording.
726 let cost_mode = if self.in_storage_recording_mode()? {
727 let contact_code_key =
728 self.budget_ref().with_observable_shadow_mode(|| {
729 self.contract_code_ledger_key(wasm_hash)
730 })?;
731 if self
732 .try_borrow_storage()?
733 .get_snapshot_value(self, &contact_code_key)?
734 .is_some()
735 {
736 crate::vm::ModuleParseCostMode::PossiblyDeferredIfRecording
737 } else {
738 crate::vm::ModuleParseCostMode::Normal
739 }
740 } else {
741 crate::vm::ModuleParseCostMode::Normal
742 };
743 #[cfg(not(any(test, feature = "recording_mode")))]
744 let cost_mode = crate::vm::ModuleParseCostMode::Normal;
745
746 Vm::new_with_cost_inputs(self, contract_id, code.as_slice(), costs, cost_mode)?
747 };
748 let relative_objects = Vec::new();
749 self.with_frame(
750 Frame::ContractVM {
751 vm: Rc::clone(&vm),
752 fn_name: *func,
753 args: args_vec,
754 instance,
755 relative_objects,
756 },
757 || vm.invoke_function_raw(self, func, args),
758 )
759 }
760 ContractExecutable::StellarAsset => self.with_frame(
761 Frame::StellarAssetContract(id.metered_clone(self)?, *func, args_vec, instance),
762 || {
763 use crate::builtin_contracts::{BuiltinContract, StellarAssetContract};
764 StellarAssetContract.call(func, self, args)
765 },
766 ),
767 }
768 }
769
770 // Notes on metering: this is covered by the called components.
771 pub(crate) fn call_n_internal(
772 &self,
773 id: &Hash,
774 func: Symbol,
775 args: &[Val],
776 reentry_mode: ContractReentryMode,
777 internal_host_call: bool,
778 ) -> Result<Val, HostError> {
779 // Internal host calls may call some special functions that otherwise
780 // aren't allowed to be called.
781 if !internal_host_call
782 && SymbolStr::try_from_val(self, &func)?
783 .to_string()
784 .as_str()
785 .starts_with(RESERVED_CONTRACT_FN_PREFIX)
786 {
787 return Err(self.err(
788 ScErrorType::Context,
789 ScErrorCode::InvalidAction,
790 "can't invoke a reserved function directly",
791 &[func.to_val()],
792 ));
793 }
794
795 if !matches!(reentry_mode, ContractReentryMode::Allowed) {
796 let reentry_distance = self
797 .try_borrow_context_stack()?
798 .iter()
799 .rev()
800 .filter_map(|c| c.frame.contract_id())
801 .position(|caller| caller == id);
802
803 match (reentry_mode, reentry_distance) {
804 // Non-reentrant calls, or calls in Allowed mode,
805 // or immediate-reentry calls in SelfAllowed mode
806 // are all acceptable.
807 (_, None)
808 | (ContractReentryMode::Allowed, _)
809 | (ContractReentryMode::SelfAllowed, Some(0)) => (),
810
811 // But any non-immediate-reentry in SelfAllowed mode,
812 // or any reentry at all in Prohibited mode, are errors.
813 (ContractReentryMode::SelfAllowed, Some(_))
814 | (ContractReentryMode::Prohibited, Some(_)) => {
815 return Err(self.err(
816 ScErrorType::Context,
817 ScErrorCode::InvalidAction,
818 "Contract re-entry is not allowed",
819 &[],
820 ));
821 }
822 }
823 }
824
825 self.fn_call_diagnostics(id, &func, args);
826
827 // Try dispatching the contract to the compiled-in registred
828 // implmentation. Only the contracts with the special (empty) executable
829 // are dispatched in this way, so that it's possible to switch the
830 // compiled-in implementation back to Wasm via
831 // `update_current_contract_wasm`.
832 // "testutils" is not covered by budget metering.
833 #[cfg(any(test, feature = "testutils"))]
834 if self.is_test_contract_executable(id)? {
835 // This looks a little un-idiomatic, but this avoids maintaining a borrow of
836 // self.0.contracts. Implementing it as
837 //
838 // if let Some(cfs) = self.try_borrow_contracts()?.get(&id).cloned() { ... }
839 //
840 // maintains a borrow of self.0.contracts, which can cause borrow errors.
841 let cfs_option = self.try_borrow_contracts()?.get(&id).cloned();
842 if let Some(cfs) = cfs_option {
843 let frame = self.create_test_contract_frame(id.clone(), func, args.to_vec())?;
844 let panic = frame.panic.clone();
845 return self.with_frame(Frame::TestContract(frame), || {
846 use std::any::Any;
847 use std::panic::AssertUnwindSafe;
848 type PanicVal = Box<dyn Any + Send>;
849
850 // We're directly invoking a native rust contract here,
851 // which we allow only in local testing scenarios, and we
852 // want it to behave as close to the way it would behave if
853 // the contract were actually compiled to WASM and running
854 // in a VM.
855 //
856 // In particular: if the contract function panics, if it
857 // were WASM it would cause the VM to trap, so we do
858 // something "as similar as we can" in the native case here,
859 // catch the native panic and attempt to continue by
860 // translating the panic back to an error, so that
861 // `with_frame` will rollback the host to its pre-call state
862 // (as best it can) and propagate the error to its caller
863 // (which might be another contract doing try_call).
864 //
865 // This is somewhat best-effort, but it's compiled-out when
866 // building a host for production use, so we're willing to
867 // be a bit forgiving.
868 let closure = AssertUnwindSafe(move || cfs.call(&func, self, args));
869 let res: Result<Option<Val>, PanicVal> =
870 crate::testutils::call_with_suppressed_panic_hook(closure);
871 match res {
872 Ok(Some(val)) => {
873 self.fn_return_diagnostics(id, &func, &val);
874 Ok(val)
875 }
876 Ok(None) => Err(self.err(
877 ScErrorType::Context,
878 ScErrorCode::MissingValue,
879 "calling unknown contract function",
880 &[func.to_val()],
881 )),
882 Err(panic_payload) => {
883 // Return an error indicating the contract function
884 // panicked.
885 //
886 // If it was a panic generated by a Env-upgraded
887 // HostError, it had its `Error` captured by
888 // `VmCallerEnv::escalate_error_to_panic`: fish the
889 // `Error` stored in the frame back out and
890 // propagate it.
891 //
892 // If it was a panic generated by user code calling
893 // panic!(...) we won't retrieve such a stored
894 // `Error`. Since we're trying to emulate
895 // what-the-VM-would-do here, and the VM traps with
896 // an unreachable error on contract panic, we
897 // generate same error (by converting a wasm
898 // trap-unreachable code). It's a little weird
899 // because we're not actually running a VM, but we
900 // prioritize emulation fidelity over honesty here.
901 let mut error: Error =
902 Error::from(wasmi::core::TrapCode::UnreachableCodeReached);
903
904 let mut recovered_error_from_panic_refcell = false;
905 if let Ok(panic) = panic.try_borrow() {
906 if let Some(err) = *panic {
907 recovered_error_from_panic_refcell = true;
908 error = err;
909 }
910 }
911
912 // If we didn't manage to recover a structured error
913 // code from the frame's refcell, and we're allowed
914 // to record dynamic strings (which happens when
915 // diagnostics are active), and we got a panic
916 // payload of a simple string, log that panic
917 // payload into the diagnostic event buffer. This
918 // code path will get hit when contracts do
919 // `panic!("some string")` in native testing mode.
920 if !recovered_error_from_panic_refcell {
921 self.with_debug_mode(|| {
922 if let Some(str) = panic_payload.downcast_ref::<&str>() {
923 let msg: String = format!(
924 "caught panic '{}' from contract function '{:?}'",
925 str, func
926 );
927 let _ = self.log_diagnostics(&msg, args);
928 } else if let Some(str) = panic_payload.downcast_ref::<String>()
929 {
930 let msg: String = format!(
931 "caught panic '{}' from contract function '{:?}'",
932 str, func
933 );
934 let _ = self.log_diagnostics(&msg, args);
935 };
936 Ok(())
937 })
938 }
939 Err(self.error(error, "caught error from function", &[]))
940 }
941 }
942 });
943 }
944 }
945
946 let res = self.call_contract_fn(id, &func, args);
947
948 match &res {
949 Ok(res) => self.fn_return_diagnostics(id, &func, res),
950 Err(_err) => {}
951 }
952
953 res
954 }
955
956 // Notes on metering: covered by the called components.
957 fn invoke_function_and_return_val(&self, hf: HostFunction) -> Result<Val, HostError> {
958 let hf_type = hf.discriminant();
959 let frame = Frame::HostFunction(hf_type);
960 match hf {
961 HostFunction::InvokeContract(invoke_args) => {
962 self.with_frame(frame, || {
963 // Metering: conversions to host objects are covered.
964 let ScAddress::Contract(ref contract_id) = invoke_args.contract_address else {
965 return Err(self.err(
966 ScErrorType::Value,
967 ScErrorCode::UnexpectedType,
968 "invoked address doesn't belong to a contract",
969 &[],
970 ));
971 };
972 let function_name: Symbol = invoke_args.function_name.try_into_val(self)?;
973 let args = self.scvals_to_val_vec(invoke_args.args.as_slice())?;
974 // since the `HostFunction` frame must be the bottom of the call stack,
975 // reentry is irrelevant, we always pass in `ContractReentryMode::Prohibited`.
976 self.call_n_internal(
977 contract_id,
978 function_name,
979 args.as_slice(),
980 ContractReentryMode::Prohibited,
981 false,
982 )
983 })
984 }
985 HostFunction::CreateContract(args) => self.with_frame(frame, || {
986 let deployer: Option<AddressObject> = match &args.contract_id_preimage {
987 ContractIdPreimage::Address(preimage_from_addr) => {
988 Some(self.add_host_object(preimage_from_addr.address.metered_clone(self)?)?)
989 }
990 ContractIdPreimage::Asset(_) => None,
991 };
992 self.create_contract_internal(deployer, args)
993 .map(<Val>::from)
994 }),
995 HostFunction::UploadContractWasm(wasm) => self.with_frame(frame, || {
996 self.upload_contract_wasm(wasm.to_vec()).map(<Val>::from)
997 }),
998 }
999 }
1000
1001 // Notes on metering: covered by the called components.
1002 pub fn invoke_function(&self, hf: HostFunction) -> Result<ScVal, HostError> {
1003 let rv = self.invoke_function_and_return_val(hf)?;
1004 self.from_host_val(rv)
1005 }
1006
1007 pub(crate) fn maybe_init_instance_storage(&self, ctx: &mut Context) -> Result<(), HostError> {
1008 // Lazily initialize the storage on first access - it's not free and
1009 // not every contract will use it.
1010 if ctx.storage.is_some() {
1011 return Ok(());
1012 }
1013
1014 let Some(instance) = ctx.frame.instance() else {
1015 return Err(self.err(
1016 ScErrorType::Context,
1017 ScErrorCode::InternalError,
1018 "access to instance in frame without instance",
1019 &[],
1020 ));
1021 };
1022
1023 ctx.storage = Some(InstanceStorageMap::from_map(
1024 instance.storage.as_ref().map_or_else(
1025 || Ok(vec![]),
1026 |m| {
1027 m.iter()
1028 .map(|i| {
1029 Ok((
1030 self.to_valid_host_val(&i.key)?,
1031 self.to_valid_host_val(&i.val)?,
1032 ))
1033 })
1034 .metered_collect::<Result<Vec<(Val, Val)>, HostError>>(self)?
1035 },
1036 )?,
1037 self,
1038 )?);
1039 Ok(())
1040 }
1041
1042 // Make the in-memory instance storage persist into the `Storage` by writing
1043 // its updated contents into corresponding `ContractData` ledger entry.
1044 fn persist_instance_storage(&self) -> Result<(), HostError> {
1045 let updated_instance_storage = self.with_current_context_mut(|ctx| {
1046 if let Some(storage) = &ctx.storage {
1047 if !storage.is_modified {
1048 return Ok(None);
1049 }
1050 Ok(Some(self.host_map_to_scmap(&storage.map)?))
1051 } else {
1052 Ok(None)
1053 }
1054 })?;
1055 if updated_instance_storage.is_some() {
1056 let contract_id = self.get_current_contract_id_internal()?;
1057 let key = self.contract_instance_ledger_key(&contract_id)?;
1058
1059 self.store_contract_instance(None, updated_instance_storage, contract_id, &key)?;
1060 }
1061 Ok(())
1062 }
1063}