1use serde::{ser::SerializeSeq, Serialize, Serializer};
2use std::collections::BTreeSet;
3use std::sync::{Arc, OnceLock};
4
5use super::super::{
6 BatchInvocationId, BatchStepId, DeviceId, ExecutionIdentityEnvelope, ExecutionLaneId, NodeId,
7 OperationId, ParticipantNodeKey, PlanHash, PlanId, ProviderId, VNextError,
8};
9use super::compiled_identity::{
10 CompiledSubmissionWaveIdentity, SubmissionWaveParticipantIdentitySeed,
11};
12use super::foundation::{canonical_operation_fingerprint, canonical_sha256, invalid_operation};
13use super::ProviderExecutionSemantics;
14
15#[derive(Debug, PartialEq, Eq, Serialize)]
16struct BatchOperationParticipantIdentityData {
17 participant_index: u32,
18 node_key: ParticipantNodeKey,
19 identity: ExecutionIdentityEnvelope,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct BatchOperationParticipantIdentity {
24 data: Arc<BatchOperationParticipantIdentityData>,
25}
26
27impl Serialize for BatchOperationParticipantIdentity {
28 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29 where
30 S: Serializer,
31 {
32 self.data.as_ref().serialize(serializer)
33 }
34}
35
36impl BatchOperationParticipantIdentity {
37 pub(super) fn new(
38 participant_index: u32,
39 node_key: ParticipantNodeKey,
40 identity: ExecutionIdentityEnvelope,
41 ) -> Self {
42 Self {
43 data: Arc::new(BatchOperationParticipantIdentityData {
44 participant_index,
45 node_key,
46 identity,
47 }),
48 }
49 }
50
51 pub fn participant_index(&self) -> u32 {
52 self.data.participant_index
53 }
54
55 pub fn node_key(&self) -> &ParticipantNodeKey {
56 &self.data.node_key
57 }
58
59 pub fn identity(&self) -> &ExecutionIdentityEnvelope {
60 &self.data.identity
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67pub struct BatchOperationNodeIdentity {
68 node_index: u32,
69 node_id: NodeId,
70 operation_id: OperationId,
71 provider_id: ProviderId,
72 provider_implementation_fingerprint: String,
73 provider_execution_semantics: ProviderExecutionSemantics,
74 work_shape_fingerprint: String,
75 participants: Vec<BatchOperationParticipantIdentity>,
76 fingerprint: String,
77}
78
79impl BatchOperationNodeIdentity {
80 pub(super) fn from_validated(
81 node_index: u32,
82 node_id: NodeId,
83 operation_id: OperationId,
84 provider_id: ProviderId,
85 provider_implementation_fingerprint: String,
86 provider_execution_semantics: ProviderExecutionSemantics,
87 work_shape_fingerprint: String,
88 participants: Vec<BatchOperationParticipantIdentity>,
89 ) -> Result<Self, VNextError> {
90 let participant_start = participants
91 .first()
92 .map(BatchOperationParticipantIdentity::participant_index);
93 if participants.is_empty()
94 || participants.iter().enumerate().any(|(index, participant)| {
95 participant_start.and_then(|start| start.checked_add(index as u32))
96 != Some(participant.participant_index())
97 || participant.node_key().node_id() != &node_id
98 || participant.identity().parts().frame_id
99 != Some(participant.node_key().frame_id())
100 || participant.identity().parts().node_id.as_ref() != Some(&node_id)
101 || participant.identity().parts().operation_id.as_ref() != Some(&operation_id)
102 || participant.identity().parts().provider_id.as_ref() != Some(&provider_id)
103 })
104 || participants
105 .windows(2)
106 .any(|pair| pair[0].node_key() >= pair[1].node_key())
107 || !canonical_sha256(&provider_implementation_fingerprint)
108 || !canonical_sha256(&work_shape_fingerprint)
109 {
110 return Err(invalid_operation(
111 "batch node identity is empty, non-canonical, or differs from its participant projections",
112 ));
113 }
114 #[derive(Serialize)]
115 struct FingerprintInput<'a> {
116 domain: &'static str,
117 node_index: u32,
118 node_id: &'a NodeId,
119 operation_id: &'a OperationId,
120 provider_id: &'a ProviderId,
121 provider_implementation_fingerprint: &'a str,
122 provider_execution_semantics: ProviderExecutionSemantics,
123 work_shape_fingerprint: &'a str,
124 participants: &'a [BatchOperationParticipantIdentity],
125 }
126 let fingerprint = canonical_operation_fingerprint(
127 &FingerprintInput {
128 domain: "ferrum.runtime-vnext.batch-operation-node-identity.v2",
129 node_index,
130 node_id: &node_id,
131 operation_id: &operation_id,
132 provider_id: &provider_id,
133 provider_implementation_fingerprint: &provider_implementation_fingerprint,
134 provider_execution_semantics,
135 work_shape_fingerprint: &work_shape_fingerprint,
136 participants: &participants,
137 },
138 "batch node identity encode failed",
139 )?;
140 Ok(Self {
141 node_index,
142 node_id,
143 operation_id,
144 provider_id,
145 provider_implementation_fingerprint,
146 provider_execution_semantics,
147 work_shape_fingerprint,
148 participants,
149 fingerprint,
150 })
151 }
152
153 pub const fn node_index(&self) -> u32 {
154 self.node_index
155 }
156
157 pub fn node_id(&self) -> &NodeId {
158 &self.node_id
159 }
160
161 pub fn operation_id(&self) -> &OperationId {
162 &self.operation_id
163 }
164
165 pub fn provider_id(&self) -> &ProviderId {
166 &self.provider_id
167 }
168
169 pub fn provider_implementation_fingerprint(&self) -> &str {
170 &self.provider_implementation_fingerprint
171 }
172
173 pub const fn provider_execution_semantics(&self) -> ProviderExecutionSemantics {
174 self.provider_execution_semantics
175 }
176
177 pub fn work_shape_fingerprint(&self) -> &str {
178 &self.work_shape_fingerprint
179 }
180
181 pub fn participants(&self) -> &[BatchOperationParticipantIdentity] {
182 &self.participants
183 }
184
185 pub fn fingerprint(&self) -> &str {
186 &self.fingerprint
187 }
188
189 pub(super) fn contains_identity(&self, identity: &ExecutionIdentityEnvelope) -> bool {
190 self.participants
191 .iter()
192 .any(|participant| participant.identity() == identity)
193 }
194}
195
196#[derive(Debug)]
199struct BatchOperationIdentityData {
200 batch_step_id: BatchStepId,
201 batch_invocation_id: BatchInvocationId,
202 plan_id: PlanId,
203 plan_hash: PlanHash,
204 device_id: DeviceId,
205 runtime_implementation_fingerprint: String,
206 lane_id: ExecutionLaneId,
207 claimed_backing_fingerprint: String,
208 nodes: OnceLock<Vec<BatchOperationNodeIdentity>>,
209 participants: OnceLock<Vec<BatchOperationParticipantIdentity>>,
210 deferred_recipe: Option<DeferredBatchOperationIdentityRecipe>,
211 fingerprint: String,
212}
213
214#[derive(Debug, Clone)]
215pub struct BatchOperationIdentity {
216 data: Arc<BatchOperationIdentityData>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
220pub struct BatchOperationIdentityMaterializationSnapshot {
221 logical_nodes: u32,
222 materialized_nodes: u32,
223 full_participant_projection: bool,
224}
225
226impl BatchOperationIdentityMaterializationSnapshot {
227 pub const fn logical_nodes(self) -> u32 {
228 self.logical_nodes
229 }
230
231 pub const fn materialized_nodes(self) -> u32 {
232 self.materialized_nodes
233 }
234
235 pub const fn full_participant_projection(self) -> bool {
236 self.full_participant_projection
237 }
238}
239
240impl PartialEq for BatchOperationIdentity {
241 fn eq(&self, other: &Self) -> bool {
242 self.data.batch_step_id == other.data.batch_step_id
243 && self.data.batch_invocation_id == other.data.batch_invocation_id
244 && self.data.plan_id == other.data.plan_id
245 && self.data.plan_hash == other.data.plan_hash
246 && self.data.device_id == other.data.device_id
247 && self.data.runtime_implementation_fingerprint
248 == other.data.runtime_implementation_fingerprint
249 && self.data.lane_id == other.data.lane_id
250 && self.data.claimed_backing_fingerprint == other.data.claimed_backing_fingerprint
251 && self.data.fingerprint == other.data.fingerprint
252 }
253}
254
255impl Eq for BatchOperationIdentity {}
256
257impl Serialize for BatchOperationIdentity {
258 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
259 where
260 S: Serializer,
261 {
262 #[derive(Serialize)]
263 struct Wire<'a> {
264 batch_step_id: BatchStepId,
265 batch_invocation_id: BatchInvocationId,
266 plan_id: &'a PlanId,
267 plan_hash: &'a PlanHash,
268 device_id: &'a DeviceId,
269 runtime_implementation_fingerprint: &'a str,
270 lane_id: ExecutionLaneId,
271 claimed_backing_fingerprint: &'a str,
272 nodes: &'a [BatchOperationNodeIdentity],
273 participants: &'a [BatchOperationParticipantIdentity],
274 fingerprint: &'a str,
275 }
276
277 Wire {
278 batch_step_id: self.data.batch_step_id,
279 batch_invocation_id: self.data.batch_invocation_id,
280 plan_id: &self.data.plan_id,
281 plan_hash: &self.data.plan_hash,
282 device_id: &self.data.device_id,
283 runtime_implementation_fingerprint: &self.data.runtime_implementation_fingerprint,
284 lane_id: self.data.lane_id,
285 claimed_backing_fingerprint: &self.data.claimed_backing_fingerprint,
286 nodes: self.nodes(),
287 participants: self.participants(),
288 fingerprint: &self.data.fingerprint,
289 }
290 .serialize(serializer)
291 }
292}
293
294struct BatchOperationNodeFingerprints<'a>(&'a [BatchOperationNodeIdentity]);
295
296impl Serialize for BatchOperationNodeFingerprints<'_> {
297 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
298 where
299 S: Serializer,
300 {
301 let mut sequence = serializer.serialize_seq(Some(self.0.len()))?;
302 for node in self.0 {
303 sequence.serialize_element(node.fingerprint())?;
304 }
305 sequence.end()
306 }
307}
308
309impl BatchOperationIdentity {
310 #[allow(clippy::too_many_arguments)]
311 fn from_deferred_validated(
312 batch_step_id: BatchStepId,
313 batch_invocation_id: BatchInvocationId,
314 plan_id: PlanId,
315 plan_hash: PlanHash,
316 device_id: DeviceId,
317 runtime_implementation_fingerprint: String,
318 lane_id: ExecutionLaneId,
319 claimed_backing_fingerprint: String,
320 deferred_recipe: DeferredBatchOperationIdentityRecipe,
321 fingerprint: String,
322 ) -> Self {
323 Self {
324 data: Arc::new(BatchOperationIdentityData {
325 batch_step_id,
326 batch_invocation_id,
327 plan_id,
328 plan_hash,
329 device_id,
330 runtime_implementation_fingerprint,
331 lane_id,
332 claimed_backing_fingerprint,
333 nodes: OnceLock::new(),
334 participants: OnceLock::new(),
335 deferred_recipe: Some(deferred_recipe),
336 fingerprint,
337 }),
338 }
339 }
340
341 #[allow(clippy::too_many_arguments)]
342 pub(super) fn from_validated(
343 batch_step_id: BatchStepId,
344 batch_invocation_id: BatchInvocationId,
345 plan_id: PlanId,
346 plan_hash: PlanHash,
347 device_id: DeviceId,
348 runtime_implementation_fingerprint: String,
349 lane_id: ExecutionLaneId,
350 claimed_backing_fingerprint: String,
351 nodes: Vec<BatchOperationNodeIdentity>,
352 ) -> Result<Self, VNextError> {
353 if nodes.is_empty()
354 || nodes.iter().enumerate().any(|(index, node)| {
355 node.node_index as usize != index
356 || node.participants.iter().any(|participant| {
357 participant.identity().parts().plan_id.as_ref() != Some(&plan_id)
358 || participant.identity().parts().plan_hash.as_ref() != Some(&plan_hash)
359 || participant.identity().parts().device_id.as_ref() != Some(&device_id)
360 || participant
361 .identity()
362 .parts()
363 .runtime_implementation_fingerprint
364 .as_deref()
365 != Some(runtime_implementation_fingerprint.as_str())
366 })
367 })
368 || nodes
369 .iter()
370 .map(BatchOperationNodeIdentity::node_id)
371 .collect::<BTreeSet<_>>()
372 .len()
373 != nodes.len()
374 || !canonical_sha256(&runtime_implementation_fingerprint)
375 || !canonical_sha256(&claimed_backing_fingerprint)
376 {
377 return Err(invalid_operation(
378 "physical batch identity is empty, non-canonical, or differs from its plan/runtime projections",
379 ));
380 }
381 let participants = nodes
382 .iter()
383 .flat_map(|node| node.participants.iter().cloned())
384 .collect::<Vec<_>>();
385 if participants
386 .iter()
387 .enumerate()
388 .any(|(index, participant)| participant.participant_index() as usize != index)
389 {
390 return Err(invalid_operation(
391 "physical batch participant indices are not globally contiguous",
392 ));
393 }
394 #[derive(Serialize)]
395 struct FingerprintInput<'a> {
396 domain: &'static str,
397 batch_step_id: BatchStepId,
398 batch_invocation_id: BatchInvocationId,
399 plan_id: &'a PlanId,
400 plan_hash: &'a PlanHash,
401 device_id: &'a DeviceId,
402 runtime_implementation_fingerprint: &'a str,
403 lane_id: ExecutionLaneId,
404 claimed_backing_fingerprint: &'a str,
405 node_fingerprints: BatchOperationNodeFingerprints<'a>,
406 }
407 let fingerprint = canonical_operation_fingerprint(
408 &FingerprintInput {
409 domain: "ferrum.runtime-vnext.physical-command-batch-identity.v2",
410 batch_step_id,
411 batch_invocation_id,
412 plan_id: &plan_id,
413 plan_hash: &plan_hash,
414 device_id: &device_id,
415 runtime_implementation_fingerprint: &runtime_implementation_fingerprint,
416 lane_id,
417 claimed_backing_fingerprint: &claimed_backing_fingerprint,
418 node_fingerprints: BatchOperationNodeFingerprints(&nodes),
419 },
420 "physical batch identity encode failed",
421 )?;
422 Ok(Self {
423 data: Arc::new(BatchOperationIdentityData {
424 batch_step_id,
425 batch_invocation_id,
426 plan_id,
427 plan_hash,
428 device_id,
429 runtime_implementation_fingerprint,
430 lane_id,
431 claimed_backing_fingerprint,
432 nodes: OnceLock::from(nodes),
433 participants: OnceLock::from(participants),
434 deferred_recipe: None,
435 fingerprint,
436 }),
437 })
438 }
439
440 pub fn batch_step_id(&self) -> BatchStepId {
441 self.data.batch_step_id
442 }
443
444 pub fn batch_invocation_id(&self) -> BatchInvocationId {
445 self.data.batch_invocation_id
446 }
447
448 pub fn plan_id(&self) -> &PlanId {
449 &self.data.plan_id
450 }
451
452 pub fn plan_hash(&self) -> &PlanHash {
453 &self.data.plan_hash
454 }
455
456 pub fn device_id(&self) -> &DeviceId {
457 &self.data.device_id
458 }
459
460 pub fn runtime_implementation_fingerprint(&self) -> &str {
461 &self.data.runtime_implementation_fingerprint
462 }
463
464 pub fn lane_id(&self) -> ExecutionLaneId {
465 self.data.lane_id
466 }
467
468 pub fn claimed_backing_fingerprint(&self) -> &str {
469 &self.data.claimed_backing_fingerprint
470 }
471
472 pub fn node_count(&self) -> usize {
473 self.data.nodes.get().map_or_else(
474 || {
475 self.data
476 .deferred_recipe
477 .as_ref()
478 .map_or(0, |recipe| recipe.topology.node_count())
479 },
480 Vec::len,
481 )
482 }
483
484 fn materialized_node_count(&self) -> usize {
485 self.data.nodes.get().map_or_else(
486 || {
487 self.data.deferred_recipe.as_ref().map_or(0, |recipe| {
488 recipe
489 .node_identities
490 .iter()
491 .filter(|identity| identity.get().is_some())
492 .count()
493 })
494 },
495 Vec::len,
496 )
497 }
498
499 pub fn materialization_snapshot(&self) -> BatchOperationIdentityMaterializationSnapshot {
500 BatchOperationIdentityMaterializationSnapshot {
501 logical_nodes: u32::try_from(self.node_count())
502 .expect("validated physical batch node count fits u32"),
503 materialized_nodes: u32::try_from(self.materialized_node_count())
504 .expect("materialized physical batch node count fits u32"),
505 full_participant_projection: self.data.participants.get().is_some(),
506 }
507 }
508
509 pub fn node_participant_count(&self, node_index: usize) -> Option<usize> {
510 if let Some(nodes) = self.data.nodes.get() {
511 return nodes.get(node_index).map(|node| node.participants().len());
512 }
513 let recipe = self.data.deferred_recipe.as_ref()?;
514 (node_index < recipe.topology.node_count()).then_some(recipe.participant_seeds.len())
515 }
516
517 pub fn node_id_at(&self, node_index: usize) -> Option<&NodeId> {
518 if let Some(nodes) = self.data.nodes.get() {
519 return nodes
520 .get(node_index)
521 .map(BatchOperationNodeIdentity::node_id);
522 }
523 self.data
524 .deferred_recipe
525 .as_ref()?
526 .topology
527 .node_id_at(node_index)
528 }
529
530 pub fn operation_id_at(&self, node_index: usize) -> Option<&OperationId> {
531 if let Some(nodes) = self.data.nodes.get() {
532 return nodes
533 .get(node_index)
534 .map(BatchOperationNodeIdentity::operation_id);
535 }
536 self.data
537 .deferred_recipe
538 .as_ref()?
539 .topology
540 .operation_id_at(node_index)
541 }
542
543 pub fn provider_id_at(&self, node_index: usize) -> Option<&ProviderId> {
544 if let Some(nodes) = self.data.nodes.get() {
545 return nodes
546 .get(node_index)
547 .map(BatchOperationNodeIdentity::provider_id);
548 }
549 self.data
550 .deferred_recipe
551 .as_ref()?
552 .topology
553 .provider_id_at(node_index)
554 }
555
556 pub fn work_shape_fingerprint_at(&self, node_index: usize) -> Option<&str> {
557 if let Some(nodes) = self.data.nodes.get() {
558 return nodes
559 .get(node_index)
560 .map(BatchOperationNodeIdentity::work_shape_fingerprint);
561 }
562 let recipe = self.data.deferred_recipe.as_ref()?;
563 (node_index < recipe.topology.node_count()).then_some(recipe.work_shape_fingerprint())
564 }
565
566 pub fn node_index(&self, node_id: &NodeId) -> Option<usize> {
567 if let Some(nodes) = self.data.nodes.get() {
568 return nodes.iter().position(|node| node.node_id() == node_id);
569 }
570 self.data
571 .deferred_recipe
572 .as_ref()?
573 .topology
574 .node_index(node_id)
575 }
576
577 pub(crate) fn materialize_node(
578 &self,
579 node_index: usize,
580 ) -> Result<&BatchOperationNodeIdentity, VNextError> {
581 if let Some(nodes) = self.data.nodes.get() {
582 return nodes
583 .get(node_index)
584 .ok_or_else(|| invalid_operation("physical batch node index is out of bounds"));
585 }
586 let recipe = self.data.deferred_recipe.as_ref().ok_or_else(|| {
587 invalid_operation("physical batch has neither materialized nodes nor a compiled recipe")
588 })?;
589 let slot = recipe.node_identities.get(node_index).ok_or_else(|| {
590 invalid_operation("compiled physical batch node index is out of bounds")
591 })?;
592 if let Some(identity) = slot.get() {
593 return Ok(identity);
594 }
595 let identity = recipe.materialize_node(node_index)?;
596 let _ = slot.set(identity);
597 slot.get().ok_or_else(|| {
598 invalid_operation("compiled physical batch node identity publication failed")
599 })
600 }
601
602 pub fn nodes(&self) -> &[BatchOperationNodeIdentity] {
603 self.data.nodes.get_or_init(|| {
604 (0..self.node_count())
605 .map(|node_index| {
606 self.materialize_node(node_index)
607 .expect("validated compiled physical batch node must materialize")
608 .clone()
609 })
610 .collect()
611 })
612 }
613
614 pub fn single_node(&self) -> Option<&BatchOperationNodeIdentity> {
615 (self.node_count() == 1).then(|| {
616 self.materialize_node(0)
617 .expect("validated single-node physical batch must materialize")
618 })
619 }
620
621 pub fn participants(&self) -> &[BatchOperationParticipantIdentity] {
622 self.data.participants.get_or_init(|| {
623 self.nodes()
624 .iter()
625 .flat_map(|node| node.participants().iter().cloned())
626 .collect()
627 })
628 }
629
630 pub fn fingerprint(&self) -> &str {
631 &self.data.fingerprint
632 }
633
634 pub(super) fn contains_identity(&self, identity: &ExecutionIdentityEnvelope) -> bool {
635 self.participants()
636 .iter()
637 .any(|participant| participant.identity() == identity)
638 }
639}
640
641#[derive(Debug)]
642struct DeferredBatchOperationIdentityRecipe {
643 topology: CompiledSubmissionWaveIdentity,
644 work_shape_fingerprint: String,
645 participant_seeds: Vec<SubmissionWaveParticipantIdentitySeed>,
646 node_identities: Box<[OnceLock<BatchOperationNodeIdentity>]>,
647}
648
649impl DeferredBatchOperationIdentityRecipe {
650 fn work_shape_fingerprint(&self) -> &str {
651 &self.work_shape_fingerprint
652 }
653
654 fn materialize_node(
655 &self,
656 node_index: usize,
657 ) -> Result<BatchOperationNodeIdentity, VNextError> {
658 let node = self
659 .topology
660 .node_at(node_index)
661 .ok_or_else(|| invalid_operation("compiled wave node index is out of bounds"))?;
662 let participant_start = node_index
663 .checked_mul(self.participant_seeds.len())
664 .and_then(|value| u32::try_from(value).ok())
665 .ok_or_else(|| {
666 invalid_operation("compiled wave participant index space exceeds u32")
667 })?;
668 let participants = self
669 .participant_seeds
670 .iter()
671 .enumerate()
672 .map(|(local_index, seed)| {
673 let local_index = u32::try_from(local_index)
674 .expect("compiled wave participant count was validated");
675 let frame = seed.frame();
676 let identity = seed
677 .operation_identity(&self.topology, node_index)
678 .ok_or_else(|| invalid_operation("compiled wave node identity disappeared"))?;
679 Ok(BatchOperationParticipantIdentity::new(
680 participant_start
681 .checked_add(local_index)
682 .expect("compiled wave participant index was validated"),
683 ParticipantNodeKey::new(
684 frame.participant(),
685 frame.frame_id(),
686 node.node_id().clone(),
687 ),
688 identity,
689 ))
690 })
691 .collect::<Result<Vec<_>, VNextError>>()?;
692 BatchOperationNodeIdentity::from_validated(
693 node.node_index(),
694 node.node_id().clone(),
695 node.operation_id().clone(),
696 node.provider_id().clone(),
697 node.provider_implementation_fingerprint().to_owned(),
698 node.provider_execution_semantics(),
699 self.work_shape_fingerprint.clone(),
700 participants,
701 )
702 }
703}
704
705impl BatchOperationIdentity {
706 #[allow(clippy::too_many_arguments)]
707 pub(super) fn from_compiled_wave(
708 topology: CompiledSubmissionWaveIdentity,
709 batch_step_id: BatchStepId,
710 batch_invocation_id: BatchInvocationId,
711 claimed_backing_fingerprint: String,
712 work_shape_fingerprint: String,
713 participant_seeds: Vec<SubmissionWaveParticipantIdentitySeed>,
714 ) -> Result<Self, VNextError> {
715 let participant_count = participant_seeds.len();
716 let participant_projection_count = topology
717 .node_count()
718 .checked_mul(participant_count)
719 .and_then(|count| u32::try_from(count).ok());
720 if topology.node_count() == 0
721 || participant_count == 0
722 || participant_projection_count.is_none()
723 || participant_seeds.windows(2).any(|pair| {
724 let left = pair[0].frame().participant();
725 let right = pair[1].frame().participant();
726 (
727 left.sequence_authority().sparse_id(),
728 left.sequence_authority().generation(),
729 left.request_authority().sparse_id(),
730 left.request_authority().generation(),
731 ) >= (
732 right.sequence_authority().sparse_id(),
733 right.sequence_authority().generation(),
734 right.request_authority().sparse_id(),
735 right.request_authority().generation(),
736 )
737 })
738 || participant_seeds.iter().any(|seed| {
739 seed.runtime_implementation_fingerprint()
740 != topology.runtime_implementation_fingerprint()
741 })
742 || !canonical_sha256(&claimed_backing_fingerprint)
743 || !canonical_sha256(&work_shape_fingerprint)
744 {
745 return Err(invalid_operation(
746 "compiled physical batch identity is empty, non-canonical, or exceeds its participant index space",
747 ));
748 }
749 #[derive(Serialize)]
750 struct FingerprintInput<'a> {
751 domain: &'static str,
752 batch_step_id: BatchStepId,
753 batch_invocation_id: BatchInvocationId,
754 topology_fingerprint: &'a str,
755 claimed_backing_fingerprint: &'a str,
756 work_shape_fingerprint: &'a str,
757 participant_seeds: &'a [SubmissionWaveParticipantIdentitySeed],
758 }
759 let fingerprint = canonical_operation_fingerprint(
760 &FingerprintInput {
761 domain: "ferrum.runtime-vnext.compiled-physical-command-batch-identity.v1",
762 batch_step_id,
763 batch_invocation_id,
764 topology_fingerprint: topology.fingerprint(),
765 claimed_backing_fingerprint: &claimed_backing_fingerprint,
766 work_shape_fingerprint: &work_shape_fingerprint,
767 participant_seeds: &participant_seeds,
768 },
769 "compiled physical batch identity encode failed",
770 )?;
771 let node_identities = std::iter::repeat_with(OnceLock::new)
772 .take(topology.node_count())
773 .collect::<Vec<_>>()
774 .into_boxed_slice();
775 let plan_id = topology.plan_id().clone();
776 let plan_hash = topology.plan_hash().clone();
777 let device_id = topology.device_id().clone();
778 let runtime_implementation_fingerprint =
779 topology.runtime_implementation_fingerprint().to_owned();
780 let lane_id = topology.lane_id();
781 Ok(Self::from_deferred_validated(
782 batch_step_id,
783 batch_invocation_id,
784 plan_id,
785 plan_hash,
786 device_id,
787 runtime_implementation_fingerprint,
788 lane_id,
789 claimed_backing_fingerprint,
790 DeferredBatchOperationIdentityRecipe {
791 topology,
792 work_shape_fingerprint,
793 participant_seeds,
794 node_identities,
795 },
796 fingerprint,
797 ))
798 }
799}
800
801#[cfg(test)]
802mod batch_operation_identity_fingerprint_tests {
803 use super::{
804 canonical_operation_fingerprint, BatchOperationNodeFingerprints,
805 BatchOperationNodeIdentity, NodeId, OperationId, ProviderExecutionSemantics, ProviderId,
806 Serialize,
807 };
808 use sha2::{Digest, Sha256};
809
810 fn fingerprint_node(index: u32, marker: char) -> BatchOperationNodeIdentity {
811 BatchOperationNodeIdentity {
812 node_index: index,
813 node_id: NodeId::new(format!("node.{index}")).unwrap(),
814 operation_id: OperationId::new(format!("operation.{index}")).unwrap(),
815 provider_id: ProviderId::new(format!("provider.{index}")).unwrap(),
816 provider_implementation_fingerprint: std::iter::repeat_n(marker, 64).collect(),
817 provider_execution_semantics: ProviderExecutionSemantics::bitwise_eager_and_replay(),
818 work_shape_fingerprint: std::iter::repeat_n(marker, 64).collect(),
819 participants: Vec::new(),
820 fingerprint: std::iter::repeat_n(marker, 64).collect(),
821 }
822 }
823
824 #[test]
825 fn streaming_fingerprint_matches_canonical_json_digest() {
826 #[derive(Serialize)]
827 struct Input<'a> {
828 domain: &'static str,
829 value: &'a str,
830 }
831
832 let input = Input {
833 domain: "ferrum.runtime-vnext.test",
834 value: "evidence",
835 };
836 let expected = format!("{:x}", Sha256::digest(serde_json::to_vec(&input).unwrap()));
837
838 assert_eq!(
839 canonical_operation_fingerprint(&input, "test fingerprint").unwrap(),
840 expected
841 );
842 }
843
844 #[test]
845 fn batch_fingerprint_projection_contains_only_validated_node_digests() {
846 let first = std::iter::repeat_n('a', 64).collect::<String>();
847 let second = std::iter::repeat_n('b', 64).collect::<String>();
848 let nodes = [fingerprint_node(0, 'a'), fingerprint_node(1, 'b')];
849
850 let encoded = serde_json::to_string(&BatchOperationNodeFingerprints(&nodes)).unwrap();
851
852 assert_eq!(encoded, format!("[\"{first}\",\"{second}\"]"));
853 assert!(!encoded.contains("node.0"));
854 assert!(!encoded.contains("operation.0"));
855 assert!(!encoded.contains("participants"));
856 }
857}