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<'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 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 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 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 {
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 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 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 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#[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#[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 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 let (_, count) = self.child_yields.get_index_mut(index).unwrap();
438 *count += 1;
439 }
440 _ => {}
441 }
442 ControlFlow::Continue(())
443 }
444}