radix_engine/system/system_modules/execution_trace/
module.rs

1use crate::blueprints::resource::*;
2use crate::errors::*;
3use crate::internal_prelude::*;
4use crate::kernel::call_frame::CallFrameMessage;
5use crate::kernel::kernel_api::*;
6use crate::kernel::kernel_callback_api::*;
7use crate::system::actor::{Actor, FunctionActor, MethodActor};
8use crate::system::module::*;
9use crate::system::system_callback::*;
10use crate::system::system_callback_api::SystemCallbackObject;
11use crate::system::type_info::TypeInfoSubstate;
12use crate::transaction::{FeeLocks, TransactionExecutionTrace};
13use radix_common::math::Decimal;
14use radix_engine_interface::blueprints::resource::*;
15use sbor::rust::collections::*;
16use sbor::rust::fmt::Debug;
17
18//===================================================================================
19// Note: ExecutionTrace must not produce any error or transactional side effect!
20//===================================================================================
21
22// TODO: Handle potential Decimal arithmetic operation (checked_add, checked_sub) errors instead of panicking.
23// ATM, ExecutionTrace cannot return any errors (as stated above), so it shall be thoroughly
24// designed.
25
26#[derive(Debug, Clone)]
27pub struct ExecutionTraceModule {
28    /// Maximum depth up to which kernel calls are being traced.
29    max_kernel_call_depth_traced: usize,
30
31    /// Current transaction index
32    current_instruction_index: usize,
33
34    /// Current kernel calls depth. Note that this doesn't necessarily correspond to the
35    /// call frame depth, as there can be nested kernel calls within a single call frame
36    /// (e.g. open_substate call inside drop_node).
37    current_kernel_call_depth: usize,
38
39    /// A stack of traced kernel call inputs, their origin, and the instruction index.
40    traced_kernel_call_inputs_stack: Vec<(ResourceSummary, TraceOrigin, usize)>,
41
42    /// A mapping of complete KernelCallTrace stacks (\w both inputs and outputs), indexed by depth.
43    kernel_call_traces_stacks: IndexMap<usize, Vec<ExecutionTrace>>,
44
45    /// Vault operations: (Caller, Vault ID, operation, instruction index)
46    vault_ops: Vec<(TraceActor, NodeId, VaultOp, usize)>,
47}
48
49impl ExecutionTraceModule {
50    pub fn update_instruction_index(&mut self, new_index: usize) {
51        self.current_instruction_index = new_index;
52    }
53}
54
55// This structure tracks changes in resource balances per actor and vault.
56// Its purpose is to record balance changes for each instruction within a transaction
57// (as observed in `TransactionExecutionTrace`).
58// These changes can be further examined using the node's Core API.
59// NOTE!
60// This feature is not a comprehensive abstraction and may be removed in the future once it
61// is deprecated in the node's Core API.
62#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
63pub struct ResourceChange {
64    pub node_id: NodeId,
65    pub vault_id: NodeId,
66    pub resource_address: ResourceAddress,
67    pub amount: Decimal,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
71pub enum WorktopChange {
72    Take(ResourceSpecifier),
73    Put(ResourceSpecifier),
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
77pub enum ResourceSpecifier {
78    Amount(ResourceAddress, Decimal),
79    Ids(ResourceAddress, IndexSet<NonFungibleLocalId>),
80}
81
82impl From<&BucketSnapshot> for ResourceSpecifier {
83    fn from(value: &BucketSnapshot) -> Self {
84        match value {
85            BucketSnapshot::Fungible {
86                resource_address,
87                liquid,
88                ..
89            } => Self::Amount(*resource_address, *liquid),
90            BucketSnapshot::NonFungible {
91                resource_address,
92                liquid,
93                ..
94            } => Self::Ids(*resource_address, liquid.clone()),
95        }
96    }
97}
98
99// Supported operations on Vaults.
100// The Recall operation is intentionally not supported, as it is impossible to identify the account
101// from which resources are recalled. This is because Recall functions with an internal vault.
102#[derive(Debug, Clone)]
103pub enum VaultOp {
104    Put(ResourceAddress, Decimal),
105    Take(ResourceAddress, Decimal),
106    // We intentionally disregard IDs and only use the amount for non-fungibles.
107    // This maintains backward compatibility for users of the `ResourceChange` struct.
108    TakeNonFungibles(ResourceAddress, Decimal),
109    TakeAdvanced(ResourceAddress, Decimal),
110    LockFee(Decimal, bool),
111}
112
113trait SystemModuleApiResourceSnapshotExtension {
114    fn read_bucket_uncosted(&self, bucket_id: &NodeId) -> Option<BucketSnapshot>;
115    fn read_proof_uncosted(&self, proof_id: &NodeId) -> Option<ProofSnapshot>;
116}
117
118impl<'a, V: SystemCallbackObject, K: KernelInternalApi<System = System<V>>>
119    SystemModuleApiResourceSnapshotExtension for SystemModuleApiImpl<'a, K>
120{
121    fn read_bucket_uncosted(&self, bucket_id: &NodeId) -> Option<BucketSnapshot> {
122        let (is_fungible_bucket, resource_address) = if let Some(substate) =
123            self.api_ref().kernel_read_substate_uncosted(
124                bucket_id,
125                TYPE_INFO_FIELD_PARTITION,
126                &TypeInfoField::TypeInfo.into(),
127            ) {
128            let type_info: TypeInfoSubstate = substate.as_typed().unwrap();
129            match type_info {
130                TypeInfoSubstate::Object(info)
131                    if info.blueprint_info.blueprint_id.package_address == RESOURCE_PACKAGE
132                        && (info.blueprint_info.blueprint_id.blueprint_name
133                            == FUNGIBLE_BUCKET_BLUEPRINT
134                            || info.blueprint_info.blueprint_id.blueprint_name
135                                == NON_FUNGIBLE_BUCKET_BLUEPRINT) =>
136                {
137                    let is_fungible = info
138                        .blueprint_info
139                        .blueprint_id
140                        .blueprint_name
141                        .eq(FUNGIBLE_BUCKET_BLUEPRINT);
142                    let parent = info.get_outer_object();
143                    let resource_address: ResourceAddress =
144                        ResourceAddress::new_or_panic(parent.as_bytes().try_into().unwrap());
145                    (is_fungible, resource_address)
146                }
147                _ => {
148                    return None;
149                }
150            }
151        } else {
152            return None;
153        };
154
155        if is_fungible_bucket {
156            let substate = self
157                .api_ref()
158                .kernel_read_substate_uncosted(
159                    bucket_id,
160                    MAIN_BASE_PARTITION,
161                    &FungibleBucketField::Liquid.into(),
162                )
163                .unwrap();
164            let liquid: FieldSubstate<LiquidFungibleResource> = substate.as_typed().unwrap();
165
166            Some(BucketSnapshot::Fungible {
167                resource_address,
168                liquid: liquid.into_payload().amount(),
169            })
170        } else {
171            let substate = self
172                .api_ref()
173                .kernel_read_substate_uncosted(
174                    bucket_id,
175                    MAIN_BASE_PARTITION,
176                    &NonFungibleBucketField::Liquid.into(),
177                )
178                .unwrap();
179            let liquid: FieldSubstate<LiquidNonFungibleResource> = substate.as_typed().unwrap();
180
181            Some(BucketSnapshot::NonFungible {
182                resource_address,
183                liquid: liquid.into_payload().ids().clone(),
184            })
185        }
186    }
187
188    fn read_proof_uncosted(&self, proof_id: &NodeId) -> Option<ProofSnapshot> {
189        let is_fungible = if let Some(substate) = self.api_ref().kernel_read_substate_uncosted(
190            proof_id,
191            TYPE_INFO_FIELD_PARTITION,
192            &TypeInfoField::TypeInfo.into(),
193        ) {
194            let type_info: TypeInfoSubstate = substate.as_typed().unwrap();
195            match type_info {
196                TypeInfoSubstate::Object(ObjectInfo {
197                    blueprint_info: BlueprintInfo { blueprint_id, .. },
198                    ..
199                }) if blueprint_id.package_address == RESOURCE_PACKAGE
200                    && (blueprint_id.blueprint_name == NON_FUNGIBLE_PROOF_BLUEPRINT
201                        || blueprint_id.blueprint_name == FUNGIBLE_PROOF_BLUEPRINT) =>
202                {
203                    blueprint_id.blueprint_name.eq(FUNGIBLE_PROOF_BLUEPRINT)
204                }
205                _ => {
206                    return None;
207                }
208            }
209        } else {
210            return None;
211        };
212
213        if is_fungible {
214            let substate = self
215                .api_ref()
216                .kernel_read_substate_uncosted(
217                    proof_id,
218                    TYPE_INFO_FIELD_PARTITION,
219                    &TypeInfoField::TypeInfo.into(),
220                )
221                .unwrap();
222            let info: TypeInfoSubstate = substate.as_typed().unwrap();
223            let resource_address =
224                ResourceAddress::new_or_panic(info.outer_object().unwrap().into());
225
226            let substate = self
227                .api_ref()
228                .kernel_read_substate_uncosted(
229                    proof_id,
230                    MAIN_BASE_PARTITION,
231                    &FungibleProofField::ProofRefs.into(),
232                )
233                .unwrap();
234            let proof: FieldSubstate<FungibleProofSubstate> = substate.as_typed().unwrap();
235
236            Some(ProofSnapshot::Fungible {
237                resource_address,
238                total_locked: proof.into_payload().amount(),
239            })
240        } else {
241            let substate = self
242                .api_ref()
243                .kernel_read_substate_uncosted(
244                    proof_id,
245                    TYPE_INFO_FIELD_PARTITION,
246                    &TypeInfoField::TypeInfo.into(),
247                )
248                .unwrap();
249            let info: TypeInfoSubstate = substate.as_typed().unwrap();
250            let resource_address =
251                ResourceAddress::new_or_panic(info.outer_object().unwrap().into());
252
253            let substate = self
254                .api_ref()
255                .kernel_read_substate_uncosted(
256                    proof_id,
257                    MAIN_BASE_PARTITION,
258                    &NonFungibleProofField::ProofRefs.into(),
259                )
260                .unwrap();
261            let proof: FieldSubstate<NonFungibleProofSubstate> = substate.as_typed().unwrap();
262
263            Some(ProofSnapshot::NonFungible {
264                resource_address,
265                total_locked: proof.into_payload().non_fungible_local_ids().clone(),
266            })
267        }
268    }
269}
270
271#[derive(Clone, Debug, PartialEq, Eq, ScryptoSbor)]
272pub enum BucketSnapshot {
273    Fungible {
274        resource_address: ResourceAddress,
275        liquid: Decimal,
276    },
277    NonFungible {
278        resource_address: ResourceAddress,
279        liquid: IndexSet<NonFungibleLocalId>,
280    },
281}
282
283impl BucketSnapshot {
284    pub fn resource_address(&self) -> ResourceAddress {
285        match self {
286            BucketSnapshot::Fungible {
287                resource_address, ..
288            } => *resource_address,
289            BucketSnapshot::NonFungible {
290                resource_address, ..
291            } => *resource_address,
292        }
293    }
294    pub fn amount(&self) -> Decimal {
295        match self {
296            BucketSnapshot::Fungible { liquid, .. } => *liquid,
297            BucketSnapshot::NonFungible { liquid, .. } => liquid.len().into(),
298        }
299    }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, ScryptoSbor)]
303pub enum ProofSnapshot {
304    Fungible {
305        resource_address: ResourceAddress,
306        total_locked: Decimal,
307    },
308    NonFungible {
309        resource_address: ResourceAddress,
310        total_locked: IndexSet<NonFungibleLocalId>,
311    },
312}
313
314impl ProofSnapshot {
315    pub fn resource_address(&self) -> ResourceAddress {
316        match self {
317            ProofSnapshot::Fungible {
318                resource_address, ..
319            } => *resource_address,
320            ProofSnapshot::NonFungible {
321                resource_address, ..
322            } => *resource_address,
323        }
324    }
325    pub fn amount(&self) -> Decimal {
326        match self {
327            ProofSnapshot::Fungible { total_locked, .. } => *total_locked,
328            ProofSnapshot::NonFungible { total_locked, .. } => total_locked.len().into(),
329        }
330    }
331}
332
333#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
334pub struct ResourceSummary {
335    pub buckets: IndexMap<NodeId, BucketSnapshot>,
336    pub proofs: IndexMap<NodeId, ProofSnapshot>,
337}
338
339// TODO: Clean up
340#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
341pub enum TraceActor {
342    Method(NodeId),
343    NonMethod,
344}
345
346impl TraceActor {
347    pub fn from_actor(actor: &Actor) -> TraceActor {
348        match actor {
349            Actor::Method(MethodActor { node_id, .. }) => TraceActor::Method(*node_id),
350            _ => TraceActor::NonMethod,
351        }
352    }
353}
354
355#[derive(Debug, Clone, ScryptoSbor, PartialEq, Eq)]
356pub struct ExecutionTrace {
357    pub origin: TraceOrigin,
358    pub kernel_call_depth: usize,
359    pub current_frame_actor: TraceActor,
360    pub current_frame_depth: usize,
361    pub instruction_index: usize,
362    pub input: ResourceSummary,
363    pub output: ResourceSummary,
364    pub children: Vec<ExecutionTrace>,
365}
366
367#[derive(Debug, Clone, Eq, PartialEq, ScryptoSbor)]
368pub struct ApplicationFnIdentifier {
369    pub blueprint_id: BlueprintId,
370    pub ident: String,
371}
372
373#[derive(Debug, Clone, Eq, PartialEq, ScryptoSbor)]
374pub enum TraceOrigin {
375    ScryptoFunction(ApplicationFnIdentifier),
376    ScryptoMethod(ApplicationFnIdentifier),
377    CreateNode,
378    DropNode,
379}
380
381impl ExecutionTrace {
382    pub fn worktop_changes(
383        &self,
384        worktop_changes_aggregator: &mut IndexMap<usize, Vec<WorktopChange>>,
385    ) {
386        if let TraceOrigin::ScryptoMethod(fn_identifier) = &self.origin {
387            if fn_identifier.blueprint_id == BlueprintId::new(&RESOURCE_PACKAGE, WORKTOP_BLUEPRINT)
388            {
389                if fn_identifier.ident == WORKTOP_PUT_IDENT {
390                    for (_, bucket_snapshot) in self.input.buckets.iter() {
391                        worktop_changes_aggregator
392                            .entry(self.instruction_index)
393                            .or_default()
394                            .push(WorktopChange::Put(bucket_snapshot.into()))
395                    }
396                } else if fn_identifier.ident == WORKTOP_TAKE_IDENT
397                    || fn_identifier.ident == WORKTOP_TAKE_ALL_IDENT
398                    || fn_identifier.ident == WORKTOP_TAKE_NON_FUNGIBLES_IDENT
399                    || fn_identifier.ident == WORKTOP_DRAIN_IDENT
400                {
401                    for (_, bucket_snapshot) in self.output.buckets.iter() {
402                        worktop_changes_aggregator
403                            .entry(self.instruction_index)
404                            .or_default()
405                            .push(WorktopChange::Take(bucket_snapshot.into()))
406                    }
407                }
408            }
409        }
410
411        // Aggregate the worktop changes for all children traces
412        for child in self.children.iter() {
413            child.worktop_changes(worktop_changes_aggregator)
414        }
415    }
416}
417
418impl ResourceSummary {
419    pub fn new_empty() -> Self {
420        Self {
421            buckets: index_map_new(),
422            proofs: index_map_new(),
423        }
424    }
425
426    pub fn is_empty(&self) -> bool {
427        self.buckets.is_empty() && self.proofs.is_empty()
428    }
429
430    fn from_message(
431        api: &mut impl SystemModuleApiResourceSnapshotExtension,
432        message: &CallFrameMessage,
433    ) -> Self {
434        let mut buckets = index_map_new();
435        let mut proofs = index_map_new();
436        for node_id in &message.move_nodes {
437            if let Some(x) = api.read_bucket_uncosted(node_id) {
438                buckets.insert(*node_id, x);
439            }
440            if let Some(x) = api.read_proof_uncosted(node_id) {
441                proofs.insert(*node_id, x);
442            }
443        }
444        Self { buckets, proofs }
445    }
446
447    fn from_node_id(
448        api: &mut impl SystemModuleApiResourceSnapshotExtension,
449        node_id: &NodeId,
450    ) -> Self {
451        let mut buckets = index_map_new();
452        let mut proofs = index_map_new();
453        if let Some(x) = api.read_bucket_uncosted(node_id) {
454            buckets.insert(*node_id, x);
455        }
456        if let Some(x) = api.read_proof_uncosted(node_id) {
457            proofs.insert(*node_id, x);
458        }
459        Self { buckets, proofs }
460    }
461}
462
463impl Default for ResourceSummary {
464    fn default() -> Self {
465        Self::new_empty()
466    }
467}
468
469impl InitSystemModule for ExecutionTraceModule {}
470impl ResolvableSystemModule for ExecutionTraceModule {
471    #[inline]
472    fn resolve_from_system(system: &mut impl HasModules) -> &mut Self {
473        &mut system.modules_mut().execution_trace
474    }
475}
476impl PrivilegedSystemModule for ExecutionTraceModule {}
477
478impl<ModuleApi: SystemModuleApiFor<Self> + SystemModuleApiResourceSnapshotExtension>
479    SystemModule<ModuleApi> for ExecutionTraceModule
480{
481    fn on_create_node(api: &mut ModuleApi, event: &CreateNodeEvent) -> Result<(), RuntimeError> {
482        if api.current_stack_id_uncosted() != 0 {
483            return Ok(());
484        }
485        match event {
486            CreateNodeEvent::Start(..) => {
487                api.module().handle_before_create_node();
488            }
489            CreateNodeEvent::IOAccess(..) => {}
490            CreateNodeEvent::End(node_id) => {
491                let current_depth = api.current_stack_depth_uncosted();
492                let resource_summary = ResourceSummary::from_node_id(api, node_id);
493                let system_state = api.system_state();
494                Self::resolve_from_system(system_state.system).handle_after_create_node(
495                    system_state.current_call_frame,
496                    current_depth,
497                    resource_summary,
498                );
499            }
500        }
501
502        Ok(())
503    }
504
505    fn on_drop_node(api: &mut ModuleApi, event: &DropNodeEvent) -> Result<(), RuntimeError> {
506        if api.current_stack_id_uncosted() != 0 {
507            return Ok(());
508        }
509
510        match event {
511            DropNodeEvent::Start(node_id) => {
512                let resource_summary = ResourceSummary::from_node_id(api, node_id);
513                api.module().handle_before_drop_node(resource_summary);
514            }
515            DropNodeEvent::End(..) => {
516                let current_depth = api.current_stack_depth_uncosted();
517                let system_state = api.system_state();
518                system_state
519                    .system
520                    .modules
521                    .execution_trace
522                    .handle_after_drop_node(system_state.current_call_frame, current_depth);
523            }
524            DropNodeEvent::IOAccess(_) => {}
525        }
526
527        Ok(())
528    }
529
530    fn before_invoke(
531        api: &mut ModuleApi,
532        invocation: &KernelInvocation<Actor>,
533    ) -> Result<(), RuntimeError> {
534        if api.current_stack_id_uncosted() != 0 {
535            return Ok(());
536        }
537
538        let message = CallFrameMessage::from_input(&invocation.args, &invocation.call_frame_data);
539        let resource_summary = ResourceSummary::from_message(api, &message);
540        let callee = &invocation.call_frame_data;
541        let args = &invocation.args;
542        let system_state = api.system_state();
543        system_state
544            .system
545            .modules
546            .execution_trace
547            .handle_before_invoke(
548                system_state.current_call_frame,
549                callee,
550                resource_summary,
551                args,
552            );
553        Ok(())
554    }
555
556    fn on_execution_finish(
557        api: &mut ModuleApi,
558        message: &CallFrameMessage,
559    ) -> Result<(), RuntimeError> {
560        if api.current_stack_id_uncosted() != 0 {
561            return Ok(());
562        }
563
564        let current_depth = api.current_stack_depth_uncosted();
565        let resource_summary = ResourceSummary::from_message(api, message);
566
567        let system_state = api.system_state();
568
569        let caller = TraceActor::from_actor(system_state.caller_call_frame);
570
571        system_state
572            .system
573            .modules
574            .execution_trace
575            .handle_on_execution_finish(
576                system_state.current_call_frame,
577                current_depth,
578                &caller,
579                resource_summary,
580            );
581
582        Ok(())
583    }
584}
585
586impl ExecutionTraceModule {
587    pub fn new(max_kernel_call_depth_traced: usize) -> ExecutionTraceModule {
588        Self {
589            max_kernel_call_depth_traced,
590            current_instruction_index: 0,
591            current_kernel_call_depth: 0,
592            traced_kernel_call_inputs_stack: vec![],
593            kernel_call_traces_stacks: index_map_new(),
594            vault_ops: Vec::new(),
595        }
596    }
597
598    fn handle_before_create_node(&mut self) {
599        // Important to always update the counter (even if we're over the depth limit).
600        self.current_kernel_call_depth += 1;
601        if self.current_kernel_call_depth - 1 > self.max_kernel_call_depth_traced {
602            return;
603        }
604
605        let instruction_index = self.instruction_index();
606        let traced_input = (
607            ResourceSummary::default(),
608            TraceOrigin::CreateNode,
609            instruction_index,
610        );
611        self.traced_kernel_call_inputs_stack.push(traced_input);
612    }
613
614    fn handle_after_create_node(
615        &mut self,
616        current_actor: &Actor,
617        current_depth: usize,
618        resource_summary: ResourceSummary,
619    ) {
620        // Important to always update the counter (even if we're over the depth limit).
621        self.current_kernel_call_depth -= 1;
622        if self.current_kernel_call_depth > self.max_kernel_call_depth_traced {
623            return;
624        }
625
626        let current_actor = TraceActor::from_actor(current_actor);
627        self.finalize_kernel_call_trace(resource_summary, current_actor, current_depth)
628    }
629
630    fn handle_before_drop_node(&mut self, resource_summary: ResourceSummary) {
631        // Important to always update the counter (even if we're over the depth limit).
632        self.current_kernel_call_depth += 1;
633        if self.current_kernel_call_depth - 1 > self.max_kernel_call_depth_traced {
634            return;
635        }
636
637        let instruction_index = self.instruction_index();
638        let traced_input = (resource_summary, TraceOrigin::DropNode, instruction_index);
639        self.traced_kernel_call_inputs_stack.push(traced_input);
640    }
641
642    fn handle_after_drop_node(&mut self, current_actor: &Actor, current_depth: usize) {
643        // Important to always update the counter (even if we're over the depth limit).
644        self.current_kernel_call_depth -= 1;
645        if self.current_kernel_call_depth > self.max_kernel_call_depth_traced {
646            return;
647        }
648
649        let traced_output = ResourceSummary::default();
650        let current_actor = TraceActor::from_actor(current_actor);
651        self.finalize_kernel_call_trace(traced_output, current_actor, current_depth)
652    }
653
654    fn handle_before_invoke(
655        &mut self,
656        current_actor: &Actor,
657        callee: &Actor,
658        resource_summary: ResourceSummary,
659        args: &IndexedScryptoValue,
660    ) {
661        // Important to always update the counter (even if we're over the depth limit).
662        self.current_kernel_call_depth += 1;
663        if self.current_kernel_call_depth - 1 > self.max_kernel_call_depth_traced {
664            return;
665        }
666
667        let origin = match &callee {
668            Actor::Method(actor @ MethodActor { ident, .. }) => {
669                TraceOrigin::ScryptoMethod(ApplicationFnIdentifier {
670                    blueprint_id: actor.get_blueprint_id(),
671                    ident: ident.clone(),
672                })
673            }
674            Actor::Function(FunctionActor {
675                blueprint_id,
676                ident,
677                ..
678            }) => TraceOrigin::ScryptoFunction(ApplicationFnIdentifier {
679                blueprint_id: blueprint_id.clone(),
680                ident: ident.clone(),
681            }),
682            Actor::BlueprintHook(..) | Actor::Root => {
683                return;
684            }
685        };
686        let instruction_index = self.instruction_index();
687        self.traced_kernel_call_inputs_stack.push((
688            resource_summary.clone(),
689            origin,
690            instruction_index,
691        ));
692
693        match &callee {
694            Actor::Method(actor @ MethodActor { node_id, ident, .. })
695                if VaultUtil::is_vault_blueprint(&actor.get_blueprint_id()) =>
696            {
697                match ident.as_str() {
698                    VAULT_PUT_IDENT => {
699                        self.handle_vault_put_input(&resource_summary, current_actor, node_id)
700                    }
701                    FUNGIBLE_VAULT_LOCK_FEE_IDENT => {
702                        self.handle_vault_lock_fee_input(current_actor, node_id, args)
703                    }
704                    VAULT_BURN_IDENT
705                    | VAULT_TAKE_IDENT
706                    | VAULT_TAKE_ADVANCED_IDENT
707                    | VAULT_RECALL_IDENT
708                    | VAULT_GET_AMOUNT_IDENT
709                    | VAULT_FREEZE_IDENT
710                    | VAULT_UNFREEZE_IDENT => { /* no-op */ }
711                    FUNGIBLE_VAULT_LOCK_FUNGIBLE_AMOUNT_IDENT
712                    | FUNGIBLE_VAULT_UNLOCK_FUNGIBLE_AMOUNT_IDENT
713                    | FUNGIBLE_VAULT_CREATE_PROOF_OF_AMOUNT_IDENT => { /* no-op */ }
714                    NON_FUNGIBLE_VAULT_TAKE_NON_FUNGIBLES_IDENT
715                    | NON_FUNGIBLE_VAULT_GET_NON_FUNGIBLE_LOCAL_IDS_IDENT
716                    | NON_FUNGIBLE_VAULT_CONTAINS_NON_FUNGIBLE_IDENT
717                    | NON_FUNGIBLE_VAULT_RECALL_NON_FUNGIBLES_IDENT
718                    | NON_FUNGIBLE_VAULT_CREATE_PROOF_OF_NON_FUNGIBLES_IDENT
719                    | NON_FUNGIBLE_VAULT_LOCK_NON_FUNGIBLES_IDENT
720                    | NON_FUNGIBLE_VAULT_UNLOCK_NON_FUNGIBLES_IDENT
721                    | NON_FUNGIBLE_VAULT_BURN_NON_FUNGIBLES_IDENT => { /* no-op */ }
722                    _ => panic!("Unhandled vault method"),
723                }
724            }
725            _ => { /* no-op */ }
726        }
727    }
728
729    fn handle_on_execution_finish(
730        &mut self,
731        current_actor: &Actor,
732        current_depth: usize,
733        caller: &TraceActor,
734        resource_summary: ResourceSummary,
735    ) {
736        // Important to always update the counter (even if we're over the depth limit).
737        self.current_kernel_call_depth -= 1;
738        if self.current_kernel_call_depth > self.max_kernel_call_depth_traced {
739            return;
740        }
741
742        match current_actor {
743            Actor::Method(actor @ MethodActor { node_id, ident, .. }) => {
744                if VaultUtil::is_vault_blueprint(&actor.get_blueprint_id()) {
745                    match ident.as_str() {
746                        VAULT_TAKE_IDENT
747                        | VAULT_TAKE_ADVANCED_IDENT
748                        | NON_FUNGIBLE_VAULT_TAKE_NON_FUNGIBLES_IDENT => {
749                            for (_, resource) in &resource_summary.buckets {
750                                let op = if ident == VAULT_TAKE_IDENT {
751                                    VaultOp::Take(resource.resource_address(), resource.amount())
752                                } else if ident == VAULT_TAKE_ADVANCED_IDENT {
753                                    VaultOp::TakeAdvanced(
754                                        resource.resource_address(),
755                                        resource.amount(),
756                                    )
757                                } else if ident == NON_FUNGIBLE_VAULT_TAKE_NON_FUNGIBLES_IDENT {
758                                    VaultOp::TakeNonFungibles(
759                                        resource.resource_address(),
760                                        resource.amount(),
761                                    )
762                                } else {
763                                    panic!("Unhandled vault method")
764                                };
765                                self.vault_ops.push((
766                                    caller.clone(),
767                                    *node_id,
768                                    op,
769                                    self.instruction_index(),
770                                ));
771                            }
772                        }
773                        VAULT_PUT_IDENT
774                        | VAULT_GET_AMOUNT_IDENT
775                        | VAULT_FREEZE_IDENT
776                        | VAULT_UNFREEZE_IDENT
777                        | VAULT_BURN_IDENT
778                        | VAULT_RECALL_IDENT => { /* no-op */ }
779                        FUNGIBLE_VAULT_LOCK_FEE_IDENT
780                        | FUNGIBLE_VAULT_LOCK_FUNGIBLE_AMOUNT_IDENT
781                        | FUNGIBLE_VAULT_UNLOCK_FUNGIBLE_AMOUNT_IDENT
782                        | FUNGIBLE_VAULT_CREATE_PROOF_OF_AMOUNT_IDENT => { /* no-op */ }
783
784                        NON_FUNGIBLE_VAULT_GET_NON_FUNGIBLE_LOCAL_IDS_IDENT
785                        | NON_FUNGIBLE_VAULT_CONTAINS_NON_FUNGIBLE_IDENT
786                        | NON_FUNGIBLE_VAULT_RECALL_NON_FUNGIBLES_IDENT
787                        | NON_FUNGIBLE_VAULT_CREATE_PROOF_OF_NON_FUNGIBLES_IDENT
788                        | NON_FUNGIBLE_VAULT_LOCK_NON_FUNGIBLES_IDENT
789                        | NON_FUNGIBLE_VAULT_UNLOCK_NON_FUNGIBLES_IDENT
790                        | NON_FUNGIBLE_VAULT_BURN_NON_FUNGIBLES_IDENT => { /* no-op */ }
791                        _ => panic!("Unhandled vault method"),
792                    }
793                }
794            }
795            Actor::Function(_) => {}
796            Actor::BlueprintHook(..) | Actor::Root => return,
797        }
798
799        let current_actor = TraceActor::from_actor(current_actor);
800        self.finalize_kernel_call_trace(resource_summary, current_actor, current_depth)
801    }
802
803    fn finalize_kernel_call_trace(
804        &mut self,
805        traced_output: ResourceSummary,
806        current_actor: TraceActor,
807        current_depth: usize,
808    ) {
809        let child_traces = self
810            .kernel_call_traces_stacks
811            .swap_remove(&(self.current_kernel_call_depth + 1))
812            .unwrap_or(vec![]);
813
814        let (traced_input, origin, instruction_index) = self
815            .traced_kernel_call_inputs_stack
816            .pop()
817            .expect("kernel call input stack underflow");
818
819        // Only include the trace if:
820        // * there's a non-empty traced input or output
821        // * OR there are any child traces: they need a parent regardless of whether it traces any inputs/outputs.
822        //   At some depth (up to the tracing limit) there must have been at least one traced input/output
823        //   so we need to include the full path up to the root.
824        if !traced_input.is_empty() || !traced_output.is_empty() || !child_traces.is_empty() {
825            let trace = ExecutionTrace {
826                origin,
827                kernel_call_depth: self.current_kernel_call_depth,
828                current_frame_actor: current_actor,
829                current_frame_depth: current_depth,
830                instruction_index,
831                input: traced_input,
832                output: traced_output,
833                children: child_traces,
834            };
835
836            let siblings = self
837                .kernel_call_traces_stacks
838                .entry(self.current_kernel_call_depth)
839                .or_insert(vec![]);
840            siblings.push(trace);
841        }
842    }
843
844    pub fn finalize(
845        mut self,
846        fee_payments: &IndexMap<NodeId, Decimal>,
847        is_success: bool,
848    ) -> TransactionExecutionTrace {
849        let mut execution_traces = Vec::new();
850        for (_, traces) in self.kernel_call_traces_stacks.drain(..) {
851            execution_traces.extend(traces);
852        }
853
854        let fee_locks = calculate_fee_locks(&self.vault_ops);
855        let resource_changes = calculate_resource_changes(self.vault_ops, fee_payments, is_success);
856
857        TransactionExecutionTrace {
858            execution_traces,
859            resource_changes,
860            fee_locks,
861        }
862    }
863
864    fn instruction_index(&self) -> usize {
865        self.current_instruction_index
866    }
867
868    fn handle_vault_put_input(
869        &mut self,
870        resource_summary: &ResourceSummary,
871        caller: &Actor,
872        vault_id: &NodeId,
873    ) {
874        let actor = TraceActor::from_actor(caller);
875        for (_, resource) in &resource_summary.buckets {
876            self.vault_ops.push((
877                actor.clone(),
878                *vault_id,
879                VaultOp::Put(resource.resource_address(), resource.amount()),
880                self.instruction_index(),
881            ));
882        }
883    }
884
885    fn handle_vault_lock_fee_input(
886        &mut self,
887        caller: &Actor,
888        vault_id: &NodeId,
889        args: &IndexedScryptoValue,
890    ) {
891        let actor = TraceActor::from_actor(caller);
892        let FungibleVaultLockFeeInput { amount, contingent } = args.as_typed().unwrap();
893        self.vault_ops.push((
894            actor,
895            *vault_id,
896            VaultOp::LockFee(amount, contingent),
897            self.instruction_index(),
898        ));
899    }
900}
901
902pub fn calculate_resource_changes(
903    mut vault_ops: Vec<(TraceActor, NodeId, VaultOp, usize)>,
904    fee_payments: &IndexMap<NodeId, Decimal>,
905    is_commit_success: bool,
906) -> IndexMap<usize, Vec<ResourceChange>> {
907    // Retain lock fee only if the transaction fails.
908    if !is_commit_success {
909        vault_ops.retain(|x| matches!(x.2, VaultOp::LockFee(..)));
910    }
911
912    // Calculate per instruction index, actor, vault resource changes.
913    let mut vault_changes =
914        index_map_new::<usize, IndexMap<NodeId, IndexMap<NodeId, (ResourceAddress, Decimal)>>>();
915    for (actor, vault_id, vault_op, instruction_index) in vault_ops {
916        if let TraceActor::Method(node_id) = actor {
917            match vault_op {
918                VaultOp::Put(resource_address, amount) => {
919                    let entry = &mut vault_changes
920                        .entry(instruction_index)
921                        .or_default()
922                        .entry(node_id)
923                        .or_default()
924                        .entry(vault_id)
925                        .or_insert((resource_address, Decimal::zero()))
926                        .1;
927                    *entry = entry.checked_add(amount).unwrap();
928                }
929                VaultOp::Take(resource_address, amount)
930                | VaultOp::TakeAdvanced(resource_address, amount)
931                | VaultOp::TakeNonFungibles(resource_address, amount) => {
932                    let entry = &mut vault_changes
933                        .entry(instruction_index)
934                        .or_default()
935                        .entry(node_id)
936                        .or_default()
937                        .entry(vault_id)
938                        .or_insert((resource_address, Decimal::zero()))
939                        .1;
940                    *entry = entry.checked_sub(amount).unwrap();
941                }
942                VaultOp::LockFee(..) => {
943                    let entry = &mut vault_changes
944                        .entry(instruction_index)
945                        .or_default()
946                        .entry(node_id)
947                        .or_default()
948                        .entry(vault_id)
949                        .or_insert((XRD, Decimal::zero()))
950                        .1;
951                    *entry = entry
952                        .checked_sub(fee_payments.get(&vault_id).cloned().unwrap_or_default())
953                        .unwrap();
954                }
955            }
956        }
957    }
958
959    // Convert into a vec for ease of consumption.
960    let mut resource_changes = index_map_new::<usize, Vec<ResourceChange>>();
961    for (instruction_index, instruction_resource_changes) in vault_changes {
962        for (node_id, map) in instruction_resource_changes {
963            for (vault_id, (resource_address, delta)) in map {
964                // Add a resource change log if non-zero
965                if !delta.is_zero() {
966                    resource_changes
967                        .entry(instruction_index)
968                        .or_default()
969                        .push(ResourceChange {
970                            resource_address,
971                            node_id,
972                            vault_id,
973                            amount: delta,
974                        });
975                }
976            }
977        }
978    }
979
980    resource_changes
981}
982
983pub fn calculate_fee_locks(vault_ops: &Vec<(TraceActor, NodeId, VaultOp, usize)>) -> FeeLocks {
984    let mut fee_locks = FeeLocks {
985        lock: Decimal::ZERO,
986        contingent_lock: Decimal::ZERO,
987    };
988    for (_, _, vault_op, _) in vault_ops {
989        if let VaultOp::LockFee(amount, is_contingent) = vault_op {
990            if !is_contingent {
991                fee_locks.lock = fee_locks.lock.checked_add(*amount).unwrap()
992            } else {
993                fee_locks.contingent_lock = fee_locks.contingent_lock.checked_add(*amount).unwrap()
994            }
995        };
996    }
997    fee_locks
998}