radix_transactions/validation/
transaction_structure_validator.rs

1use crate::internal_prelude::*;
2use core::ops::ControlFlow;
3
4pub trait IntentStructure {
5    fn intent_hash(&self) -> IntentHash;
6    fn children(&self) -> impl ExactSizeIterator<Item = SubintentHash>;
7
8    /// Should perform all the validation of the intent, except the relationship to other intents.
9    fn validate_intent(
10        &self,
11        validator: &TransactionValidator,
12        aggregation: &mut AcrossIntentAggregation,
13    ) -> Result<ManifestYieldSummary, IntentValidationError>;
14}
15
16pub trait IntentTreeStructure {
17    type RootIntentStructure: IntentStructure;
18    type SubintentStructure: IntentStructure + HasSubintentHash;
19    fn root(&self) -> &Self::RootIntentStructure;
20    fn non_root_subintents<'a>(
21        &'a self,
22    ) -> impl ExactSizeIterator<Item = &'a Self::SubintentStructure>;
23}
24
25pub struct ValidatedIntentTreeInformation {
26    pub intent_relationships: IntentRelationships,
27    pub overall_validity_range: OverallValidityRangeV2,
28    pub root_yield_summary: ManifestYieldSummary,
29}
30
31impl TransactionValidator {
32    pub fn validate_intents_and_structure(
33        &self,
34        intent_tree: &impl IntentTreeStructure,
35    ) -> Result<ValidatedIntentTreeInformation, TransactionValidationError> {
36        let intent_relationships = self.validate_intent_relationships(intent_tree)?;
37
38        let non_root_subintent_details = &intent_relationships.non_root_subintents;
39        let mut aggregation = AcrossIntentAggregation::start();
40        let mut yield_summaries: IndexMap<IntentHash, ManifestYieldSummary> =
41            index_map_with_capacity(intent_tree.non_root_subintents().len() + 1);
42
43        let root_yield_summary = {
44            let root_intent_hash = intent_tree.root().intent_hash();
45            let yield_summary = intent_tree
46                .root()
47                .validate_intent(self, &mut aggregation)
48                .map_err(|err| {
49                    TransactionValidationError::IntentValidationError(
50                        TransactionValidationErrorLocation::for_root(root_intent_hash),
51                        err,
52                    )
53                })?;
54            yield_summaries.insert(root_intent_hash, yield_summary.clone());
55            yield_summary
56        };
57
58        for (index, subintent) in intent_tree.non_root_subintents().enumerate() {
59            let subintent_hash = subintent.subintent_hash();
60            let yield_summary =
61                subintent
62                    .validate_intent(self, &mut aggregation)
63                    .map_err(|err| {
64                        TransactionValidationError::IntentValidationError(
65                            TransactionValidationErrorLocation::NonRootSubintent(
66                                SubintentIndex(index),
67                                subintent_hash,
68                            ),
69                            err,
70                        )
71                    })?;
72            yield_summaries.insert(subintent_hash.into(), yield_summary);
73        }
74
75        let overall_validity_range = aggregation.finalize(&self.config)?;
76
77        for (child_hash, child_details) in non_root_subintent_details {
78            let child_intent_hash = IntentHash::Subintent(*child_hash);
79            // This checks that the YIELD_TO_PARENTs in a subintent match the YIELD_TO_CHILDS in the parent.
80            // The instruction validation has already checked that the subintents end with a YIELD_TO_PARENT.
81            let parent_yield_summary = yield_summaries.get(&child_details.parent).unwrap();
82            let parent_yield_child_calls =
83                *parent_yield_summary.child_yields.get(child_hash).unwrap();
84            let child_yield_summary = yield_summaries.get(&child_intent_hash).unwrap();
85            let child_yield_parent_calls = child_yield_summary.parent_yields;
86            if parent_yield_child_calls != child_yield_parent_calls {
87                return Err(
88                    SubintentStructureError::MismatchingYieldChildAndYieldParentCountsForSubintent
89                        .for_subintent(child_details.index, *child_hash),
90                );
91            }
92        }
93
94        Ok(ValidatedIntentTreeInformation {
95            intent_relationships,
96            overall_validity_range,
97            root_yield_summary,
98        })
99    }
100
101    /// The root intent can be either:
102    /// * If validating a full transaction: a transaction intent
103    /// * If validating a partial transaction: a root subintent
104    fn validate_intent_relationships(
105        &self,
106        intent_tree: &impl IntentTreeStructure,
107    ) -> Result<IntentRelationships, TransactionValidationError> {
108        let mut root_intent_details = RootIntentRelationshipDetails::default();
109        let mut non_root_subintent_details =
110            IndexMap::<SubintentHash, SubintentRelationshipDetails>::default();
111
112        // STEP 1
113        // ------
114        // * We establish that the non-root subintents are unique
115        // * We create an index from the SubintentHash to SubintentIndex
116        for (index, subintent) in intent_tree.non_root_subintents().enumerate() {
117            let subintent_hash = subintent.subintent_hash();
118            let index = SubintentIndex(index);
119            let details = SubintentRelationshipDetails::default_for(index);
120            if let Some(_) = non_root_subintent_details.insert(subintent_hash, details) {
121                return Err(SubintentStructureError::DuplicateSubintent
122                    .for_subintent(index, subintent_hash));
123            }
124        }
125
126        // STEP 2
127        // ------
128        // We establish, for each parent intent, that each of its children:
129        // * Exist as subintents in the transaction tree
130        // * Has no other parents
131        //
132        // We also:
133        // * Save the unique parent on each subintent which is a child
134        // * Save the children of an intent into its intent details
135        //
136        // After this step, we know that each subintent has at most one parent.
137        // We determine that every subintent has exactly one parent in step 4.
138
139        // STEP 2A - Handle children of the root intent
140        {
141            let parent_hash = intent_tree.root().intent_hash();
142            let intent_details = &mut root_intent_details;
143            for child_hash in intent_tree.root().children() {
144                let child_subintent_details = non_root_subintent_details
145                    .get_mut(&child_hash)
146                    .ok_or_else(|| {
147                        SubintentStructureError::ChildSubintentNotIncludedInTransaction(child_hash)
148                            .for_unindexed()
149                    })?;
150                if child_subintent_details.parent == PLACEHOLDER_PARENT {
151                    child_subintent_details.parent = parent_hash;
152                } else {
153                    return Err(SubintentStructureError::SubintentHasMultipleParents
154                        .for_subintent(child_subintent_details.index, child_hash));
155                }
156                intent_details.children.push(child_subintent_details.index);
157            }
158        }
159
160        // STEP 2B - Handle the children of each subintent
161        for subintent in intent_tree.non_root_subintents() {
162            let subintent_hash = subintent.subintent_hash();
163            let parent_hash: IntentHash = subintent_hash.into();
164            let children = subintent.children();
165            let mut children_details = Vec::with_capacity(children.len());
166            for child_hash in children {
167                let child_subintent_details = non_root_subintent_details
168                    .get_mut(&child_hash)
169                    .ok_or_else(|| {
170                        SubintentStructureError::ChildSubintentNotIncludedInTransaction(child_hash)
171                            .for_unindexed()
172                    })?;
173                if child_subintent_details.parent == PLACEHOLDER_PARENT {
174                    child_subintent_details.parent = parent_hash;
175                } else {
176                    return Err(SubintentStructureError::SubintentHasMultipleParents
177                        .for_subintent(child_subintent_details.index, child_hash));
178                }
179                children_details.push(child_subintent_details.index);
180            }
181            non_root_subintent_details
182                .get_mut(&subintent_hash)
183                .unwrap()
184                .children = children_details;
185        }
186
187        // STEP 3
188        // ------
189        // We traverse the child relationships from the root, and mark a depth.
190        // We error if any exceed the maximum depth.
191        //
192        // The iteration count is guaranteed to be bounded by the number of subintents because:
193        // * Each subintent has at most one parent from step 2.
194        // * Each parent -> child relationship is traversed at most once in the iteration.
195        //   Quick proof by contradiction:
196        //   - Assume not. Then some parent A is visited more than once.
197        //   - Take the earliest such A in the iteration.
198        //   - On both of its visits, A can only have been visited from its parent B.
199        //   - But then B must have been visited more than once, contradicting the minimality of A.
200        let mut work_list = vec![];
201        for index in root_intent_details.children.iter() {
202            work_list.push((*index, 1));
203        }
204
205        let max_depth = if intent_tree.root().intent_hash().is_for_subintent() {
206            self.config.max_subintent_depth - 1
207        } else {
208            self.config.max_subintent_depth
209        };
210
211        loop {
212            let Some((index, depth)) = work_list.pop() else {
213                break;
214            };
215            if depth > max_depth {
216                let (hash, _) = non_root_subintent_details.get_index(index.0).unwrap();
217                return Err(
218                    SubintentStructureError::SubintentExceedsMaxDepth.for_subintent(index, *hash)
219                );
220            }
221            let (_, subintent_details) = non_root_subintent_details.get_index_mut(index.0).unwrap();
222            subintent_details.depth = depth;
223            for index in subintent_details.children.iter() {
224                work_list.push((*index, depth + 1));
225            }
226        }
227
228        // STEP 4
229        // ------
230        // We check that every subintent has a marked "depth from root".
231        //
232        // Combined with step 2 and step 3, we now have that:
233        // * Every subintent has a unique parent.
234        // * Every subintent is reachable from the root.
235        //
236        // Therefore there is a unique path from every subintent to the root, which implies
237        // the subintents form a tree.
238        for (hash, details) in non_root_subintent_details.iter() {
239            if details.depth == 0 {
240                return Err(
241                    SubintentStructureError::SubintentIsNotReachableFromTheTransactionIntent
242                        .for_subintent(details.index, *hash),
243                );
244            }
245        }
246
247        Ok(IntentRelationships {
248            root_intent: root_intent_details,
249            non_root_subintents: non_root_subintent_details,
250        })
251    }
252}
253
254// This type is public so it can be used by the toolkit.
255#[must_use]
256pub struct AcrossIntentAggregation {
257    total_reference_count: usize,
258    overall_start_epoch_inclusive: Epoch,
259    overall_end_epoch_exclusive: Epoch,
260    overall_start_timestamp_inclusive: Option<Instant>,
261    overall_end_timestamp_exclusive: Option<Instant>,
262}
263
264impl AcrossIntentAggregation {
265    pub fn start() -> Self {
266        Self {
267            total_reference_count: 0,
268            overall_start_epoch_inclusive: Epoch::zero(),
269            overall_end_epoch_exclusive: Epoch::of(u64::MAX),
270            overall_start_timestamp_inclusive: None,
271            overall_end_timestamp_exclusive: None,
272        }
273    }
274
275    pub fn finalize(
276        self,
277        config: &TransactionValidationConfig,
278    ) -> Result<OverallValidityRangeV2, TransactionValidationError> {
279        if self.total_reference_count > config.max_total_references {
280            return Err(TransactionValidationError::IntentValidationError(
281                TransactionValidationErrorLocation::AcrossTransaction,
282                IntentValidationError::TooManyReferences {
283                    total: self.total_reference_count,
284                    limit: config.max_total_references,
285                },
286            ));
287        }
288        Ok(OverallValidityRangeV2 {
289            epoch_range: EpochRange {
290                start_epoch_inclusive: self.overall_start_epoch_inclusive,
291                end_epoch_exclusive: self.overall_end_epoch_exclusive,
292            },
293            proposer_timestamp_range: ProposerTimestampRange {
294                start_timestamp_inclusive: self.overall_start_timestamp_inclusive,
295                end_timestamp_exclusive: self.overall_end_timestamp_exclusive,
296            },
297        })
298    }
299
300    pub fn record_reference_count(
301        &mut self,
302        count: usize,
303        config: &TransactionValidationConfig,
304    ) -> Result<(), IntentValidationError> {
305        if count > config.max_references_per_intent {
306            return Err(IntentValidationError::TooManyReferences {
307                total: count,
308                limit: config.max_references_per_intent,
309            });
310        }
311        self.total_reference_count = self.total_reference_count.saturating_add(count);
312        Ok(())
313    }
314
315    pub fn update_headers(
316        &mut self,
317        start_epoch_inclusive: Epoch,
318        end_epoch_exclusive: Epoch,
319        start_timestamp_inclusive: Option<&Instant>,
320        end_timestamp_exclusive: Option<&Instant>,
321    ) -> Result<(), HeaderValidationError> {
322        if start_epoch_inclusive > self.overall_start_epoch_inclusive {
323            self.overall_start_epoch_inclusive = start_epoch_inclusive;
324        }
325        if end_epoch_exclusive < self.overall_end_epoch_exclusive {
326            self.overall_end_epoch_exclusive = end_epoch_exclusive;
327        }
328        if self.overall_start_epoch_inclusive >= self.overall_end_epoch_exclusive {
329            return Err(HeaderValidationError::NoValidEpochRangeAcrossAllIntents);
330        }
331        if let Some(start_timestamp_inclusive) = start_timestamp_inclusive {
332            if self.overall_start_timestamp_inclusive.is_none()
333                || self
334                    .overall_start_timestamp_inclusive
335                    .as_ref()
336                    .is_some_and(|t| start_timestamp_inclusive > t)
337            {
338                self.overall_start_timestamp_inclusive = Some(*start_timestamp_inclusive);
339            }
340        }
341        if let Some(end_timestamp_exclusive) = end_timestamp_exclusive {
342            if self.overall_end_timestamp_exclusive.is_none()
343                || self
344                    .overall_end_timestamp_exclusive
345                    .as_ref()
346                    .is_some_and(|t| end_timestamp_exclusive < t)
347            {
348                self.overall_end_timestamp_exclusive = Some(*end_timestamp_exclusive);
349            }
350        }
351        match (
352            self.overall_start_timestamp_inclusive.as_ref(),
353            self.overall_end_timestamp_exclusive.as_ref(),
354        ) {
355            (Some(start_inclusive), Some(end_exclusive)) => {
356                if start_inclusive >= end_exclusive {
357                    return Err(HeaderValidationError::NoValidTimestampRangeAcrossAllIntents);
358                }
359            }
360            _ => {}
361        }
362        Ok(())
363    }
364}
365
366pub struct IntentRelationships {
367    pub root_intent: RootIntentRelationshipDetails,
368    pub non_root_subintents: IndexMap<SubintentHash, SubintentRelationshipDetails>,
369}
370
371#[derive(Default)]
372pub struct RootIntentRelationshipDetails {
373    pub children: Vec<SubintentIndex>,
374}
375
376pub struct SubintentRelationshipDetails {
377    pub index: SubintentIndex,
378    pub parent: IntentHash,
379    pub depth: usize,
380    pub children: Vec<SubintentIndex>,
381}
382
383impl SubintentRelationshipDetails {
384    fn default_for(index: SubintentIndex) -> Self {
385        Self {
386            index,
387            parent: PLACEHOLDER_PARENT,
388            depth: Default::default(),
389            children: Default::default(),
390        }
391    }
392}
393
394const PLACEHOLDER_PARENT: IntentHash =
395    IntentHash::Transaction(TransactionIntentHash(Hash([0u8; Hash::LENGTH])));
396
397// This type is public so it can be used by the toolkit.
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct ManifestYieldSummary {
400    pub parent_yields: usize,
401    pub child_yields: IndexMap<SubintentHash, usize>,
402}
403
404impl ManifestYieldSummary {
405    pub fn new_with_children(children: impl Iterator<Item = SubintentHash>) -> Self {
406        Self {
407            parent_yields: 0,
408            child_yields: children.map(|child| (child, 0)).collect(),
409        }
410    }
411}
412
413impl ManifestInterpretationVisitor for ManifestYieldSummary {
414    type Output = ManifestValidationError;
415
416    fn on_end_instruction(&mut self, details: OnEndInstruction) -> ControlFlow<Self::Output> {
417        // Safe from overflow due to checking max instruction count
418        match details.effect {
419            ManifestInstructionEffect::Invocation {
420                kind: InvocationKind::YieldToParent,
421                ..
422            } => {
423                self.parent_yields += 1;
424            }
425            ManifestInstructionEffect::Invocation {
426                kind:
427                    InvocationKind::YieldToChild {
428                        child_index: ManifestNamedIntent(index),
429                    },
430                ..
431            } => {
432                let index = index as usize;
433
434                // This should exist because we are handling this after the instruction,
435                // so the interpreter should have errored with ChildIntentNotRegistered
436                // if the child yield was invalid.
437                let (_, count) = self.child_yields.get_index_mut(index).unwrap();
438                *count += 1;
439            }
440            _ => {}
441        }
442        ControlFlow::Continue(())
443    }
444}