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