radix_transactions/validation/
transaction_structure_validator.rs1use 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 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 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 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 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 {
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 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 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 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#[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#[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 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 let (_, count) = self.child_yields.get_index_mut(index).unwrap();
436 *count += 1;
437 }
438 _ => {}
439 }
440 ControlFlow::Continue(())
441 }
442}