radix_transactions/manifest/static_resource_movements/
visitor.rs

1use super::*;
2use crate::internal_prelude::*;
3use crate::manifest::*;
4use core::ops::*;
5use radix_engine_interface::prelude::*;
6
7/// A [`ManifestInterpretationVisitor`] that statically tracks the resources in the worktop and
8/// reports the account withdraws and deposits made.
9pub struct StaticResourceMovementsVisitor {
10    /// The resource content of the worktop.
11    worktop: TrackedResources,
12    /// Bounds against all existing buckets tracked by the visitor.
13    tracked_buckets: IndexMap<ManifestBucket, (ResourceAddress, TrackedResource)>,
14    /// The blueprint of all running named addresses
15    tracked_named_addresses: IndexMap<ManifestNamedAddress, BlueprintId>,
16    /// The information about the invocations observed in this manifest. This will be surfaced to
17    /// the user when they call the output function.
18    invocation_static_information: IndexMap<usize, InvocationStaticInformation>,
19    /// Details about the currently running instruction. Has a value between OnStartInstruction and OnEndInstruction.
20    current_instruction: Option<CurrentInstruction>,
21    /// Details the assertion to apply to the contents returned from the next invocation.
22    next_invocation_assertion: Option<(OwnedNextCallAssertion, ChangeSource)>,
23}
24
25pub struct CurrentInstruction {
26    index: usize,
27    sent_resources: TrackedResources,
28}
29
30/// Created by the visitor, generally references a particular instruction, or maybe an initial YIELD_TO_PARENT.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum ChangeSource {
33    InitialYieldFromParent,
34    Invocation { instruction_index: usize },
35    NewBucket { instruction_index: usize },
36    Assertion { instruction_index: usize },
37}
38
39impl ChangeSource {
40    pub fn invocation_at(instruction_index: usize) -> Self {
41        Self::Invocation { instruction_index }
42    }
43
44    pub fn bucket_at(instruction_index: usize) -> Self {
45        Self::NewBucket { instruction_index }
46    }
47
48    pub fn assertion_at(instruction_index: usize) -> Self {
49        Self::Assertion { instruction_index }
50    }
51}
52
53impl StaticResourceMovementsVisitor {
54    pub fn new(initial_worktop_state_is_unknown: bool) -> Self {
55        let mut worktop = TrackedResources::new_empty();
56
57        if initial_worktop_state_is_unknown {
58            worktop.mut_add_unspecified_resources([ChangeSource::InitialYieldFromParent])
59        }
60
61        Self {
62            worktop,
63            tracked_buckets: Default::default(),
64            tracked_named_addresses: Default::default(),
65            invocation_static_information: Default::default(),
66            current_instruction: None,
67            next_invocation_assertion: None,
68        }
69    }
70
71    pub fn output(self) -> StaticResourceMovementsOutput {
72        StaticResourceMovementsOutput {
73            invocation_static_information: self.invocation_static_information,
74        }
75    }
76
77    fn handle_invocation_end(
78        &mut self,
79        invocation_kind: InvocationKind<'_>,
80        args: &ManifestValue,
81        current_instruction: CurrentInstruction,
82    ) -> Result<InvocationStaticInformation, StaticResourceMovementsError> {
83        // Get the invocation inputs from the aggregated resources sent during the current instruction.
84        let invocation_input = current_instruction.sent_resources;
85
86        let change_source = ChangeSource::Invocation {
87            instruction_index: current_instruction.index,
88        };
89
90        // TODO: In the future we should propagate errors from the native instruction conversion.
91        // We do not do it at the moment as we have found issues when decoding valid invocations as
92        // their manifest SBOR types.
93        let mut invocation_output = match self.resolve_native_invocation(invocation_kind, args) {
94            Ok(Some((matched_invocation, receiver))) => {
95                matched_invocation.output(InvocationDetails {
96                    receiver,
97                    sent_resources: &invocation_input,
98                    source: change_source,
99                })?
100            }
101            Err(..) | Ok(None) => {
102                TrackedResources::new_with_possible_balance_of_unspecified_resources([
103                    change_source,
104                ])
105            }
106        };
107
108        if let Some((assertion, change_source)) = self.next_invocation_assertion.take() {
109            // FUTURE TWEAK: Could output an inequality constraints when handling the assertions,
110            // for use in any analyzers.
111            match assertion.as_ref() {
112                NextCallAssertion::ReturnsOnly { constraints } => {
113                    invocation_output
114                        .handle_resources_only_assertion(constraints, change_source)?;
115                }
116                NextCallAssertion::ReturnsInclude { constraints } => {
117                    invocation_output
118                        .handle_resources_include_assertion(constraints, change_source)?;
119                }
120            }
121        }
122
123        // Add the returned resources to the worktop
124        self.worktop.mut_add(invocation_output.clone())?;
125
126        Ok(InvocationStaticInformation {
127            kind: invocation_kind.into(),
128            input: invocation_input,
129            output: invocation_output,
130        })
131    }
132
133    fn resolve_native_invocation(
134        &self,
135        invocation_kind: InvocationKind,
136        args: &ManifestValue,
137    ) -> Result<
138        Option<(TypedManifestNativeInvocation, InvocationReceiver)>,
139        StaticResourceMovementsError,
140    > {
141        // Creating a typed native invocation to use in interpreting the invocation.
142        match invocation_kind {
143            InvocationKind::DirectMethod { address, method } => {
144                let resolved_dynamic_address = ResolvedDynamicAddress::StaticAddress(*address);
145                let Some(typed_invocation) =
146                    TypedManifestNativeInvocation::from_direct_method_invocation(
147                        &resolved_dynamic_address,
148                        method,
149                        args,
150                    )?
151                else {
152                    return Ok(None);
153                };
154                let invocation_receiver = InvocationReceiver::DirectAccess(*address);
155                Ok(Some((typed_invocation, invocation_receiver)))
156            }
157            InvocationKind::Method {
158                address,
159                module_id,
160                method,
161            } => {
162                let resolved_dynamic_address = match address {
163                    ManifestGlobalAddress::Static(global_address) => {
164                        ResolvedDynamicAddress::StaticAddress(*global_address)
165                    }
166                    ManifestGlobalAddress::Named(named_address) => {
167                        let blueprint_id = self.tracked_named_addresses.get(named_address)
168                            .expect("Interpreter should have validated the address exists, because we're handling this on instruction end");
169                        ResolvedDynamicAddress::BlueprintResolvedFromNamedAddress(
170                            blueprint_id.clone(),
171                        )
172                    }
173                };
174                let Some(typed_invocation) = TypedManifestNativeInvocation::from_method_invocation(
175                    &resolved_dynamic_address,
176                    module_id,
177                    method,
178                    args,
179                )?
180                else {
181                    return Ok(None);
182                };
183                let invocation_receiver =
184                    InvocationReceiver::GlobalMethod(resolved_dynamic_address);
185                Ok(Some((typed_invocation, invocation_receiver)))
186            }
187            InvocationKind::Function {
188                address: ManifestPackageAddress::Static(package_address),
189                blueprint,
190                function,
191            } => {
192                let blueprint_id = BlueprintId::new(package_address, blueprint);
193                let Some(typed_invocation) =
194                    TypedManifestNativeInvocation::from_function_invocation(
195                        &blueprint_id,
196                        function,
197                        args,
198                    )?
199                else {
200                    return Ok(None);
201                };
202                let invocation_receiver = InvocationReceiver::BlueprintFunction(blueprint_id);
203                Ok(Some((typed_invocation, invocation_receiver)))
204            }
205            InvocationKind::YieldToParent
206            | InvocationKind::YieldToChild { .. }
207            | InvocationKind::Function {
208                address: ManifestPackageAddress::Named(_),
209                ..
210            } => Ok(None),
211        }
212    }
213
214    fn current_instruction_index(&mut self) -> usize {
215        self.current_instruction
216            .as_ref()
217            .expect("Should only be called during an instruction")
218            .index
219    }
220
221    fn current_instruction_sent_resources(&mut self) -> &mut TrackedResources {
222        &mut self
223            .current_instruction
224            .as_mut()
225            .expect("Should only be called during an instruction")
226            .sent_resources
227    }
228
229    fn handle_start_instruction(
230        &mut self,
231        OnStartInstruction { index, .. }: OnStartInstruction,
232    ) -> Result<(), StaticResourceMovementsError> {
233        self.current_instruction = Some(CurrentInstruction {
234            index,
235            sent_resources: TrackedResources::new_empty(),
236        });
237        Ok(())
238    }
239
240    fn handle_end_instruction(
241        &mut self,
242        OnEndInstruction { effect, index }: OnEndInstruction,
243    ) -> Result<(), StaticResourceMovementsError> {
244        let instruction_data = self.current_instruction.take().unwrap();
245
246        // We only care about invocations. Ignore anything that is not an invocation.
247        let ManifestInstructionEffect::Invocation { kind, args, .. } = effect else {
248            return Ok(());
249        };
250
251        // Handle the invocation and get its static information back.
252        let invocation_static_information =
253            self.handle_invocation_end(kind, args, instruction_data)?;
254
255        // Adding the static information to the state to surface later to the consumer.
256        self.invocation_static_information
257            .insert(index, invocation_static_information);
258
259        Ok(())
260    }
261
262    fn handle_new_bucket(
263        &mut self,
264        OnNewBucket { bucket, state }: OnNewBucket,
265    ) -> Result<(), StaticResourceMovementsError> {
266        let source = ChangeSource::NewBucket {
267            instruction_index: self.current_instruction_index(),
268        };
269        let (resource_address, resource_amount) = match state.source_amount {
270            BucketSourceAmount::AllOnWorktop { resource_address } => {
271                let resource_amount = self.worktop.mut_take_resource(
272                    *resource_address,
273                    ResourceTakeAmount::All,
274                    source,
275                )?;
276                (*resource_address, resource_amount)
277            }
278            BucketSourceAmount::AmountFromWorktop {
279                resource_address,
280                amount,
281            } => {
282                let resource_amount = self.worktop.mut_take_resource(
283                    *resource_address,
284                    ResourceTakeAmount::exact_amount(amount)?,
285                    source,
286                )?;
287                (*resource_address, resource_amount)
288            }
289            BucketSourceAmount::NonFungiblesFromWorktop {
290                resource_address,
291                ids,
292            } => {
293                let resource_amount = self.worktop.mut_take_resource(
294                    *resource_address,
295                    ResourceTakeAmount::exact_non_fungibles(ids.iter().cloned()),
296                    source,
297                )?;
298                (*resource_address, resource_amount)
299            }
300        };
301
302        self.tracked_buckets
303            .insert(bucket, (resource_address, resource_amount));
304
305        Ok(())
306    }
307
308    fn handle_consume_bucket(
309        &mut self,
310        OnConsumeBucket {
311            bucket,
312            destination,
313            ..
314        }: OnConsumeBucket,
315    ) -> Result<(), StaticResourceMovementsError> {
316        let (resource_address, amount) = self
317            .tracked_buckets
318            .swap_remove(&bucket)
319            .expect("Interpreter should ensure the bucket lifetimes are validated");
320
321        match destination {
322            BucketDestination::Worktop => {
323                self.worktop.mut_add_resource(resource_address, amount)?;
324            }
325            BucketDestination::Burned => {}
326            BucketDestination::Invocation(_) => self
327                .current_instruction_sent_resources()
328                .mut_add_resource(resource_address, amount)?,
329        }
330
331        Ok(())
332    }
333
334    fn handle_pass_expression(
335        &mut self,
336        OnPassExpression {
337            expression,
338            destination,
339            ..
340        }: OnPassExpression,
341    ) -> Result<(), StaticResourceMovementsError> {
342        match (expression, destination) {
343            (ManifestExpression::EntireWorktop, ExpressionDestination::Invocation(_)) => {
344                let entire_worktop = self.worktop.take_all();
345                self.current_instruction_sent_resources()
346                    .mut_add(entire_worktop)?;
347            }
348            (ManifestExpression::EntireAuthZone, _) => {}
349        }
350
351        Ok(())
352    }
353
354    fn handle_resource_assertion(
355        &mut self,
356        OnResourceAssertion { assertion }: OnResourceAssertion,
357    ) -> Result<(), StaticResourceMovementsError> {
358        // FUTURE TWEAK: Could add inequality constraints when handling assertions,
359        // for use in any analyzers.
360        let change_source = ChangeSource::assertion_at(self.current_instruction_index());
361        match assertion {
362            ResourceAssertion::Worktop(WorktopAssertion::ResourceNonZeroAmount {
363                resource_address,
364            }) => self.worktop.handle_resource_assertion(
365                *resource_address,
366                ResourceBounds::non_zero(),
367                change_source,
368            ),
369            ResourceAssertion::Worktop(WorktopAssertion::ResourceAtLeastAmount {
370                resource_address,
371                amount,
372            }) => self.worktop.handle_resource_assertion(
373                *resource_address,
374                ResourceBounds::at_least_amount(amount)?,
375                change_source,
376            ),
377            ResourceAssertion::Worktop(WorktopAssertion::ResourceAtLeastNonFungibles {
378                resource_address,
379                ids,
380            }) => self.worktop.handle_resource_assertion(
381                *resource_address,
382                ResourceBounds::at_least_non_fungibles(ids.iter().cloned()),
383                change_source,
384            ),
385            ResourceAssertion::Worktop(WorktopAssertion::ResourcesOnly { constraints }) => self
386                .worktop
387                .handle_resources_only_assertion(constraints, change_source),
388            ResourceAssertion::Worktop(WorktopAssertion::ResourcesInclude { constraints }) => self
389                .worktop
390                .handle_resources_include_assertion(constraints, change_source),
391            ResourceAssertion::NextCall(next_call_assertion) => {
392                if self.next_invocation_assertion.is_some() {
393                    panic!("Interpreter should have verified that a next call assertion must be used before another is created");
394                }
395                self.next_invocation_assertion = Some((next_call_assertion.into(), change_source));
396                Ok(())
397            }
398            ResourceAssertion::Bucket(BucketAssertion::Contents { bucket, constraint }) => {
399                let (_, tracked_resource) = self
400                    .tracked_buckets
401                    .get_mut(&bucket)
402                    .expect("Interpreter should have already validated that the bucket exists");
403                tracked_resource.handle_assertion(
404                    ResourceBounds::new_for_manifest_constraint(constraint)?,
405                    change_source,
406                )
407            }
408        }
409    }
410
411    fn handle_new_named_address(
412        &mut self,
413        OnNewNamedAddress {
414            named_address,
415            package_address,
416            blueprint_name,
417            ..
418        }: OnNewNamedAddress,
419    ) -> Result<(), StaticResourceMovementsError> {
420        self.tracked_named_addresses.insert(
421            named_address,
422            BlueprintId::new(package_address, blueprint_name),
423        );
424        Ok(())
425    }
426
427    fn handle_finish(&mut self, OnFinish: OnFinish) -> Result<(), StaticResourceMovementsError> {
428        // We should report an error if we know for sure that the worktop is not empty
429        for (_resource, resource_bound) in self.worktop.specified_resources() {
430            let (lower_bound, _upper_bound) = resource_bound.bounds().numeric_bounds();
431            if lower_bound.is_positive() {
432                return Err(StaticResourceMovementsError::WorktopEndsWithKnownResourcesPresent);
433            }
434        }
435        Ok(())
436    }
437}
438
439impl ManifestInterpretationVisitor for StaticResourceMovementsVisitor {
440    type Output = StaticResourceMovementsError;
441
442    fn on_start_instruction(&mut self, event: OnStartInstruction) -> ControlFlow<Self::Output> {
443        match self.handle_start_instruction(event) {
444            Ok(()) => ControlFlow::Continue(()),
445            Err(err) => ControlFlow::Break(err),
446        }
447    }
448
449    fn on_end_instruction(&mut self, event: OnEndInstruction) -> ControlFlow<Self::Output> {
450        match self.handle_end_instruction(event) {
451            Ok(()) => ControlFlow::Continue(()),
452            Err(err) => ControlFlow::Break(err),
453        }
454    }
455
456    fn on_new_bucket(&mut self, event: OnNewBucket) -> ControlFlow<Self::Output> {
457        match self.handle_new_bucket(event) {
458            Ok(()) => ControlFlow::Continue(()),
459            Err(err) => ControlFlow::Break(err),
460        }
461    }
462
463    fn on_consume_bucket(&mut self, event: OnConsumeBucket) -> ControlFlow<Self::Output> {
464        match self.handle_consume_bucket(event) {
465            Ok(()) => ControlFlow::Continue(()),
466            Err(err) => ControlFlow::Break(err),
467        }
468    }
469
470    fn on_pass_expression(&mut self, event: OnPassExpression) -> ControlFlow<Self::Output> {
471        match self.handle_pass_expression(event) {
472            Ok(()) => ControlFlow::Continue(()),
473            Err(err) => ControlFlow::Break(err),
474        }
475    }
476
477    fn on_resource_assertion(&mut self, event: OnResourceAssertion) -> ControlFlow<Self::Output> {
478        match self.handle_resource_assertion(event) {
479            Ok(()) => ControlFlow::Continue(()),
480            Err(err) => ControlFlow::Break(err),
481        }
482    }
483
484    fn on_new_named_address(&mut self, event: OnNewNamedAddress) -> ControlFlow<Self::Output> {
485        match self.handle_new_named_address(event) {
486            Ok(()) => ControlFlow::Continue(()),
487            Err(err) => ControlFlow::Break(err),
488        }
489    }
490
491    fn on_finish(&mut self, event: OnFinish) -> ControlFlow<Self::Output> {
492        match self.handle_finish(event) {
493            Ok(()) => ControlFlow::Continue(()),
494            Err(err) => ControlFlow::Break(err),
495        }
496    }
497}