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