use super::trace::ExecutionEvent;
use crate::shim::{
econ::TokenAmount, fvm_shared_latest::ActorID, fvm_shared_latest::error::ExitCode,
};
use crate::utils::get_size::{GetSize, vec_heap_size_with_fn_helper};
use cid::Cid;
use fil_actors_shared::fvm_ipld_amt::{Amt, Amtv0};
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::RawBytes;
use fvm_shared2::receipt::Receipt as Receipt_v2;
use fvm_shared3::event::ActorEvent as ActorEvent_v3;
use fvm_shared3::event::Entry as Entry_v3;
use fvm_shared3::event::StampedEvent as StampedEvent_v3;
pub use fvm_shared3::receipt::Receipt as Receipt_v3;
use fvm_shared4::event::ActorEvent as ActorEvent_v4;
use fvm_shared4::event::Entry as Entry_v4;
use fvm_shared4::event::StampedEvent as StampedEvent_v4;
use fvm_shared4::receipt::Receipt as Receipt_v4;
use fvm2::executor::ApplyRet as ApplyRet_v2;
use fvm3::executor::ApplyRet as ApplyRet_v3;
use fvm4::executor::ApplyRet as ApplyRet_v4;
use serde::Serialize;
use spire_enum::prelude::delegated_enum;
use std::borrow::Borrow as _;
#[delegated_enum(impl_conversions)]
#[derive(Clone, Debug)]
pub enum ApplyRet {
V2(ApplyRet_v2),
V3(ApplyRet_v3),
V4(ApplyRet_v4),
}
impl ApplyRet {
pub fn failure_info(&self) -> Option<String> {
delegate_apply_ret!(self => |r| r.failure_info.as_ref().map(|failure| format!("{failure} (RetCode={})", self.exit_code())))
}
pub fn miner_tip(&self) -> TokenAmount {
delegate_apply_ret!(self.miner_tip.borrow().into())
}
pub fn penalty(&self) -> TokenAmount {
delegate_apply_ret!(self.penalty.borrow().into())
}
pub fn msg_receipt(&self) -> Receipt {
delegate_apply_ret!(self.msg_receipt.clone().into())
}
pub fn return_data(&self) -> RawBytes {
delegate_apply_ret!(self => |r| r.msg_receipt.return_data.clone())
}
pub fn exit_code(&self) -> ExitCode {
ExitCode::new(delegate_apply_ret!(self => |r| r.msg_receipt.exit_code.value()))
}
pub fn gas_used(&self) -> u64 {
match self {
ApplyRet::V2(v2) => v2.msg_receipt.gas_used as u64,
ApplyRet::V3(v3) => v3.msg_receipt.gas_used,
ApplyRet::V4(v4) => v4.msg_receipt.gas_used,
}
}
pub fn refund(&self) -> TokenAmount {
delegate_apply_ret!(self.refund.borrow().into())
}
pub fn base_fee_burn(&self) -> TokenAmount {
delegate_apply_ret!(self.base_fee_burn.borrow().into())
}
pub fn over_estimation_burn(&self) -> TokenAmount {
delegate_apply_ret!(self.over_estimation_burn.borrow().into())
}
pub fn trace_has_call_return_exit_code(&self, code: ExitCode) -> bool {
let want = code.value();
match self {
ApplyRet::V2(_) => false,
ApplyRet::V3(r) => r.exec_trace.iter().any(
|e| matches!(e, fvm3::trace::ExecutionEvent::CallReturn(c, _) if c.value() == want),
),
ApplyRet::V4(r) => r.exec_trace.iter().any(
|e| matches!(e, fvm4::trace::ExecutionEvent::CallReturn(c, _) if c.value() == want),
),
}
}
pub fn into_exec_trace(self) -> Vec<ExecutionEvent> {
delegate_apply_ret!(self => |r| r.exec_trace.into_iter().map(Into::into).collect())
}
pub fn exec_trace(&self) -> Vec<ExecutionEvent> {
delegate_apply_ret!(self => |r| r.exec_trace.iter().cloned().map(Into::into).collect())
}
pub fn into_receipt_and_events(self) -> (Receipt, Option<Vec<StampedEvent>>) {
match self {
ApplyRet::V2(v2) => (v2.msg_receipt.into(), None),
ApplyRet::V3(v3) => {
let events = v3
.msg_receipt
.events_root
.is_some()
.then(|| v3.events.into_iter().map(Into::into).collect());
(v3.msg_receipt.into(), events)
}
ApplyRet::V4(v4) => {
let events = v4
.msg_receipt
.events_root
.is_some()
.then(|| v4.events.into_iter().map(Into::into).collect());
(v4.msg_receipt.into(), events)
}
}
}
}
#[delegated_enum(impl_conversions)]
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum Receipt {
V2(Receipt_v2),
V3(Receipt_v3),
V4(Receipt_v4),
}
impl GetSize for Receipt {
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
delegate_receipt!(self.return_data.bytes().get_heap_size_with_tracker(tracker))
}
}
impl PartialEq for Receipt {
fn eq(&self, other: &Self) -> bool {
self.exit_code() == other.exit_code()
&& self.return_data() == other.return_data()
&& self.gas_used() == other.gas_used()
&& self.events_root() == other.events_root()
}
}
impl Receipt {
#[cfg(test)]
pub fn empty_success() -> Self {
Receipt::V4(Receipt_v4 {
exit_code: fvm_shared4::error::ExitCode::OK,
return_data: Default::default(),
gas_used: 0,
events_root: None,
})
}
pub fn exit_code(&self) -> ExitCode {
match self {
Receipt::V2(v2) => ExitCode::new(v2.exit_code.value()),
Receipt::V3(v3) => ExitCode::new(v3.exit_code.value()),
Receipt::V4(v4) => v4.exit_code,
}
}
pub fn return_data(&self) -> RawBytes {
delegate_receipt!(self.return_data.clone())
}
pub fn gas_used(&self) -> u64 {
match self {
Receipt::V2(v2) => v2.gas_used as u64,
Receipt::V3(v3) => v3.gas_used,
Receipt::V4(v4) => v4.gas_used,
}
}
pub fn events_root(&self) -> Option<Cid> {
match self {
Receipt::V2(_) => None,
Receipt::V3(v3) => v3.events_root,
Receipt::V4(v4) => v4.events_root,
}
}
pub fn get_receipt(
db: &impl Blockstore,
receipts: &Cid,
i: u64,
) -> anyhow::Result<Option<Self>> {
if let Ok(amt) = Amtv0::load(receipts, db)
&& let Ok(receipts) = amt.get(i)
{
return Ok(receipts.cloned().map(Receipt::V4));
}
let amt = Amtv0::load(receipts, db)?;
let receipts = amt.get(i)?;
Ok(receipts.cloned().map(Receipt::V2))
}
pub fn get_receipts(db: &impl Blockstore, receipts_cid: Cid) -> anyhow::Result<Vec<Receipt>> {
let mut receipts = Vec::new();
if let Ok(amt) = Amtv0::<fvm_shared4::receipt::Receipt, _>::load(&receipts_cid, db) {
amt.for_each_cacheless(|_, receipt| {
receipts.push(Receipt::V4(receipt.clone()));
Ok(())
})?;
} else {
let amt = Amtv0::<fvm_shared2::receipt::Receipt, _>::load(&receipts_cid, db)?;
amt.for_each_cacheless(|_, receipt| {
receipts.push(Receipt::V2(receipt.clone()));
Ok(())
})?;
}
Ok(receipts)
}
}
#[delegated_enum(impl_conversions)]
#[derive(Clone, Debug)]
pub enum Entry {
V3(Entry_v3),
V4(Entry_v4),
}
impl Entry {
#[cfg(test)]
pub fn new(
flags: crate::shim::fvm_shared_latest::event::Flags,
key: String,
codec: u64,
value: Vec<u8>,
) -> Self {
Entry::V4(Entry_v4 {
flags,
key,
codec,
value,
})
}
pub fn into_parts(self) -> (u64, String, u64, Vec<u8>) {
delegate_entry!(self => |e| (e.flags.bits(), e.key, e.codec, e.value))
}
pub fn value(&self) -> &Vec<u8> {
delegate_entry!(self.value.borrow())
}
pub fn codec(&self) -> u64 {
delegate_entry!(self.codec)
}
pub fn key(&self) -> &String {
delegate_entry!(self.key.borrow())
}
}
#[delegated_enum(impl_conversions)]
#[derive(Clone, Debug)]
pub enum ActorEvent {
V3(ActorEvent_v3),
V4(ActorEvent_v4),
}
#[delegated_enum(impl_conversions)]
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum StampedEvent {
V3(StampedEvent_v3),
V4(StampedEvent_v4),
}
impl GetSize for StampedEvent {
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
delegate_stamped_event!(self => |e| vec_heap_size_with_fn_helper(&e.event.entries, tracker, |e, mut tr| {
(e.key.get_heap_size_with_tracker(&mut tr).0 + e.value.get_heap_size_with_tracker(&mut tr).0, tr)
}))
}
}
impl StampedEvent {
#[cfg(test)]
pub fn new_indexed(emitter: ActorID, key: &str) -> Self {
Self::V4(create_raw_event_v4(emitter, key))
}
pub fn emitter(&self) -> ActorID {
delegate_stamped_event!(self.emitter)
}
pub fn entries(&self) -> Vec<Entry> {
delegate_stamped_event!(self => |e| e.event.entries.iter().cloned().map(Into::into).collect())
}
pub fn into_entries(self) -> Vec<Entry> {
match self {
StampedEvent::V3(e) => e.event.entries.into_iter().map(Into::into).collect(),
StampedEvent::V4(e) => e.event.entries.into_iter().map(Into::into).collect(),
}
}
pub fn get_events<DB: Blockstore>(
db: &DB,
events_root: &Cid,
) -> anyhow::Result<Vec<StampedEvent>> {
let mut events = Vec::new();
if let Ok(amt) = Amt::<StampedEvent_v4, _>::load(events_root, db) {
amt.for_each_cacheless(|_, event| {
events.push(StampedEvent::V4(event.clone()));
Ok(())
})?;
} else {
let amt = Amt::<StampedEvent_v3, _>::load(events_root, db)?;
amt.for_each_cacheless(|_, event| {
events.push(StampedEvent::V3(event.clone()));
Ok(())
})?;
}
Ok(events)
}
}
#[cfg(test)]
pub(crate) fn create_raw_event_v4(emitter: u64, key: &str) -> fvm_shared4::event::StampedEvent {
fvm_shared4::event::StampedEvent {
emitter,
event: fvm_shared4::event::ActorEvent {
entries: vec![fvm_shared4::event::Entry {
flags: fvm_shared4::event::Flags::FLAG_INDEXED_ALL,
key: key.to_string(),
codec: fvm_ipld_encoding::IPLD_RAW,
value: key.as_bytes().to_vec(),
}],
},
}
}
#[cfg(test)]
pub(crate) fn create_raw_event_v3(emitter: u64, key: &str) -> fvm_shared3::event::StampedEvent {
fvm_shared3::event::StampedEvent {
emitter,
event: fvm_shared3::event::ActorEvent {
entries: vec![fvm_shared3::event::Entry {
flags: fvm_shared3::event::Flags::FLAG_INDEXED_ALL,
key: key.to_string(),
codec: fvm_ipld_encoding::IPLD_RAW,
value: key.as_bytes().to_vec(),
}],
},
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Receipt {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
#[derive(derive_quickcheck_arbitrary::Arbitrary, Clone)]
enum Helper {
V2 {
exit_code: u32,
return_data: Vec<u8>,
gas_used: i64,
},
V3 {
exit_code: u32,
return_data: Vec<u8>,
gas_used: u64,
events_root: Option<::cid::Cid>,
},
V4 {
exit_code: u32,
return_data: Vec<u8>,
gas_used: u64,
events_root: Option<::cid::Cid>,
},
}
match Helper::arbitrary(g) {
Helper::V2 {
exit_code,
return_data,
gas_used,
} => Self::V2(Receipt_v2 {
exit_code: exit_code.into(),
return_data: return_data.into(),
gas_used,
}),
Helper::V3 {
exit_code,
return_data,
gas_used,
events_root,
} => Self::V3(Receipt_v3 {
exit_code: exit_code.into(),
return_data: return_data.into(),
gas_used,
events_root,
}),
Helper::V4 {
exit_code,
return_data,
gas_used,
events_root,
} => Self::V4(Receipt_v4 {
exit_code: exit_code.into(),
return_data: return_data.into(),
gas_used,
events_root,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fvm4::trace::ExecutionEvent as E4;
use quickcheck_macros::quickcheck;
#[quickcheck]
fn receipt_cbor_serde_serialize(receipt: Receipt) {
let encoded = fvm_ipld_encoding::to_vec(&receipt).unwrap();
let encoded2 = match &receipt {
Receipt::V2(v) => fvm_ipld_encoding::to_vec(v),
Receipt::V3(v) => fvm_ipld_encoding::to_vec(v),
Receipt::V4(v) => fvm_ipld_encoding::to_vec(v),
}
.unwrap();
assert_eq!(encoded, encoded2);
}
macro_rules! apply_ret {
($variant:ident, $inner:ident, $receipt:expr, $exec_trace:expr $(, $ef:ident: $ev:expr)*) => {
ApplyRet::$variant($inner {
msg_receipt: $receipt,
penalty: Default::default(),
miner_tip: Default::default(),
base_fee_burn: Default::default(),
over_estimation_burn: Default::default(),
refund: Default::default(),
gas_refund: 0,
gas_burned: 0,
failure_info: None,
exec_trace: $exec_trace,
$($ef: $ev,)*
})
};
}
#[test]
fn trace_has_call_return_exit_code_across_versions() {
let want = ExitCode::SYS_OUT_OF_GAS;
let miss = ExitCode::new(33);
let v4 = |t| apply_ret!(V4, ApplyRet_v4, Receipt_v4 { exit_code: ExitCode::OK, return_data: RawBytes::default(), gas_used: 0, events_root: None }, t, events: vec![], return_codec: None);
assert!(v4(vec![E4::CallReturn(want, None)]).trace_has_call_return_exit_code(want));
assert!(!v4(vec![E4::CallReturn(want, None)]).trace_has_call_return_exit_code(miss));
assert!(
!v4(vec![E4::CallReturn(ExitCode::OK, None)]).trace_has_call_return_exit_code(want)
);
assert!(!v4(vec![]).trace_has_call_return_exit_code(want));
use fvm3::trace::ExecutionEvent as E3;
let v3 = |t| apply_ret!(V3, ApplyRet_v3, Receipt_v3 { exit_code: fvm_shared3::error::ExitCode::OK, return_data: RawBytes::default(), gas_used: 0, events_root: None }, t, events: vec![]);
let oog3 = fvm_shared3::error::ExitCode::SYS_OUT_OF_GAS;
assert!(v3(vec![E3::CallReturn(oog3, None)]).trace_has_call_return_exit_code(want));
assert!(!v3(vec![E3::CallReturn(oog3, None)]).trace_has_call_return_exit_code(miss));
use fvm2::trace::ExecutionEvent as E2;
let v2 = apply_ret!(
V2,
ApplyRet_v2,
Receipt_v2 {
exit_code: fvm_shared2::error::ExitCode::OK,
return_data: RawBytes::default(),
gas_used: 0
},
vec![E2::CallReturn(RawBytes::default())]
);
assert!(!v2.trace_has_call_return_exit_code(want));
}
}