Repository: stellar/rs-soroban-env
Pull request #1244 (2023-11-22 04:36:42)

Add Host::has_frame
### What
Add a method to the host for determining if the host currently has a frame on the stack.

### Why
In several places in the SDK it would be ideal if we could error if a function is used inside or outside of a contract function, because doing so would be invalid.

In some code being added or auth tracking in the SDK I need to reliable know if we are inside a contract function or not.

I tried to do this using top invocation hooks, but the way native contract functions get called in the SDK doesn't pass through anything from the Env that is available outside of the contract. We can sort of know if we're in a contract, but it isn't guaranteed to be correct in edge cases. We might change that some day, but at the moment that limitation exists.

Exposing whether the host currently has a frame is a simple way the host can indicate to SDK test utilities if it is currently in a contract function or not.

Close https://github.com/stellar/rs-soroban-sdk/issues/1084


Patch:
diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs
index db899370..7fa7a1cd 100644
--- a/soroban-env-host/src/host/frame.rs
+++ b/soroban-env-host/src/host/frame.rs
@@ -132,6 +132,14 @@ pub(crate) enum Frame {
 }
 
 impl Host {
+    /// Returns if the host currently has a frame on the stack.
+    ///
+    /// A frame being on the stack indicates that a contract is currently
+    /// executing.
+    pub fn has_frame(&self) -> Result<bool, HostError> {
+        self.with_current_frame_opt(|opt| Ok(opt.is_some()))
+    }
+
     /// Helper function for [`Host::with_frame`] below. Pushes a new [`Context`]
     /// on the context stack, returning a [`RollbackPoint`] such that if
     /// operation fails, it can be used to roll the [`Host`] back to the state


Test patch:
diff --git a/soroban-env-host/observations/test__frame__has_frame.json b/soroban-env-host/observations/test__frame__has_frame.json
new file mode 100644
index 00000000..1ab62cf2
--- /dev/null
+++ b/soroban-env-host/observations/test__frame__has_frame.json
@@ -0,0 +1,6 @@
+{
+  "   0 begin": "cpu:9996, mem:0, prngs:-/9b4a753, objs:-/-, vm:-/-, evt:-, store:-/-, foot:-, stk:-, auth:-/-",
+  "   1 push TEST:   0:()": "cpu:64312, mem:2670, objs:-/3@f25d6726, store:-/2@8f53be5d, foot:2@b5543661, stk:1@bee58c5, auth:1@b0976283/-",
+  "   2 pop TEST:   0: -> Ok(Void)": "",
+  "   3 end": "cpu:64312, mem:2670, prngs:-/9b4a753, objs:-/3@f25d6726, vm:-/-, evt:-, store:-/2@8f53be5d, foot:2@b5543661, stk:-, auth:-/-"
+}
\ No newline at end of file
diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs
index 78b8ab8b..f566b02a 100644
--- a/soroban-env-host/src/test.rs
+++ b/soroban-env-host/src/test.rs
@@ -13,6 +13,7 @@ mod depth_limit;
 mod dispatch;
 mod event;
 mod finish;
+mod frame;
 mod hostile;
 #[cfg(opt_build)]
 mod hostile_opt;
diff --git a/soroban-env-host/src/test/frame.rs b/soroban-env-host/src/test/frame.rs
new file mode 100644
index 00000000..7994b6b6
--- /dev/null
+++ b/soroban-env-host/src/test/frame.rs
@@ -0,0 +1,36 @@
+use crate::{host::HostError, ContractFunctionSet, Host};
+use soroban_env_common::{
+    xdr::{Hash, ScAddress},
+    Symbol, Val,
+};
+use std::rc::Rc;
+
+#[test]
+fn has_frame() -> Result<(), HostError> {
+    struct NoopContractFunctionSet;
+    impl ContractFunctionSet for NoopContractFunctionSet {
+        fn call(&self, _func: &Symbol, _host: &Host, _args: &[Val]) -> Option<Val> {
+            None
+        }
+    }
+
+    let host = observe_host!(Host::test_host_with_recording_footprint());
+
+    // Host has no frame outside of executing a contract.
+    assert!(!host.has_frame()?);
+
+    // Host has a frame when executing a contract.
+    let id = [0u8; 32];
+    let address = host.add_host_object(ScAddress::Contract(Hash(id)))?;
+    host.register_test_contract(address, Rc::new(NoopContractFunctionSet))?;
+    let func = Symbol::try_from_small_str("")?;
+    host.with_test_contract_frame(Hash(id), func, || {
+        assert!(host.has_frame()?);
+        Ok(().into())
+    })?;
+
+    // Host has no frame outside of executing a contract.
+    assert!(!host.has_frame()?);
+
+    Ok(())
+}