Repository: stellar/rs-soroban-env
Pull request #342 (2022-08-18 21:34:33)

Support contract events
### What

Resolves https://github.com/stellar/rs-soroban-env/issues/322

cc @paulbellamy 

### Why

[TODO: Why this change is being made. Include any context required to understand the why.]

### Known limitations

~Lacks testing.~ Added a native test.
Need to update the sdk (https://github.com/stellar/rs-soroban-sdk/issues/451) ~and write a test contract which records events.~ 




Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6fe920ac..b3fb7e72 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -724,7 +724,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 [[package]]
 name = "stellar-xdr"
 version = "0.0.1"
-source = "git+https://github.com/stellar/rs-stellar-xdr?rev=f4fe3091#f4fe309136070c3d932bfcb8019ded953b842e5c"
+source = "git+https://github.com/stellar/rs-stellar-xdr?rev=61de11d#61de11d0ce5eb8a6e722d903af2abfadb5f5bd0e"
 dependencies = [
  "serde",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index c8536a4c..3c26cef2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,5 +14,5 @@ soroban-env-common = { path = "soroban-env-common" }
 soroban-env-host = { path = "soroban-env-host" }
 soroban-env-macros = { path = "soroban-env-macros" }
 soroban-native-sdk-macros = { path = "soroban-native-sdk-macros" }
-stellar-xdr = { git = "https://github.com/stellar/rs-stellar-xdr", rev = "f4fe3091" }
+stellar-xdr = { git = "https://github.com/stellar/rs-stellar-xdr", rev = "61de11d" }
 wasmi = { package = "soroban-wasmi", git = "https://github.com/stellar/wasmi", rev = "7b1f2355" }
diff --git a/soroban-env-common/src/env.rs b/soroban-env-common/src/env.rs
index 995b173f..22bdecdc 100644
--- a/soroban-env-common/src/env.rs
+++ b/soroban-env-common/src/env.rs
@@ -140,27 +140,27 @@ macro_rules! call_macro_with_all_host_functions {
                 // This one variant of logging does not take a format string and
                 // is live in both Env=Guest and Env=Host configurations.
                 {"_", fn log_value(v:RawVal) -> RawVal }
-
                 /// Get the binary contractID of the contract which invoked the
                 /// running contract. Traps if the running contract was not
                 /// invoked by a contract.
                 {"0", fn get_invoking_contract() -> Object }
                 {"1", fn obj_cmp(a:RawVal, b:RawVal) -> i64 }
-                {"2", fn contract_event(v:RawVal) -> RawVal }
-                {"3", fn system_event(v:RawVal) -> RawVal }
+                /// Records a contract event. `topics` is expected to be a `SCVec` with
+                /// length <= 4 that cannot contain Vecs, Maps, or Binaries > 32 bytes
+                /// On succes, returns an `SCStatus::Ok`.
+                {"2", fn contract_event(topics:Object, data:RawVal) -> RawVal }
                 /// Get the binary contractID of the contract which invoked the
                 /// running contract. Traps if the running contract was not
                 /// invoked by a contract.
-                {"4", fn get_current_contract() -> Object }
-
+                {"3", fn get_current_contract() -> Object }
                 /// Return the protocol version of the current ledger as a u32.
-                {"5", fn get_ledger_version() -> RawVal }
+                {"4", fn get_ledger_version() -> RawVal }
                 /// Return the sequence number of the current ledger as a u32.
-                {"6", fn get_ledger_sequence() -> RawVal }
+                {"5", fn get_ledger_sequence() -> RawVal }
                 /// Return the timestamp number of the current ledger as a u64.
-                {"7", fn get_ledger_timestamp() -> Object }
+                {"6", fn get_ledger_timestamp() -> Object }
                 /// Return the network identity of the current ledger as bytes.
-                {"8", fn get_ledger_network_id() -> Object }
+                {"7", fn get_ledger_network_id() -> Object }
             }
 
             mod u64 "u" {
diff --git a/soroban-env-common/src/meta.rs b/soroban-env-common/src/meta.rs
index f01616c6..81cd881c 100644
--- a/soroban-env-common/src/meta.rs
+++ b/soroban-env-common/src/meta.rs
@@ -43,5 +43,5 @@
 // implementations over a long period of time.
 
 soroban_env_macros::generate_env_meta_consts!(
-    interface_version: 4,
+    interface_version: 5,
 );
diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs
index 5ada5037..ba7032d6 100644
--- a/soroban-env-host/src/budget.rs
+++ b/soroban-env-host/src/budget.rs
@@ -11,14 +11,15 @@ pub enum CostType {
     WasmInsnExec = 4,
     WasmMemAlloc = 5,
     HostEventDebug = 6,
-    HostFunction = 7,
-    VisitObject = 8,
-    PushFrame = 9,
-    PopFrame = 10,
+    HostEventContract = 7,
+    HostFunction = 8,
+    VisitObject = 9,
+    PushFrame = 10,
+    PopFrame = 11,
     // Tracks a single Val (RawVal or primative Object like U64) <=> ScVal
     // conversion cost. Most of these Val counterparts in ScVal (except e.g.
     // Symbol) consumes a single int64 and therefore is a constant overhead.
-    ValXdrConv = 11,
+    ValXdrConv = 12,
 }
 
 // TODO: add XDR support for iterating over all the elements of an enum
@@ -32,6 +33,7 @@ impl CostType {
             CostType::WasmInsnExec,
             CostType::WasmMemAlloc,
             CostType::HostEventDebug,
+            CostType::HostEventContract,
             CostType::HostFunction,
             CostType::VisitObject,
             CostType::PushFrame,
diff --git a/soroban-env-host/src/events.rs b/soroban-env-host/src/events.rs
index 8bc99f4c..7790fb20 100644
--- a/soroban-env-host/src/events.rs
+++ b/soroban-env-host/src/events.rs
@@ -1,6 +1,6 @@
 use std::fmt::Display;
 
-use crate::{xdr, RawVal, Status};
+use crate::{xdr, xdr::ContractEvent, RawVal, Status};
 #[cfg(feature = "vm")]
 use crate::{
     xdr::{ScUnknownErrorCode, ScVmErrorCode},
@@ -9,18 +9,22 @@ use crate::{
 use log::debug;
 use tinyvec::TinyVec;
 
-// TODO: update this when ContractEvent shows up in the XDR defns.
 // TODO: optimize storage on this to use pools / bumpalo / etc.
 #[derive(Clone, Debug)]
 pub enum HostEvent {
-    #[allow(dead_code)]
-    Contract(/*ContractEvent*/),
+    Contract(ContractEvent),
     Debug(DebugEvent),
 }
 
 #[derive(Clone, Debug, Default)]
 pub struct Events(pub Vec<HostEvent>);
 
+// Maximum number of topics in a `ContractEvent`. This applies to both
+// `Contract` and `System` types of contract events.
+pub(crate) const CONTRACT_EVENT_TOPICS_LIMIT: usize = 4;
+// Maximum number of bytes in a topic binary.
+pub(crate) const TOPIC_BYTES_LENGTH_LIMIT: usize = 32;
+
 impl Events {
     // Records the smallest variant of a debug HostEvent it can, returning the size of the
     // in_args slice (for charging to a budget).
@@ -30,10 +34,15 @@ impl Events {
         len as u64
     }
 
+    // Records a contract HostEvent.
+    pub fn record_contract_event(&mut self, ce: ContractEvent) {
+        self.0.push(HostEvent::Contract(ce))
+    }
+
     pub fn dump_to_debug_log(&self) {
         for e in self.0.iter() {
             match e {
-                HostEvent::Contract() => debug!("Contract event: <TBD>"),
+                HostEvent::Contract(e) => debug!("Contract event: {:?}", e),
                 HostEvent::Debug(e) => debug!("Debug event: {}", e),
             }
         }
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index eb41ab51..a6868e2b 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -9,10 +9,13 @@ use num_bigint::{BigInt, Sign};
 use num_integer::Integer;
 use num_traits::cast::ToPrimitive;
 use num_traits::{Pow, Signed, Zero};
-use soroban_env_common::{EnvVal, TryConvert, TryFromVal, TryIntoVal};
+use soroban_env_common::{EnvVal, TryConvert, TryFromVal, TryIntoVal, OK};
 use std::ops::{Add, BitAnd, BitOr, BitXor, Mul, Neg, Not, Rem, Shl, Shr, Sub};
 
-use soroban_env_common::xdr::{AccountId, Hash, PublicKey, ReadXdr, ThresholdIndexes, WriteXdr};
+use soroban_env_common::xdr::{
+    AccountId, ContractEvent, ContractEventBody, ContractEventType, ContractEventV0,
+    ExtensionPoint, Hash, PublicKey, ReadXdr, ThresholdIndexes, WriteXdr,
+};
 
 use crate::budget::{Budget, CostType};
 use crate::events::{DebugError, DebugEvent, Events};
@@ -208,7 +211,7 @@ impl Host {
     /// _represented_ some other error. This function only returns Err(...) when
     /// there was a failure to record the event, such as when budget is
     /// exceeded.
-    pub fn debug_event<T>(&self, src: T) -> Result<(), HostError>
+    pub fn record_debug_event<T>(&self, src: T) -> Result<(), HostError>
     where
         DebugEvent: From<T>,
     {
@@ -220,11 +223,30 @@ impl Host {
         // contract runs out of gas in; this is an atomic action from the
         // contract's perspective.
         let event: DebugEvent = src.into();
-        let len = event.args.len() as u64;
-        self.get_events_mut(|events| Ok(events.record_debug_event(event)))?;
+        let len = self.get_events_mut(|events| Ok(events.record_debug_event(event)))?;
         self.charge_budget(CostType::HostEventDebug, len)
     }
 
+    // Records a contract event.
+    pub fn record_contract_event(
+        &self,
+        type_: ContractEventType,
+        topics: ScVec,
+        data: ScVal,
+    ) -> Result<(), HostError> {
+        let ce = ContractEvent {
+            ext: ExtensionPoint::V0,
+            contract_id: self.get_current_contract_id().ok(),
+            type_,
+            body: ContractEventBody::V0(ContractEventV0 { topics, data }),
+        };
+        self.get_events_mut(|events| Ok(events.record_contract_event(ce)))?;
+        // Notes on metering: the length of topics and the complexity of data
+        // have been covered by various `ValXdrConv` charges. Here we charge 1
+        // unit just for recording this event.
+        self.charge_budget(CostType::HostEventDebug, 1)
+    }
+
     pub(crate) fn visit_storage<F, U>(&self, f: F) -> Result<U, HostError>
     where
         F: FnOnce(&mut Storage) -> Result<U, HostError>,
@@ -232,9 +254,18 @@ impl Host {
         f(&mut *self.0.storage.borrow_mut())
     }
 
-    pub fn recover_storage(self) -> Result<Storage, Self> {
+    /// Accept a _unique_ (refcount = 1) host reference and destroy the
+    /// underlying [`HostImpl`], returning its constituent components to the
+    /// caller as a tuple wrapped in `Ok(...)`. If the provided host reference
+    /// is not unique, returns `Err(self)`.
+    pub fn try_finish(self) -> Result<(Storage, Budget, Events), Self> {
         Rc::try_unwrap(self.0)
-            .map(|host_impl| host_impl.storage.into_inner())
+            .map(|host_impl| {
+                let storage = host_impl.storage.into_inner();
+                let budget = host_impl.budget.into_inner();
+                let events = host_impl.events.into_inner();
+                (storage, budget, events)
+            })
             .map_err(Host)
     }
 
@@ -446,18 +477,7 @@ impl Host {
                         HostObject::Bin(b) => {
                             Ok(ScObject::Bytes(self.map_err(b.clone().try_into())?))
                         }
-                        HostObject::BigInt(bi) => {
-                            let (sign, data) = bi.to_bytes_be();
-                            match sign {
-                                Sign::Minus => Ok(ScObject::BigInt(ScBigInt::Negative(
-                                    self.map_err(data.try_into())?,
-                                ))),
-                                Sign::NoSign => Ok(ScObject::BigInt(ScBigInt::Zero)),
-                                Sign::Plus => Ok(ScObject::BigInt(ScBigInt::Positive(
-                                    self.map_err(data.try_into())?,
-                                ))),
-                            }
-                        }
+                        HostObject::BigInt(bi) => self.scobj_from_bigint(bi),
                         HostObject::Hash(_) => todo!(),
                         HostObject::PublicKey(_) => todo!(),
                     },
@@ -693,6 +713,16 @@ impl Host {
             ScContractCode::Wasm(contract_wasm.try_into().map_err(|_| self.err_general(""))?);
         self.create_contract_with_id(contract_code, contract_id)
     }
+
+    /// Records a `System` contract event. `topics` is expected to be a `SCVec`
+    /// with length <= 4 that cannot contain Vecs, Maps, or Binaries > 32 bytes
+    /// On succes, returns an `SCStatus::Ok`.
+    pub fn system_event(&self, topics: Object, data: RawVal) -> Result<RawVal, HostError> {
+        let topics = self.event_topics_from_host_obj(topics)?;
+        let data = self.from_host_val(data)?;
+        self.record_contract_event(ContractEventType::System, topics, data)?;
+        Ok(OK.into())
+    }
 }
 
 impl EnvBase for Host {
@@ -785,17 +815,17 @@ impl EnvBase for Host {
     }
 
     fn log_static_fmt_val(&self, fmt: &'static str, v: RawVal) {
-        self.debug_event(DebugEvent::new().msg(fmt).arg(v))
+        self.record_debug_event(DebugEvent::new().msg(fmt).arg(v))
             .expect("unable to record debug event")
     }
 
     fn log_static_fmt_static_str(&self, fmt: &'static str, s: &'static str) {
-        self.debug_event(DebugEvent::new().msg(fmt).arg(s))
+        self.record_debug_event(DebugEvent::new().msg(fmt).arg(s))
             .expect("unable to record debug event")
     }
 
     fn log_static_fmt_val_static_str(&self, fmt: &'static str, v: RawVal, s: &'static str) {
-        self.debug_event(DebugEvent::new().msg(fmt).arg(v).arg(s))
+        self.record_debug_event(DebugEvent::new().msg(fmt).arg(v).arg(s))
             .expect("unable to record debug event")
     }
 
@@ -807,7 +837,8 @@ impl EnvBase for Host {
         for s in strs {
             evt = evt.arg(*s)
         }
-        self.debug_event(evt).expect("unable to record debug event")
+        self.record_debug_event(evt)
+            .expect("unable to record debug event")
     }
 }
 
@@ -815,7 +846,7 @@ impl CheckedEnv for Host {
     type Error = HostError;
 
     fn log_value(&self, v: RawVal) -> Result<RawVal, HostError> {
-        self.debug_event(DebugEvent::new().msg("log").arg(v))?;
+        self.record_debug_event(DebugEvent::new().msg("log").arg(v))?;
         Ok(RawVal::from_void())
     }
 
@@ -852,12 +883,11 @@ impl CheckedEnv for Host {
         })
     }
 
-    fn contract_event(&self, v: RawVal) -> Result<RawVal, HostError> {
-        todo!()
-    }
-
-    fn system_event(&self, v: RawVal) -> Result<RawVal, HostError> {
-        todo!()
+    fn contract_event(&self, topics: Object, data: RawVal) -> Result<RawVal, HostError> {
+        let topics = self.event_topics_from_host_obj(topics)?;
+        let data = self.from_host_val(data)?;
+        self.record_contract_event(ContractEventType::Contract, topics, data)?;
+        Ok(OK.into())
     }
 
     fn get_current_contract(&self) -> Result<Object, HostError> {
@@ -1263,7 +1293,7 @@ impl CheckedEnv for Host {
                 let evt = DebugEvent::new()
                     .msg("try_call got error from callee contract")
                     .arg::<RawVal>(e.status.clone().into());
-                self.debug_event(evt)?;
+                self.record_debug_event(evt)?;
                 Ok(e.status.into())
             }
         }
diff --git a/soroban-env-host/src/host/conversion.rs b/soroban-env-host/src/host/conversion.rs
index db29311a..9aab367a 100644
--- a/soroban-env-host/src/host/conversion.rs
+++ b/soroban-env-host/src/host/conversion.rs
@@ -1,9 +1,16 @@
-use crate::events::DebugError;
-use crate::xdr::{ScHostFnErrorCode, ScHostObjErrorCode, ScHostValErrorCode};
-use crate::{Host, HostError, Object, RawVal};
+use crate::xdr::{
+    Hash, LedgerKey, LedgerKeyContractData, ScBigInt, ScHostFnErrorCode, ScHostObjErrorCode,
+    ScHostValErrorCode, ScObject, ScStatic, ScVal, ScVec, Uint256,
+};
+use crate::{
+    budget::CostType,
+    events::{DebugError, CONTRACT_EVENT_TOPICS_LIMIT, TOPIC_BYTES_LENGTH_LIMIT},
+    host_object::HostObject,
+    Host, HostError, Object, RawVal, Tag,
+};
 use ed25519_dalek::{PublicKey, Signature, SIGNATURE_LENGTH};
+use num_bigint::{BigInt, Sign};
 use sha2::{Digest, Sha256};
-use soroban_env_common::xdr::{Hash, LedgerKey, LedgerKeyContractData, ScStatic, ScVal, Uint256};
 
 impl Host {
     pub(crate) fn usize_to_u32(&self, u: usize, msg: &'static str) -> Result<u32, HostError> {
@@ -150,4 +157,90 @@ impl Host {
         }
         self.storage_key_from_rawval(k)
     }
+
+    // TODO: impl a `TryFrom` trait once the "metered_" class is ready
+    pub(crate) fn scobj_from_bigint(&self, bi: &BigInt) -> Result<ScObject, HostError> {
+        let (sign, data) = bi.to_bytes_be();
+        match sign {
+            Sign::Minus => Ok(ScObject::BigInt(ScBigInt::Negative(
+                self.map_err(data.try_into())?,
+            ))),
+            Sign::NoSign => Ok(ScObject::BigInt(ScBigInt::Zero)),
+            Sign::Plus => Ok(ScObject::BigInt(ScBigInt::Positive(
+                self.map_err(data.try_into())?,
+            ))),
+        }
+    }
+
+    fn event_topic_from_rawval(&self, topic: RawVal) -> Result<ScVal, HostError> {
+        self.charge_budget(CostType::ValXdrConv, 1)?;
+        if topic.is_u63() {
+            Ok(ScVal::U63(unsafe { topic.unchecked_as_u63() }))
+        } else {
+            match topic.get_tag() {
+                Tag::Object => {
+                    unsafe {
+                        self.unchecked_visit_val_obj(topic, |ob| {
+                            // charge budget
+                            let sco = match ob {
+                                None => Err(self.err_status(ScHostObjErrorCode::UnknownReference)),
+                                Some(ho) => match ho {
+                                    // TODO: use more event-specific error codes than `UnexpectedType`
+                                    HostObject::Vec(_) => {
+                                        Err(self.err_status(ScHostObjErrorCode::UnexpectedType))
+                                    }
+                                    // TODO: use more event-specific error codes than `UnexpectedType`
+                                    HostObject::Map(_) => {
+                                        Err(self.err_status(ScHostObjErrorCode::UnexpectedType))
+                                    }
+                                    HostObject::Bin(b) => {
+                                        if b.len() > TOPIC_BYTES_LENGTH_LIMIT {
+                                            // TODO: use more event-specific error codes than `UnexpectedType`.
+                                            // Something like "topic binary exceeds length limit"
+                                            return Err(
+                                                self.err_status(ScHostObjErrorCode::UnexpectedType)
+                                            );
+                                        }
+                                        Ok(ScObject::Bytes(self.map_err(b.clone().try_into())?))
+                                    }
+                                    HostObject::U64(u) => Ok(ScObject::U64(*u)),
+                                    HostObject::I64(i) => Ok(ScObject::I64(*i)),
+                                    HostObject::BigInt(bi) => self.scobj_from_bigint(bi),
+                                    HostObject::Hash(_) => todo!(),
+                                    HostObject::PublicKey(_) => todo!(),
+                                },
+                            }?;
+                            Ok(ScVal::Object(Some(sco)))
+                        })
+                    }
+                }
+                _ => self.from_host_val(topic),
+            }
+        }
+    }
+
+    pub(crate) fn event_topics_from_host_obj(&self, topics: Object) -> Result<ScVec, HostError> {
+        unsafe {
+            self.unchecked_visit_val_obj(topics.into(), |ob| {
+                self.charge_budget(CostType::ValXdrConv, 1)?;
+                match ob {
+                    None => Err(self.err_status(ScHostObjErrorCode::UnknownReference)),
+                    Some(ho) => match ho {
+                        HostObject::Vec(vv) => {
+                            if vv.len() > CONTRACT_EVENT_TOPICS_LIMIT {
+                                // TODO: proper error code "event topics exceeds count limit"
+                                return Err(self.err_status(ScHostObjErrorCode::UnknownError));
+                            }
+                            let mut sv = Vec::new();
+                            for e in vv.iter() {
+                                sv.push(self.event_topic_from_rawval(e.val)?);
+                            }
+                            Ok(ScVec(self.map_err(sv.try_into())?))
+                        }
+                        _ => Err(self.err_status(ScHostObjErrorCode::UnexpectedType)),
+                    },
+                }
+            })
+        }
+    }
 }
diff --git a/soroban-env-host/src/host/err_helper.rs b/soroban-env-host/src/host/err_helper.rs
index 02fd08d8..5914179f 100644
--- a/soroban-env-host/src/host/err_helper.rs
+++ b/soroban-env-host/src/host/err_helper.rs
@@ -12,7 +12,7 @@ impl Host {
         DebugError: From<T>,
     {
         let ds: DebugError = src.into();
-        if let Err(e) = self.debug_event(ds.event) {
+        if let Err(e) = self.record_debug_event(ds.event) {
             e
         } else {
             let mut he: HostError = ds.status.into();


Test patch:
diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs
index a7fbd927..80e62445 100644
--- a/soroban-env-host/src/test.rs
+++ b/soroban-env-host/src/test.rs
@@ -9,6 +9,8 @@ mod vec;
 
 #[cfg(feature = "vm")]
 mod budget_metering;
+#[cfg(feature = "testutils")]
+mod contract_event;
 #[cfg(feature = "vm")]
 mod invocation;
 #[cfg(all(feature = "vm", feature = "testutils"))]
diff --git a/soroban-env-host/src/test/contract_event.rs b/soroban-env-host/src/test/contract_event.rs
new file mode 100644
index 00000000..268f6a2e
--- /dev/null
+++ b/soroban-env-host/src/test/contract_event.rs
@@ -0,0 +1,67 @@
+use crate::{
+    events::HostEvent,
+    xdr::{
+        ContractEvent, ContractEventBody, ContractEventType, ContractEventV0, ExtensionPoint, Hash,
+        ScMap, ScMapEntry, ScObject::Map, ScVal,
+    },
+    ContractFunctionSet, Env, EnvBase, Host, HostError, RawVal, Symbol, OK,
+};
+use std::rc::Rc;
+
+pub struct EmptyContractWithEvents;
+
+impl ContractFunctionSet for EmptyContractWithEvents {
+    fn call(&self, _func: &Symbol, host: &Host, _args: &[RawVal]) -> Option<RawVal> {
+        // Add a contract event
+        let mut data = host.map_new();
+        data = host.map_put(data, 1_u32.into(), 2_u32.into());
+        let mut topics = host.vec_new(().into());
+        topics = host.vec_push(topics, 0u32.into());
+        topics = host.vec_push(topics, 1u32.into());
+        Some(host.contract_event(topics, data.to_raw()))
+    }
+}
+
+#[test]
+fn contract_event() -> Result<(), HostError> {
+    let host = Host::default();
+    let dummy_id = [0; 32];
+    let mut id = host.binary_new();
+    id = host.binary_copy_from_slice(id, 0u32.into(), &dummy_id);
+    let test_contract = Rc::new(EmptyContractWithEvents {});
+    let sym = Symbol::from_str("add");
+    let args = host.test_vec_obj::<i32>(&[1, 2])?;
+    host.register_test_contract(id, test_contract)?;
+    assert_eq!(
+        host.call(id, sym.into(), args.into()).get_payload(),
+        OK.to_raw().get_payload()
+    );
+
+    let event_ref = ContractEvent {
+        ext: ExtensionPoint::V0,
+        contract_id: Some(Hash(dummy_id)),
+        type_: ContractEventType::Contract,
+        body: ContractEventBody::V0(ContractEventV0 {
+            topics: host.map_err(vec![ScVal::U32(0), ScVal::U32(1)].try_into())?,
+            data: ScVal::Object(Some(Map(host.map_err(ScMap::try_from(vec![
+                ScMapEntry {
+                    key: ScVal::U32(1),
+                    val: ScVal::U32(2),
+                },
+            ]))?))),
+        }),
+    };
+
+    // Fish out the last contract event and check that it is
+    // correct, and formats as expected.
+    let events = host.get_events();
+    match events.0.last() {
+        Some(HostEvent::Contract(ce)) => {
+            assert_eq!(*ce, event_ref)
+        }
+        _ => {
+            panic!("missing contract event")
+        }
+    };
+    Ok(())
+}