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